Bokeh Different Hovers for source and target nodes - python

I create a network graph with Bokeh networkx from a df:
df = pd.DataFrame('source': [1,2,3], 'target': ['a', 'b', 'c], 'name': ['a1', 'b2', 'c3'])
with source nodes from column source and target nodes from column target
Is there any way to show up hovers of
HoverTool(tooltips = [("SOURCE", "$source"), ("NAME", "$name")])
for 'source' nodes and
HoverTool(tooltips = [("TARGET", "$target")])
for target nodes?
My code is as following:
import pandas as pd
from bokeh.io import show
import networkx as nx
from bokeh.models import Plot, MultiLine, Circle
from bokeh.models.graphs import from_networkx
net_graph = networkx.from_pandas_edgelist(df, 'source', 'target', 'name')
for index, row in df.iterrows():
net_graph.nodes[row['source']]['source_hover'] = row['source']
net_graph.nodes[row['source']]['name hover'] = row['name']
net_graph.nodes[row['target']]['target hover'] = row['target']
graph_plot = Plot(plot_width = 800, plot_height = 600, x_range = Range1d(-1.1, 1.1), y_range = Range1d(-1.1, 1.1))
graph_setup = from_networkx(net_graph, nx.spring_layout, scale = 1, center = (0, 0))
graph_plot.add_tools(HoverTool(renderers=[graph_setup], tooltips=[("SOURCE", "#source_hover"), ("NAME", "#name_hover")]))
graph_plot.add_tools(HoverTool(renderers=[graph_setup], tooltips=[("TARGET", "#target_hover")]))
graph_setup.node_renderer.glyph = Circle(size = 20, fill_color = 'red')
graph_setup.edge_renderer.glyph = MultiLine(line_color = "grey", line_alpha = 0.8, line_width = 1)
graph_plot.renderers.append(graph_setup)
show(graph_plot)

you could change renderers below by your graph or if you have different glyphs you could add p, p2, etc. I cannot help very well because your intention is unclear and we don't have your codes.
p.add_tools(HoverTool(renderers=[], tooltips=[("SOURCE", "$source"), ("NAME", "$name")])
p.add_tools(HoverTool(renderers=[], tooltips=[("TARGET", "$target")])
renderers=[] part will help you to do that. renderers are basically your lines/bars etc. for above you've got Multiline (which have list of renderers) and Circle. so basically for circle graph just use renderers[]
for example for circle first give a name:
circlename = Circle(size = 20, fill_color = 'red')
and in hover tool add renderers=[circlename]
MultiLine part is little bit tricky. You've got multiple lines. so you have to give names to all of them. You basically could use dictinary and for loop to give names. and could detetermine in renderers part like above. You could check it out from here

Related

Bokeh hover special variable `$data_x` shows number instead of FactorRange category label for multi-line glyph

I am using Bokeh multi_line to show several lines using a categorical x_range,
and would like hover to display the x category hovered. I thought $data_x might help, but it shows numerical values related to category indexes rather than the category labels. I can use CustomJSHover with special_vars["segment_index"] to display what I want, but is there a simpler way?
To demonstrate, this code creates a figure with multi_line():
from collections import defaultdict
import pandas as pd
from bokeh import palettes
from bokeh.plotting import show, figure
from bokeh.models import CustomJSHover, HoverTool
# Substantive data.
df_data = pd.DataFrame.from_records([
dict(date="2001 Q1", output=100, inputs=100),
dict(date="2001 Q2", output=105, inputs=102),
dict(date="2001 Q3", output=110, inputs=105),
])
# Make list of lists for multi_line(), with metadata.
lines_data = defaultdict(list)
for var in ["inputs", "output"]:
lines_data["variable"].append(var)
lines_data["date"].append(df_data["date"])
lines_data["value"].append(df_data[var])
lines_data["color"] = palettes.Category10_10[:2]
fig = figure(
x_range = df_data["date"],
plot_height=400,
)
fig.multi_line(
source = lines_data,
xs = "date",
ys = "value",
color = "color",
legend_group = "variable",
line_width = 5,
line_alpha = 0.6,
hover_line_alpha = 1.0, # Highlight hover line.
)
The hover I want can be created like this using CustomJSHover:
# Custom hover formatting #date.
hover_date = CustomJSHover(
# Show value[$segment_index].
code="""
console.log("> Show value[$segment_index] hover", value);
return "" + value[special_vars["segment_index"]];
""")
fig.add_tools(HoverTool(
tooltips=[
('variable', '#variable'),
('date', '#date{custom}'), # Show hovered date only.
('value', '$data_y'),
],
formatters={'#date': hover_date},
))
show(fig)
Potentially a more straightforward hover specification would use something like $data_x without a custom format, except $data_x itself apparently does not look up the label in the FactorRange (applying this HoverTool instead of the one above):
# Simple hover showing $data_x.
fig.add_tools(HoverTool(
tooltips=[
('variable', '#variable'),
('date', '$data_x'), # Does not show x_range value!
('value', '$data_y'),
]))
show(fig)
Now, hovering over a line shows a 'date' like "1.500" instead of "2001 Q2" etc.
Am I missing a trick, or is CustomJSHover the best way to show the x category?

How do I resize my Plotly bar height and show only bar’s edge (in subplot)?

this is my first foray into Plotly. I love the ease of use compared to matplotlib and bokeh. However I'm stuck on some basic questions on how to beautify my plot. First, this is the code below (its fully functional, just copy and paste!):
import plotly.express as px
from plotly.subplots import make_subplots
import plotly as py
import pandas as pd
from plotly import tools
d = {'Mkt_cd': ['Mkt1','Mkt2','Mkt3','Mkt4','Mkt5','Mkt1','Mkt2','Mkt3','Mkt4','Mkt5'],
'Category': ['Apple','Orange','Grape','Mango','Orange','Mango','Apple','Grape','Apple','Orange'],
'CategoryKey': ['Mkt1Apple','Mkt2Orange','Mkt3Grape','Mkt4Mango','Mkt5Orange','Mkt1Mango','Mkt2Apple','Mkt3Grape','Mkt4Apple','Mkt5Orange'],
'Current': [15,9,20,10,20,8,10,21,18,14],
'Goal': [50,35,21,44,20,24,14,29,28,19]
}
dataset = pd.DataFrame(d)
grouped = dataset.groupby('Category', as_index=False).sum()
data = grouped.to_dict(orient='list')
v_cat = grouped['Category'].tolist()
v_current = grouped['Current']
v_goal = grouped['Goal']
fig1 = px.bar(dataset, x = v_current, y = v_cat, orientation = 'h',
color_discrete_sequence = ["#ff0000"],height=10)
fig2 = px.bar(dataset, x = v_goal, y = v_cat, orientation = 'h',height=15)
trace1 = fig1['data'][0]
trace2 = fig2['data'][0]
fig = make_subplots(rows = 1, cols = 1, shared_xaxes=True, shared_yaxes=True)
fig.add_trace(trace2, 1, 1)
fig.add_trace(trace1, 1, 1)
fig.update_layout(barmode = 'overlay')
fig.show()
Here is the Output:
Question1: how do I make the width of v_current (shown in red bar) smaller? As in, it should be smaller in height since this is a horizontal bar. I added the height as 10 for trace1 and 15 for trace2, but they are still showing at the same heights.
Question2: Is there a way to make the v_goal (shown in blue bar) only show it's right edge, instead of a filled out bar? Something like this:
If you noticed, I also added a line under each of the category. Is there a quick way to add this as well? Not a deal breaker, just a bonus. Other things I'm trying to do is add animation, etc but that's for some other time!
Thanks in advance for answering!
Running plotly.express wil return a plotly.graph_objs._figure.Figure object. The same goes for plotly.graph_objects running go.Figure() together with, for example, go.Bar(). So after building a figure using plotly express, you can add lines or traces through references directly to the figure, like:
fig['data'][0].width = 0.4
Which is exactly what you need to set the width of your bars. And you can easily use this in combination with plotly express:
Code 1
fig = px.bar(grouped, y='Category', x = ['Current'],
orientation = 'h', barmode='overlay', opacity = 1,
color_discrete_sequence = px.colors.qualitative.Plotly[1:])
fig['data'][0].width = 0.4
Plot 1
In order to get the bars or shapes to indicate the goal levels, you can use the approach described by DerekO, or you can use:
for i, g in enumerate(grouped.Goal):
fig.add_shape(type="rect",
x0=g+1, y0=grouped.Category[i], x1=g, y1=grouped.Category[i],
line=dict(color='#636EFA', width = 28))
Complete code:
import plotly.express as px
from plotly.subplots import make_subplots
import plotly as py
import pandas as pd
from plotly import tools
d = {'Mkt_cd': ['Mkt1','Mkt2','Mkt3','Mkt4','Mkt5','Mkt1','Mkt2','Mkt3','Mkt4','Mkt5'],
'Category': ['Apple','Orange','Grape','Mango','Orange','Mango','Apple','Grape','Apple','Orange'],
'CategoryKey': ['Mkt1Apple','Mkt2Orange','Mkt3Grape','Mkt4Mango','Mkt5Orange','Mkt1Mango','Mkt2Apple','Mkt3Grape','Mkt4Apple','Mkt5Orange'],
'Current': [15,9,20,10,20,8,10,21,18,14],
'Goal': [50,35,21,44,20,24,14,29,28,19]
}
dataset = pd.DataFrame(d)
grouped = dataset.groupby('Category', as_index=False).sum()
fig = px.bar(grouped, y='Category', x = ['Current'],
orientation = 'h', barmode='overlay', opacity = 1,
color_discrete_sequence = px.colors.qualitative.Plotly[1:])
fig['data'][0].width = 0.4
fig['data'][0].marker.line.width = 0
for i, g in enumerate(grouped.Goal):
fig.add_shape(type="rect",
x0=g+1, y0=grouped.Category[i], x1=g, y1=grouped.Category[i],
line=dict(color='#636EFA', width = 28))
f = fig.full_figure_for_development(warn=False)
fig.show()
You can use Plotly Express and then directly access the figure object as #vestland described, but personally I prefer to use graph_objects to make all of the changes in one place.
I'll also point out that since you are stacking bars in one chart, you don't need subplots. You can create a graph_object with fig = go.Figure() and add traces to get stacked bars, similar to what you already did.
For question 1, if you are using go.Bar(), you can pass a width parameter. However, this is in units of the position axis, and since your y-axis is categorical, width=1 will fill the entire category, so I have chosen width=0.25 for the red bar, and width=0.3 (slightly larger) for the blue bar since that seems like it was your intention.
For question 2, the only thing that comes to mind is a hack. Split the bars into two sections (one with height = original height - 1), and set its opacity to 0 so that it is transparent. Then place down bars of height 1 on top of the transparent bars.
If you don't want the traces to show up in the legend, you can set this individually for each bar by passing showlegend=False to fig.add_trace, or hide the legend entirely by passing showlegend=False to the fig.update_layout method.
import plotly.express as px
import plotly.graph_objects as go
# from plotly.subplots import make_subplots
import plotly as py
import pandas as pd
from plotly import tools
d = {'Mkt_cd': ['Mkt1','Mkt2','Mkt3','Mkt4','Mkt5','Mkt1','Mkt2','Mkt3','Mkt4','Mkt5'],
'Category': ['Apple','Orange','Grape','Mango','Orange','Mango','Apple','Grape','Apple','Orange'],
'CategoryKey': ['Mkt1Apple','Mkt2Orange','Mkt3Grape','Mkt4Mango','Mkt5Orange','Mkt1Mango','Mkt2Apple','Mkt3Grape','Mkt4Apple','Mkt5Orange'],
'Current': [15,9,20,10,20,8,10,21,18,14],
'Goal': [50,35,21,44,20,24,14,29,28,19]
}
dataset = pd.DataFrame(d)
grouped = dataset.groupby('Category', as_index=False).sum()
data = grouped.to_dict(orient='list')
v_cat = grouped['Category'].tolist()
v_current = grouped['Current']
v_goal = grouped['Goal']
fig = go.Figure()
## you have a categorical plot and the units for width are in position axis units
## therefore width = 1 will take up the entire allotted space
## a width value of less than 1 will be the fraction of the allotted space
fig.add_trace(go.Bar(
x=v_current,
y=v_cat,
marker_color="#ff0000",
orientation='h',
width=0.25
))
## you can show the right edge of the bar by splitting it into two bars
## with the majority of the bar being transparent (opacity set to 0)
fig.add_trace(go.Bar(
x=v_goal-1,
y=v_cat,
marker_color="#ffffff",
opacity=0,
orientation='h',
width=0.30,
))
fig.add_trace(go.Bar(
x=[1]*len(v_cat),
y=v_cat,
marker_color="#1f77b4",
orientation='h',
width=0.30,
))
fig.update_layout(barmode='relative')
fig.show()

How to add filter in the graph

Code is below
from io import StringIO
text = '''Product,Count
Pen,10
Pencil,15
Book, 10'''
df = pd.read_csv(StringIO(text))
df.plot(x="Product", y="Count", kind="bar")
How to add filter in the graph itself that user has to privilege to select which product has to display in the graph and count also let's say if count > 11 then only Pencil has to appear.
Is there any alternate way also there to do this?
IF one column is date column can we do filtering with date column also
matplotlib.widgets
As suggested in the comments, one way to do this is using the matplotlib.widgets and you can read more about them here, though for the actual implementation, I found most useful their examples of Sliders and Check buttons. Using your minimal example, the simplest adaptation I could come up with (that looks ok) would look like this:
import pandas as pd
from io import StringIO
import matplotlib.pyplot as plt
import matplotlib.gridspec as gridspec
from matplotlib.widgets import Slider, CheckButtons
text = '''Product,Count
Pen,10
Pencil,15
Book,10'''
df = pd.read_csv(StringIO(text))
fig, ax = plt.subplots()
gs = gridspec.GridSpec(
nrows = 2,
ncols = 2,
figure = fig,
wspace = 0.3,
hspace = 0.6,
height_ratios = [2,1]
)
ax.set_position(gs[0,:].get_position(fig))
axMinCount = fig.add_subplot(gs[1,0])
axProducts = fig.add_subplot(gs[1,1])
labels = ('Pen', 'Pencil', 'Book')
minimum = 5
actives = [True, True, True]
df.loc[actives & (df['Count'] >= minimum)].plot(
x = 'Product', y = 'Count', kind = 'bar', ax = ax, legend = False
)
sMinCount = Slider(axMinCount, 'Min Count', 0, 20, valinit = minimum, valstep = 1)
cProducts = CheckButtons(axProducts, labels, actives)
def update(val):
minimum = sMinCount.val
df_filtered = df.loc[actives & (df['Count'] >= minimum)]
if not df_filtered.empty:
df_filtered.plot(
x = 'Product', y = 'Count', kind = 'bar', ax = ax, legend = False
)
else:
ax.cla()
def check(label):
index = labels.index(label)
actives[index] = not actives[index]
df_filtered = df.loc[actives & (df['Count'] >= minimum)]
if not df_filtered.empty:
df_filtered.plot(
x = 'Product', y = 'Count', kind = 'bar', ax = ax, legend = False
)
else:
ax.cla()
sMinCount.on_changed(update)
cProducts.on_clicked(check)
plt.show()
With various filtering settings, the result looks like this:
ipywidgets (Jupyter notebook)
I'd suggest also trying ipywidgets, which have a much nicer user interface than matplotlib.widgets. You can read more about Using Interact. Using your minimal example:
import pandas as pd
from io import StringIO
from ipywidgets import interact
text = '''Product,Count
Pen,10
Pencil,15
Book,10'''
df = pd.read_csv(StringIO(text))
# This is a wrapper of the function that follows, providing the interactive input
#interact(MinCount = (0, 20, 1), pen = True, pencil = True, book = True)
# Note that in the core function below, you can set the starting values
def plotter_fun(MinCount = 0, pen = True, pencil = True, book = True):
# Filter the data using the interactive input
df_filtered = df.loc[(pen, pencil, book) & (df['Count'] >= MinCount)]
# If all data has been filtered out, announce it
if df_filtered.empty:
print('No data to show.')
# Otherwise plot
else:
df_filtered.plot(x = 'Product', y = 'Count', kind = 'bar')
The result with various filtering settings looks as follows:
Of course, there are many options for configuring the layout etc.
This solution is designed to work primarily in Jupyter Notebook,
though if you'd like to embed this functionality somewhere else, you
can read about Embedding Jupyter Widgets in Other Contexts than the
Notebook.

Changing bokeh grid lines position

I am trying to plot a few points on a graph, similarly to a heat map.
Sample code (adapted from the heat map section here):
import pandas as pd
from bokeh.io import output_notebook, show
from bokeh.models import BasicTicker, ColorBar, ColumnDataSource, LinearColorMapper, PrintfTickFormatter
from bokeh.plotting import figure
from bokeh.transform import transform
import numpy as np
# change this if you don't run it on a Jupyter Notebook
output_notebook()
testx = np.random.randint(0,10,10)
testy = np.random.randint(0,10,10)
npdata = np.stack((testx,testy), axis = 1)
hist, bins = np.histogramdd(npdata, normed = False, bins = (10,10), range=((0,10),(0,10)))
data = pd.DataFrame(hist, columns = [str(x) for x in range(10)])
data.columns.name = 'y'
data['x'] = [str(x) for x in range(10)]
data = data.set_index('x')
df = pd.DataFrame(data.stack(), columns=['present']).reset_index()
source = ColumnDataSource(df)
colors = ['lightblue', "yellow"]
mapper = LinearColorMapper(palette=colors, low=df.present.min(), high=df.present.max())
p = figure(plot_width=400, plot_height=400, title="test circle map",
x_range=list(data.index), y_range=list((data.columns)),
toolbar_location=None, tools="", x_axis_location="below")
p.circle(x="x", y="y", size=20, source=source,
line_color=None, fill_color=transform('present', mapper))
p.axis.axis_line_color = None
p.axis.major_tick_line_color = None
p.axis.major_label_text_font_size = "10pt"
p.axis.major_label_standoff = 10
p.xaxis.major_label_orientation = 0
show(p)
That returns:
Now, as you can see, the grid lines are centered on the points(circles), and I would like, instead to have the circles enclosed in a square created by the lines.
I went through this to see if I could find information on how to offset the grid lines by 0.5 (that would have worked), but I was not able to.
There's nothing built into Bokeh to accomplish this kind of offsetting of categorical ticks, but you can write a custom extension to do it:
CS_CODE = """
import {CategoricalTicker} from "models/tickers/categorical_ticker"
export class MyTicker extends CategoricalTicker
type: "MyTicker"
get_ticks: (start, end, range, cross_loc) ->
ticks = super(start, end, range, cross_loc)
# shift the default tick locations by half a categorical bin width
ticks.major = ([x, 0.5] for x in ticks.major)
return ticks
"""
class MyTicker(CategoricalTicker):
__implementation__ = CS_CODE
p.xgrid.ticker = MyTicker()
p.ygrid.ticker = MyTicker()
Note that Bokeh assumes CoffeeScript by default when the code is just a string, but it's possible to use pure JS or TypeScript as well. Adding this to your code yields:
Please note the comment about output_notebook you must call it (possibly again, if you have called it previously) after the custom model is defined, due to #6107

Networkx plotting in bokeh: how to set edge width based on graph edge weight

For a given networkx graph G, I would like to adjust the edge line_width as a function of graph weights in Bokeh. Simplified example:
import networkx as nx
import pandas as pd
from bokeh.models import Plot, ColumnDataSource, Range1d, from_networkx, Circle,MultiLine
from bokeh.io import show, output_file
from bokeh.palettes import Viridis
#define graph
source = ['A', 'A', 'A','a','B','B','B','b']
target = ['a', 'B','b','b','a','b','A','a']
weight = [.1,.2,.3,.4,.4,.3, .2, .1]
df = pd.DataFrame([source,target,weight])
df = df.transpose()
df.columns = ['source','target','weight']
G=nx.from_pandas_dataframe(df, 'source', 'target', ['weight'])
#set node attributes
node_color = {'A':Viridis[10][0], 'B':Viridis[10][9],'a':Viridis[10][4],'b':Viridis[10][4]}
node_size = {'A':50, 'B':40,'a':10,'b':10}
node_initial_pos = {'A':(-0.5,0), 'B':(0.5,0),'a':(0,0.25),'b':(0,-0.25)}
nx.set_node_attributes(G, 'node_color', node_color)
nx.set_node_attributes(G, 'node_size', node_size)
nx.set_node_attributes(G, 'node_initial_pos', node_initial_pos)
#source with node color, size and initial pos (perhaps )
node_source = ColumnDataSource(pd.DataFrame.from_dict({k:v for k,v in G.nodes(data=True)}, orient='index'))
plot = Plot(plot_width=400, plot_height=400,
x_range=Range1d(-1.1,1.1), y_range=Range1d(-1.1,1.1))
graph_renderer = from_networkx(G, nx.spring_layout, scale=0.5, center=(0,0))
#style
graph_renderer.node_renderer.data_source = node_source
graph_renderer.node_renderer.glyph = Circle(fill_color = 'node_color',size = 'node_size', line_color = None)
graph_renderer.edge_renderer.glyph = MultiLine(line_color="#CCCCCC", line_alpha=0.8, line_width=2)
plot.renderers.append(graph_renderer)
output_file('test.html')
show(plot)
This provides a plot, but I'm unable to link G's weights to the line_width property of the edge_renderer. Rather than line_width=2, I would like the line_width to be the graph weights. I have tried creating a new ColumnDataSource based off of the df that was passed into G, but no luck. For example, the following gives "Bokeh Error Cannot read property 'length' of undefined":
nodal_relation_df['weight'] = nodal_relation_df['weight'].apply(float)
graph_source = ColumnDataSource(nodal_relation_df)
graph_renderer.edge_renderer.data_source = graph_source
Any other approaches to pairing the graph_renderer.edge_renderer.glyph's line_width to the graph weights?
Bokeh 0.12.14, Networkx 2.1, Python 3.5.4, Jupyter 5.0.0
Given a networkX graph with weighted edges named G and a bokeh plot object named plot, I propose a more compact form:
graph_renderer.edge_renderer.data_source.data["line_width"] = [G.get_edge_data(a,b)['weight'] for a, b in G.edges()]
graph_renderer.edge_renderer.glyph.line_width = {'field': 'line_width'}
plot.renderers.append(graph_renderer)
add following lines before output:
weight_map = dict(zip(zip(source, target), weight))
weight_map.update(zip(zip(target, source), weight))
data = graph_renderer.edge_renderer.data_source.data
data["line_width"] = [weight_map[edge] * 10 for edge in zip(data["start"], data["end"])]
graph_renderer.edge_renderer.glyph.line_width = {'field': 'line_width'}

Categories

Resources