Plotly Hover breaks when adding color attribute - colors

My graph is doing something strange. The hover data works for some of the points, but not all of them.
When I comment out the color piece, it works just fine. But I really would like the colors to stay.
Here is my code for the graph:
df = pd.DataFrame(data['df'])
df['MeasureDate'] = pd.to_datetime(df['MeasureDate'], infer_datetime_format=True)
df['Sign'] = np.where(df['DeviationValue'] > 0, 'Positive', 'Negative')
fig = px.area(df,
y='DeviationValue',
title=f"Graph of {data['engine']} going back one year from {data['date']}",
color='Sign',
color_discrete_map={
'Positive': 'green',
'Negative': 'red'
},
labels={
"DeviationValue": 'Deviation',
'index': ''
},
custom_data=['MeasureDate', 'DeviationValue']
)
fig.update_traces(showlegend=False, hovertemplate='Date: %{customdata[0]} <br>Deviation: %{customdata[1]}')
Please let me know if you have any solutions or thoughts! Thanks!

Related

Capture which items are displayed on Python Plotly Express chart based on legend selection

DISTRIB_DESCRIPTION="Ubuntu 20.04.5 LTS"
Streamlit, version 1.12.2
plotly==5.10.0
I have a Plotly Express px.scatter chart being generated in a Streamlit page. The different data points available to be shown are set by the color= parameter in ...
fig = px.scatter(x=df[x_column_name],
y=df[y_column_name],
color=df[color_column_name])
Which data (color) points are actually shown on the chart can be selected in the legend (see images.)
Is there a way to detect in the code (via the fig or something else) which data points (colors) have actually been selected in the legend to appear on the chart? I.e. In the example pictures, for the Streamlit (Python) code to know that only DMP, OTP, and BP are currently being seen on the plotly chart?
All selected
None selected
DMP, OTP, BP selected
FULL CODE
def control_chart_by_compound(df,
x_column_name,
y_column_name,
color_column_name,
chart_width = 800,
marker_line_width = 1,
standard_deviation = False,
stddev_colors = ["#CCFF00","#FFCC99","#FF9966"],
average = False,
average_color = "green",
custom_marker_lines = [],
custom_marker_lines_colors = []
):
if custom_marker_lines_colors == []:
custom_marker_lines_colors = CSS_blues()
fig = px.scatter(x=df[x_column_name],
y=df[y_column_name],
color=df[color_column_name],
width=chart_width,
labels={
"x": x_column_name,
"y": y_column_name,
color_column_name: "Compounds"
},
)
# Adds buttons select or deselect all amongst the legend (default the compounds as different colors)
fig.update_layout(dict(updatemenus=[
dict(
type = "buttons",
direction = "left",
buttons=list([
dict(
args=["visible", "legendonly"],
label="Deselect All compounds",
method="restyle"
),
dict(
args=["visible", True],
label="Select All compounds",
method="restyle"
)
]),
pad={"r": 10, "t": 10},
showactive=False,
x=1,
xanchor="right",
y=1.1,
yanchor="top"
),
]
))
if average != False:
fig.add_hline(y=np.average(df[y_column_name]),
line_color=average_color,
line_width=marker_line_width,
line_dash="dash")
# Add zero hline
fig.add_hline(y=0, line_color="gainsboro")
### Standard deviations
if standard_deviation != False:
stddev = df[y_column_name].std()
for idx, color in enumerate(stddev_colors):
fig.add_hline(y=stddev * (idx+1), line_color=color, line_width=marker_line_width,)
fig.add_hline(y=-stddev * (idx+1), line_color=color, line_width=marker_line_width,)
for idx, line in enumerate(custom_marker_lines):
fig.add_hline(y=line, line_color=custom_marker_lines_colors[idx], line_width=marker_line_width,)
fig.add_hline(y=-line, line_color=custom_marker_lines_colors[idx], line_width=marker_line_width,)
# Background to clear
fig.update_layout({
'plot_bgcolor': 'rgba(0, 0, 0, 0)',
'paper_bgcolor': 'rgba(0, 0, 0, 0)',
})
fig.update_layout(xaxis=dict(showgrid=False),
yaxis=dict(showgrid=False))
return fig

Creating GlyphRenderers for modifying the legend

I want to create a bokeh application that can filter points based on some attribute. Here is a very simple code example for my use case that filters points on the plot using checkboxes.
from bokeh.plotting import ColumnDataSource, figure, curdoc
import bokeh.models as bmo
from bokeh.layouts import row
import numpy as np
def update_filter(selected_colors):
keep_indices = []
for i, color in enumerate(cds.data['color']):
if color2idx[color] in selected_colors:
keep_indices.append(i)
view.filters[0] = bmo.IndexFilter(keep_indices)
cds = ColumnDataSource(data=dict(
x=np.random.rand(10),
y=np.random.rand(10),
color=['red', 'green', 'blue', 'red', 'green',
'blue', 'red', 'green', 'blue', 'red'])
)
view = bmo.CDSView(source=cds, filters=[bmo.IndexFilter(np.arange(10))])
checkboxes = bmo.CheckboxGroup(labels=['red', 'green', 'blue'], active=[0, 1, 2])
color2idx = {'red': 0, 'green': 1, 'blue': 2}
checkboxes.on_change('active', lambda attr, old_val, new_val: update_filter(new_val))
fig = figure(plot_width=400, plot_height=400, title='Visualize')
fig.circle(x='x', y='y', fill_color='color', size=10, source=cds, view=view, legend_field='color')
curdoc().add_root(row(checkboxes, fig))
curdoc().title = 'Plot'
It works well, however, when I filter points out by de-selecting one of the checkboxes, the legend becomes erroneous.
Below is a screenshot when all the colors are selected:
And this is a screenshot when one of the colors is de-selected:
As it can be seen, the legend for "green" became red in color when the checkbox for "green" was de-selected.
I found that legends do not work properly with CDSView and it is still an unsolved issue: https://github.com/bokeh/bokeh/issues/8010
So, I wrote the function below that would modify the legend so that it is not erroneous.
def update_legend():
# Find the indices in the CDS that are visible
filters = view.filters
visible_indices = set(list(range(len(cds.data['x']))))
for filter in filters:
visible_indices = visible_indices & set(filter.indices)
# Get a list of visible colors
visible_colors = set([cds.data['color'][i] for i in visible_indices])
# Create a dummy figure to obtain renderers
dummy_figure = figure(plot_width=0, plot_height=0, title='')
legend_items = []
# Does not work
for color in visible_colors:
renderer = dummy_figure.circle(x=[0], y=[0], fill_color=color, size=10)
legend_items.append(bmo.LegendItem(label=color, renderers=[renderer]))
fig.legend[0].items = legend_items
And added another event callback for the checkbox group:
checkboxes.on_change('active', lambda attr, old_val, new_val: update_legend())
When I did the above, the labels in the legend were corrected but now the glyphs are not rendered in the legend. Below is a screenshot of the same:
What am I doing wrong? How should I create a GlyphRenderer for the legend such that the issue gets resolved?
This works for Bokeh v2.1.1. In addition to your original code you can also click on a legend item to show/hide the circles.
from bokeh.plotting import ColumnDataSource, figure, curdoc
from bokeh.models import CheckboxGroup, Row, CDSView, IndexFilter
import numpy as np
colors = ['red', 'green', 'blue']
cds = ColumnDataSource(dict(x=np.random.rand(10),
y=np.random.rand(10),
color=['red', 'green', 'blue', 'red', 'green', 'blue', 'red', 'green', 'blue', 'red']))
def update_filter(selected_colors):
for i in range(len(colors)):
renderers[i].visible = True if i in selected_colors else False
checkboxes = CheckboxGroup(labels=colors, active=[0, 1, 2], width = 50)
checkboxes.on_change('active', lambda attr, old_val, new_val: update_filter(new_val))
fig = figure(plot_width=400, plot_height=400, title='Visualize')
views = [CDSView(source=cds, filters=[IndexFilter([i for i, x in enumerate(cds.data['color']) if x == color])]) for color in colors]
renderers = [fig.circle(x='x', y='y', fill_color='color', size=10, source=cds, view=views[i], legend=color) for i,color in enumerate(colors)]
fig.legend.click_policy = 'hide'
curdoc().add_root(Row(checkboxes, fig))
curdoc().title = 'Plot'
Result:

Controlling which elements receive hover tooltip in Bokeh

I'm currently trying to find a way to use an boolean array in order to decide which elements recive HoverTooltips in bokeh while only using one dictonary for the plot.
I allready tryed to use the bokeh rendering function but this didn't quite work.
source.data = dict(
x=data_source.loc[:, 'x_col_name'],
y=data_source.loc[:, 'y_col_name'],
color=color_selection(selected), # Translates bool to color
alpha=alpha_selection(selected), # Translates bool to transparency
active=selection # boolean array of selected elements
)
Only active datapoints should recieve a HoverTooltip
As a temporary work-around you could hide the tooltips in a callback like this (works for Bokeh v1.3.0):
from bokeh.plotting import figure, show
from bokeh.models import CustomJS
data = dict(
x=[1, 2, 3, 4, 5],
y=[1, 2, 3, 4, 5],
color=['red', 'green', 'red', 'green', 'red'],
active=[False, True, False, True, False],
)
p = figure(tooltips=[('x','#x'),('y', '#y'), ('active', '#active')])
p.circle('x', 'y', color='color', size=10, source = data)
code_hover = '''
if (cb_data.index.indices.length > 0) {
var active_index = cb_data.index.indices[0]
var data = cb_data.renderer.data_source.data
var show_tooltip = data['active'][active_index]
var tooltip_index = 0
if (show_tooltip) {
document.getElementsByClassName('bk-tooltip')[tooltip_index].style.display = 'block';
}
else {
document.getElementsByClassName('bk-tooltip')[tooltip_index].style.display = 'none';
}
}
'''
p.hover.callback = CustomJS(code = code_hover)
show(p)
Please note the tooltip_index in the callback. If you have more tooltips you need to change that index. See also this post
Result:
As of Bokeh 1.x there is no built-in capability to filter hover results. That feature is planned for the 2.0 release, you can follow this issue here: https://github.com/bokeh/bokeh/issues/9087

vbar in bokeh doesn't not support nonselection color and alpha?

Update the question:
How to select a certain species in barplot, nonselected bars will change color?
How to show text on top of each bar?
from bokeh.sampledata.iris import flowers
from bokeh.plotting import figure, output_file, show
from bokeh.models import ColumnDataSource, CategoricalColorMapper
from bokeh.layouts import column, row
#color mapper to color data by species
mapper = CategoricalColorMapper(factors = ['setosa','versicolor', 'virginica'],\
palette = ['green', 'blue', 'red'])
output_file("plots.html")
#group by species and plot barplot for count
species = flowers.groupby('species')
source = ColumnDataSource(species)
p = figure(plot_width = 800, plot_height = 400, title = 'Count by Species', \
x_range = source.data['species'], tools = 'box_select')
p.vbar(x = 'species', top = 'petal_length_count', width = 0.8, source = source,\
nonselection_fill_color = 'gray', nonselection_fill_alpha = 0.2,\
color = {'field': 'species', 'transform': mapper})
show(p)
First: please try to ask unrelated questions in separate SO posts.
Hit testing and selection was not implemented for vbar and hbar until recently. Using the recent 0.12.11 release, your code behaves as you are wanting:
Regarding labels for each bar, you want to use the LabelSet annotation, as demonstrated in the User's Guide Something like:
labels = LabelSet(x='species', y='petal_count_length', text='some_column',
x_offset=5, y_offset=5, source=source)
p.add_layout(labels)
The linking question is too vague. I would suggest opening a new SO question with more information and description of what exactly you are trying to accomplish.

Bokeh: One url per glyph

I have a set of datapoints, each with a url unique to it. What I want to do is to be able to scatter plot my data, and then open the associated url when clicking the glyph. I have read the discussion here and followed the example here, but neither gets me where I want to be.
I have, somewhat arbitrarily and haphazardly, tried to save the urls in the tag property, to be recalled by the TapTool:
from bokeh.models import OpenURL, TapTool
from bokeh.plotting import figure, show
p = figure(plot_width = 1200,
plot_height = 700,
tools = 'tap')
p.circle(data_x,
data_y,
tags = list(data_urls))
taptool = p.select(type = TapTool, arg = "tag")
taptool.callback = OpenURL(url = '#tag')
show(p)
I have not been able to find any place in the Bokeh documentation that explains the nuts and bolts needed to assemble the behaviour that i want. At least not in terms I can understand.
Could someone please point me in the right direction? Thanks!
The tags property is not relevant, and largely disused. You need to put the URLs in a column in the plot data source, so that the OpenURL callback can access it:
from bokeh.models import ColumnDataSource, OpenURL, TapTool
from bokeh.plotting import figure, show
p = figure(plot_width=400, plot_height=400,
tools="tap", title="Click the Dots")
source = ColumnDataSource(data=dict(
x=[1, 2, 3, 4, 5],
y=[2, 5, 8, 2, 7],
color=["navy", "orange", "olive", "firebrick", "gold"]
))
p.circle('x', 'y', color='color', size=20, source=source)
# use the "color" column of the CDS to complete the URL
# e.g. if the glyph at index 10 is selected, then #color
# will be replaced with source.data['color'][10]
url = "http://www.colors.commutercreative.com/#color/"
taptool = p.select(type=TapTool)
taptool.callback = OpenURL(url=url)
show(p)
This example is documented (and live) here:
https://docs.bokeh.org/en/latest/docs/user_guide/interaction/callbacks.html#openurl

Resources