[PYTHON] Draw a graph that can be moved around with HoloViews and Bokeh

We are planning to introduce python in the workplace. If I was looking for something I couldn't do with Excel and something I could do with Python, You can draw ** interactive graphs ** using a library called ** Bokeh **. I researched various things together with ** HoloViews ** that I was interested in in the article of here, so I summarized it.

What is an interactive graph?

Refers to a graph that responds to user operations. (Interactive = bidirectional) holoviewデモ.gif You can move around with the mouse, zoom, and display information like this.

environment

For the time being, I wanted to get started quickly, so I implemented it with Google Colaboratory. Therefore, the description about library installation is omitted in this article. (Unverified, but if you install the library, it should work with Jupyter Notebook)

data

Prepare the following data of the first Pokemon in DataFrame format. 20201230_085515.png Data is taken from the official Pokédex and Pokemon Wiki.

Implementation

Basic

How to draw a basic graph. This time I will write a scatter graph.

#Import of graph related library
import holoviews as hv
hv.extension('bokeh')

#Basic drawing
poke_graph = hv.Scatter(poke_df,kdims=["height"],vdims=["weight"])
#display
poke_graph

20201230_090817.png Extend holoviews to bokeh mode with hv.extension ('bokeh'). ( The graph will not be displayed unless the above is described for each cell ) Create a scatter plot instance with hv.Scatter (). In the case of Scatter, specify the dataset to be used and ** kdims ** and ** vdims ** as arguments. kdims: key dimensions. Key element. x-axis image. vdims: value dimensions. Value element corresponding to the above key. In addition to the y-axis, it can also be applied to the optional values ​​described below.

The display can be displayed just by writing the instance name like pandas. (Jupyter Notebook requires a spell called output_notebook () to display the graph)

Setting options

holoviews can add graph options later.

hv.extension('bokeh')

#Title setting
poke_graph.opts(title="Pokemon data")
#Grid drawing
poke_graph.opts(show_grid=True)
#Change axis label
poke_graph.opts(xlabel="height", ylabel="Weight")

20201230_091008.png Set various options with instance.opts (…). It is set individually for the sake of clarity, but it can also be set collectively with a single .opts. poke_graph.opts (title =" Pokemon data ", show_grid = True, xlabel =" Takasa ", ylabel =" Weight ")

The settings related to markers look like this

hv.extension('bokeh')

#Marker color setting
poke_graph.opts(color = "red")
#Marker edge color setting
poke_graph.opts(line_color = "black")
#Marker size setting
poke_graph.opts(size=7)
#Marker transparency setting
poke_graph.opts(alpha = 0.8)

20201230_091145.png

The above is a bit cramped, so I added settings to make it easier to see.

hv.extension('bokeh')

#Background color setting
poke_graph.opts(bgcolor = "whitesmoke")
#Setting the drawing size of the graph area
poke_graph.opts(width=640, height=480)
#Font size is uniform and large
poke_graph.opts(fontscale=1.5)
#Axis format change
poke_graph.opts(xformatter="%.1f m", yformatter="%.1f kg")

20201230_091500.png

Apply elements to options

Application to marker size

Here, let's apply the total race value to the size of the marker as in the bubble chart. When applying an element to an option, the display will be strange if the element has not been added to vdims . So, add an element to the dimension with .add_dimension ().

poke_graph2 = poke_graph.add_dimension("Total race value",dim_pos=2,vdim=True,dim_val = "total")

Specify the element name in the first argument. Specify the number to add (insert) the element with dim_pos. (I don't know how the order of vdims will affect it, but for the time being, it would be nice if the new element didn't start at the beginning. Try to add to vdims with vdim = True (if omitted, it will be added to kdims). Specify the data to be actually used as a value with dim_val =. In this case, the dataset has already been passed in DataFrame format, so you can just specify it by column name. In addition to the values ​​specified in the dataset, you can also specify new values ​​such as dim_val = df ["special "] and dim_val = poke_df ["total "] ** 2.

Once added to vdims, you will be able to pass the dimension name you added to the argument with opts.

poke_graph2 = poke_graph2.opts(size="Total race value")

20201230_160724.png Oops. The reason for the above is that size is passed a large value such as 600. To avoid this, convert the value with a mathematical formula. You must then use dim () to apply the element to the formula .

from holoviews import dim
poke_graph2 = poke_graph2.opts(size=(dim("Total race value")/poke_df["total"].min())*5)

20201230_092108.png In the above example, the ratio of the total value of the total race value of each Pokemon to the minimum value of the total race value is multiplied by 5 to obtain the size value.

Application to marker color

It is also possible to specify the marker color from the element. As with size, first add an element to vdims and specify the dimension name in color =.

hv.extension('bokeh')

#Added type 1 to data element
poke_graph3 = poke_graph2.add_dimension("Type 1",dim_pos=2,vdim=True,dim_val = "Type 1")
#Set the marker color according to "Type 1"
poke_graph3.opts(color="Type 1")
#Set the legend position to the right
poke_graph3.opts(legend_position="right")
#Reset the width of the drawing size
poke_graph3.opts(width=800)

20201230_214005.png In the above, the color is automatically assigned, but it can be any color. Create a dictionary with the element to be passed to color as the key and the color as the value, and pass it to cmap =.

color_table = {"normal":"cornsilk", "Fire":"orangered", "Mizu":"royalblue", "Denki":"gold",
                "Kusa":"green", "Ice":"powderblue", "Fighting":"darkorange", "Poison":"mediumorchid",
                "Ground":"saddlebrown", "flight":"lightcyan", "Esper":"khaki", "insect":"yellowgreen",
                "Iwa":"dimgrey", "ghost":"mediumpurple", "Dragon":"lightseagreen",
                "Evil":"indigo", "Steel":"steelblue","Fairy":"pink",
                }
poke_graph3.opts(color="Type 1",cmap=color_table)

20201230_092207.png The same color name as matplotlib can be used. Maybe you can also specify hexadecimal RGB.

Use tooltips

Try using a tooltip, one of the features of bokeh.

hv.extension('bokeh')

#Add Pokemon name to data element
poke_graph4 = poke_graph3.add_dimension("name",dim_pos=2,vdim=True,dim_val = "name")

#Import hover tools
from bokeh.models import HoverTool
#Hover tool settings
TOOLTIPS = [("name","@name")]
hover = HoverTool(tooltips=TOOLTIPS)
#Apply hover tool settings to the graph
poke_graph4 = poke_graph4.opts(plot=dict(tools=[hover]))

20201230_092318.png Hover tools are a type of tooltip. Information on the element that the cursor is on can be displayed. Create an instance of HoverTool and pass the information you want to display in the argumenttooltips =as a list of tuples. Information to be displayed on one line is described in the tuple. Multiple lines can be displayed by passing multiple tuples. Specify the dimension name added to vdims with " @ ~ ". If the dimension name is Japanese, it will not be recognized unless it is enclosed in {} like " @ {attack} " . In addition to "@ ~", basic information of elements can be displayed by "\ $ x" or "$ index".

In addition, a more elaborate layout is possible with HTML description. In addition to the \

tag and \ tag, the \ tag can also be used to display images.

TOOLTIPS_2 = """
<div>
  :
</div>
"""

Pass the character string with HTML description like. If you pass the @ dimension name as a character string like <img src =" @ img_path "> in the text, that information will be displayed.

Sample (Click to view code as it is redundant)

code
#Picture book No.Define a function that specifies the image address from
def path_create(id):
  result = r"file:///C:\Users\aaa\Desktop\Book\pokemon_" + str(id).zfill(3) + ".png "
  return result

#Add data element
poke_graph5 = poke_graph4.add_dimension("img_path",dim_pos=2,vdim=True,
                                        dim_val = poke_df["No."].apply(path_create))
poke_graph5 = poke_graph5.add_dimension("Type 2",dim_pos=2,vdim=True,dim_val = "Type 2")
poke_graph5 = poke_graph5.add_dimension("HP",dim_pos=2,vdim=True,dim_val = "HP")
poke_graph5 = poke_graph5.add_dimension("attack",dim_pos=2,vdim=True,dim_val = "attack")
poke_graph5 = poke_graph5.add_dimension("defense",dim_pos=2,vdim=True,dim_val = "defense")
poke_graph5 = poke_graph5.add_dimension("Special attack",dim_pos=2,vdim=True,dim_val = "Special attack")
poke_graph5 = poke_graph5.add_dimension("Special defense",dim_pos=2,vdim=True,dim_val = "Special defense")
poke_graph5 = poke_graph5.add_dimension("Agility",dim_pos=2,vdim=True,dim_val = "Agility")

#Tooltip settings
TOOLTIPS_2 = """
<div style="clear:both; margin: 0px 10px 0px 10px;">
  <div style="float:left ;margin: 0px 0px 0px 0px;">
    <p style="margin: 5px 0px 5px 5px"><span style="font-size:12px; color:navy ;font-weight:bold">@name</span></p>
    <img src="@img_path" height="72" alt="" width="72"><br>
    <span style="font-size:10px">
height:@{height}{0.f} m<br>
Weight:@{weight}{0.f} kg<br>
    </span>
  </div>
  <div style="float:right">
    <span style="font-size:10px">
      <div style="clear:both">
        <p align="right">type:&nbsp;@{Type 1}&nbsp;&nbsp;&nbsp;@{Type 2}</p>
      </div>
      <div style="clear:both">
        <table align="right" style="margin: 5px 0px 0px 0px">
          <tr><td align="right">HP: </td><td align="right">@HP</td></tr>
          <tr><td align="right">Kogeki:</td><td align="right">@{attack}</td></tr>
          <tr><td align="right">Bougyo:</td><td align="right">@{defense}</td></tr>
          <tr><td align="right">Tokukou:</td><td align="right">@{Special attack}</td></tr>
          <tr><td align="right">Tokubo:</td><td align="right">@{Special defense}</td></tr>
          <tr><td align="right">Quickness:</td><td align="right">@{Agility}</td></tr>
        </table>
      </div>
    </span>
  </div>
</div>
"""
hover2 = HoverTool(tooltips=TOOLTIPS_2)

#Apply hover tool settings to the graph
poke_graph5 = poke_graph5.opts(plot=dict(tools=[hover2]))

20201230_093518.png Since the local image is not displayed in Google Coboratory, it will be as above. Save it in HTML format (described later) and open it from the file to display the image. 20201230_093708.png (For the image, save the one borrowed from the official Pokemon book locally and refer to it)

Save the result to a file

Holoviews (Bokeh) graphs can be output as files in HTML format. If you output the file, you should be able to muzzle the graph even on a PC that is not in the python environment.

#Save graph as html
renderer = hv.renderer('bokeh')
renderer.save(poke_graph5, 'output_poke_graph5')

In the above example, poke_graph5 is output as output_poke_graph5.html. The output destination is the current directory. For Google Colabo, mount Google Drive with drive.mount ("/ content/gdrive ") and change the current directory with % cd" gdrive/My Drive/any folder ".

Impressions

At first, I couldn't grasp how to write Holoviews, but I felt that I could write a simple description if I understood kdims and vdims. However, I feel that ** with the current understanding, there are still high hurdles to replace the Excel work in the workplace **. There seems to be much more that can be done with Holoviews (Bokeh), so I have to study more and find out what cannot be achieved with Excel. I would also like to hear about the experiences of people who have replaced Excel work in the workplace with Python.

reference

Recommended Posts