Capture which items are displayed on Python Plotly Express chart based on legend selection - python-3.x

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

Related

Plotly Custom Legend

I have a plotly plot which looks like this:
The Code I am using is below:
fig = make_subplots(specs=[[{"secondary_y": True}]])
fig.add_trace(go.Scatter( x = pf['Timestamp'], y = pf['Price_A'], name ='<b>A</b>',
mode = 'lines+markers',
marker_color = 'rgba(255, 0, 0, 0.8)',
line = dict(width = 3 ), yaxis = "y1"),
secondary_y=False,)
fig.add_trace(go.Scatter( x = df['Timestamp'], y = df['Price_B'], name='<b>B</b>',
mode = 'lines+markers',
marker_color = 'rgba(0, 196, 128, 0.8)',
line = dict(width = 3 ), yaxis = "y1") ,
secondary_y=False,)
for i in pf2['Timestamp']:
fig.add_vline(x=i, line_width=3, line_dash="dash", line_color="purple",
name='Event')
fig.update_layout( title="<b>Change over Time</b>", font=dict( family="Courier New,
monospace", size=16, color="RebeccaPurple"),
legend=dict(
yanchor="top",
y=0.99,
xanchor="left",
x=0.01
))
How can I add the entry in the legend for the event that is denoted by the vertical lines?
When you use add_vline, you are adding an annotation which will not have a corresponding legend entry.
You'll need to instead use go.Scatter to plot the vertical lines, passing the minimum and maximum values in your data (plus or minus some padding) to the y parameter. Then you can set this same y-range for your plot. This will give you the appearance of vertical lines while still showing the full range of your data.
Update: you can use a legend group so that the vertical lines appear as a single entry in the legend
For example:
from pkg_resources import yield_lines
import plotly.express as px
import plotly.graph_objects as go
fig = go.Figure()
df = px.data.stocks()
for col in ['GOOG','AMZN']:
fig.add_trace(go.Scatter(
x=df['date'],
y=df[col]
))
vlines = ["2018-07-01","2019-04-01","2019-07-01"]
min_y,max_y = df[['GOOG','AMZN']].min().min(), df[['GOOG','AMZN']].max().max()
padding = 0.05*(max_y-min_y)
for i,x in enumerate(vlines):
fig.add_trace(go.Scatter(
x=[x]*2,
y=[min_y-padding, max_y+padding],
mode='lines',
line=dict(color='purple', dash="dash"),
name="vertical lines",
legendgroup="vertical lines",
showlegend=True if i == 0 else False
))
fig.update_yaxes(range=[min_y-padding, max_y+padding])
fig.show()

Equal spacing between pie charts of different sizes in matplotlib

I am having difficulties with setting an equal space between pie charts of different sizes. The 5 are correctly arranged in one row, but the distance between the contours of neighboring pies aren't equal. I tried many abbreviations of the following code, all of them not making a big difference in the output (see image):
#code:
import matplotlib.pyplot as plt
import pandas as pd
labels = 'Verkehr', 'Maschinen und Motoren', 'Feuerungen', 'Industrie / Gewerbe', 'Land- und Forstwirtschaft'
sizesax1 = [108295, 10107, 7220, 11551, 7220]
sizesax2 = [77882, 6676, 6676, 13351, 6676]
sizesax3 = [55652, 4417, 6184, 15900, 6184]
sizesax4 = [36327, 2642, 4632, 16512, 5944]
sizesax5 = [18781, 1409, 3287, 1878, 4695]
fig, (ax1, ax2, ax3, ax4, ax5) = plt.subplots(1, 5, figsize =(20,4))
ax1.pie(sizesax1, startangle=0, colors = ('red', 'darkblue', 'orange', 'yellow', 'green'), radius=1*4)
ax2.pie(sizesax2, startangle=0, colors = ('red', 'darkblue', 'orange', 'yellow', 'green'), radius=.77*4)
ax3.pie(sizesax3, startangle=0, colors = ('red', 'darkblue', 'orange', 'yellow', 'green'), radius=.61*4)
ax4.pie(sizesax4, startangle=0, colors = ('red', 'darkblue', 'orange', 'yellow', 'green'), radius=.46*4)
ax5.pie(sizesax5, startangle=0, colors = ('red', 'darkblue', 'orange', 'yellow', 'green'), radius=.33*4)
some additions i tried:
fig.subplots_adjust(left=None, bottom=None, right=None, top=None, wspace=1, hspace=None)
or
fig.tight_layout()
#giving me this error message:
/srv/conda/envs/notebook/lib/python3.7/site-packages/ipykernel_launcher.py:17: UserWarning:
Tight layout not applied. The bottom and top margins cannot be made large enough to
accommodate all axes decorations.
and some others.
Big thank you already for reading this! I am a complete beginner in python and just managed to come as far as you see in this image:
enter image description here
It is not clear what it is required. I'll assume it is the following image:
Fundamentally, the problem is that the pie needs a square aspect ratio, which is not provided by a row of subplots.
The simplest solution, is to create only one plot and plot there multiple pies with different centres. Something like:
import matplotlib.pyplot as plt
sizes = [ [108295, 10107, 7220, 11551, 7220],
[77882, 6676, 6676, 13351, 6676],
[55652, 4417, 6184, 15900, 6184],
[36327, 2642, 4632, 16512, 5944],
[18781, 1409, 3287, 1878, 4695]]
colors = ('red', 'darkblue', 'orange', 'yellow', 'green')
R = 4
radius = [R*i for i in [1.0, 0.77, 0.61, 0.46, 0.33] ]
wid = sum(radius)*2
hei = R*2
fig, ax = plt.subplots(figsize =(wid,hei))
fig.subplots_adjust(left = 0, right = 1, bottom = 0, top = 1)
y = R
x = 0
for i in range(5):
x += radius[i]
ax.pie(sizes[i], startangle = 0, colors = colors,
radius = radius[i], center = (x,y) )
x += radius[i]
ax.set(xlim =(0,x), ylim=(0,R*2))
plt.savefig("aaa.png")
Notice that my figure aspect ratio is not the (20,4) of the question, which does not hold for the way I interpreted the intended result.
But it might be the case that there is the need of having these in different axes. If so, the idea is:
Use gridspec to create a single row with 5 columns and provide the ratios so that they correspond to the required radius.
Plot the larger pie in the left slot.
In all remaining slots, use a subgrid, dividing into a column of three (sub-)slots.
Set the height ratios so that the middle one ends up with an aspect ratio of a square.
Plot the pies in the middle slots.
Here we go:
import matplotlib.pyplot as plt
sizes = [ [108295, 10107, 7220, 11551, 7220],
[77882, 6676, 6676, 13351, 6676],
[55652, 4417, 6184, 15900, 6184],
[36327, 2642, 4632, 16512, 5944],
[18781, 1409, 3287, 1878, 4695]]
colors = ('red', 'darkblue', 'orange', 'yellow', 'green')
R = 4
radius = [R*i for i in [1.0, 0.77, 0.61, 0.46, 0.33] ]
wid = sum(radius)*2
hei = R*2
ratios = [i/radius[0] for i in radius] # for gridspec
fig = plt.figure(figsize =(wid,hei))
gs = fig.add_gridspec(1, 5,
width_ratios = ratios,
wspace=0, left = 0, right = 1, bottom = 0, top = 1)
ax = fig.add_subplot(gs[0,0])
ax.pie(sizes[0], startangle = 0, colors = colors, radius = 1 )
ax.set(xlim=(-1,1) ,ylim=(-1,1))
for i in range(1,5):
mid = ratios[i]/sum(ratios)*wid
inrat = [(hei-mid)/2, mid, (hei-mid)/2]
ings = gs[0,i].subgridspec(3, 1, hspace=0,
height_ratios = inrat)
ax = fig.add_subplot(ings[1,0])
ax.pie(sizes[i], startangle = 0, colors = colors, radius = 1 )
ax.set(xlim=(-1,1), ylim=(-1,1))
plt.savefig("aaa.png")

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:

Adding labels to bokeh pie chart wedge

I am new to bokeh, and want to render a pie chart using bokeh figure.
I used the reference from https://docs.bokeh.org/en/latest/docs/gallery/pie_chart.html in order to create my pie chart figure.
Now, I need to add on each part of the pie chart a label which represent the percentage of this part, and the label position should be align to the center.
I could not find a simple way to do it via the documentation, and try to find ways to do it manually, like this example: Adding labels in pie chart wedge in bokeh
I tried to create a label set and add the layout to the plot but i could not figure out if there is a way to control the label position, size, and font. text_align (right, left, center) does not do the job for me.
Here is my code - this function create and return an html of the pie chart
The chart argument contains the relevant data for the chart. in this case its a tuple (size 1), and series[0] contains the name of the series (series.title), list of x values (series.x), and list of y values (series.y)
def render_piechart(self, chart):
"""
Renders PieChart object using Bokeh
:param chart: Pie chart
:return:
"""
series = chart.series[0]
data_dict = dict(zip(series.x, series.y))
data = pd.Series(data_dict).reset_index(name='value').rename(columns={'index': 'Category'})
data['angle'] = data['value'] / data['value'].sum() * 2 * pi
data['color'] = palette[:len(series.x)]
data['percentage'] = data['value'] / data['value'].sum() * 100
data['percentage'] = data['percentage'].apply(lambda x: str(round(x, 2)) + '%')
TOOLTIPS = [('Category', '#Category'), ('Value', '#value'), ('Percentage', '#percentage')]
fig = figure(title=series.title,
plot_width=400 if chart.sizehint == 'medium' else 600,
plot_height=350 if chart.sizehint == 'medium' else 450,
tools='hover', tooltips=TOOLTIPS, x_range=(-0.5, 1.0))
fig.wedge(x=0, y=1, radius=0.45, start_angle=cumsum('angle', include_zero=True),
end_angle=cumsum('angle'), line_color='white', fill_color='color',
legend='Category', source=data)
fig.title.text_font_size = '20pt'
source = ColumnDataSource(data)
labels = LabelSet(x=0, y=1, text='percentage', level='glyph', angle=cumsum('angle', include_zero=True),
source=source, render_mode='canvas')
fig.add_layout(labels)
fig.axis.axis_label = None
fig.axis.visible = False
fig.grid.grid_line_color = None
return bokeh.embed.file_html(fig, bokeh.resources.CDN)
And this is the results:
pie chart consist of 3 parts
pie chart consist of 10 parts
in the 2 examples - the series title is 'kuku'
x and y values for the first example:
x=["A", "B", "C"]
y=[10, 20, 30]
and for the second example:
x=["A", "B", "C", "D", "E", "F", "G", "H", "I"]
y=[10, 20, 30, 100, 90, 80, 70, 60, 30 , 40 ,50]
I know that in the past i could do it easily with Donut but it is deprecated.
I want to be able to get something like this one:
example1
or this: example2
The problem, as you understand, is here:
labels = LabelSet(x=0, y=1, text='percentage', level='glyph', angle=cumsum('angle', include_zero=True), source=source, render_mode='canvas')
It's a bit confusing to create labels in Bokeh, but still:
you should add columns like 'text_pos_x' and 'text_pos_y' for every row you draw and fill it in with coordinates where you would like to place the text. And then apply it in LabelSet function, giving x='text_pos_x' and y='text_pos_y' so that every single part of plot have its own coordinates where to place a label:
labels = LabelSet(x='text_pos_x', y='text_pos_y', text='percentage', level='glyph', angle=0, source=source, render_mode='canvas')
and yes, it's necessary to set angle = 0 to avoid text being rotated.
To complete #Higem 's answer I would suggest you some formula to centre your labels correctly on your pie chart. I modified your code as follows:
def render_piechart(self, chart):
"""
Renders PieChart object using Bokeh
:param chart: Pie chart
:return:
"""
radius = 0.45 # Radius of your pie chart
series = chart.series[0]
data_dict = dict(zip(series.x, series.y))
data = pd.Series(data_dict).reset_index(name='value').rename(columns={'index': 'Category'})
data['angle'] = data['value'] / data['value'].sum() * 2 * pi
data['color'] = palette[:len(series.x)]
data['percentage'] = data['value'] / data['value'].sum() * 100
data['percentage'] = data['percentage'].apply(lambda x: str(round(x, 2)) + '%')
# Projection on X and Y axis for label positioning
data['label_x_pos'] = np.cos(data['angle'].cumsum()-data['angle'].div(2))*3*radius/4
data['label_y_pos'] = np.sin(data['angle'].cumsum()-data['angle'].div(2))*3*radius/4
TOOLTIPS = [('Category', '#Category'), ('Value', '#value'), ('Percentage', '#percentage')]
fig = figure(title=series.title,
plot_width=400 if chart.sizehint == 'medium' else 600,
plot_height=350 if chart.sizehint == 'medium' else 450,
tools='hover', tooltips=TOOLTIPS, x_range=(-0.5, 1.0))
fig.wedge(x=0, y=0, radius=radius, start_angle=cumsum('angle', include_zero=True),
end_angle=cumsum('angle'), line_color='white', fill_color='color',
legend='Category', source=data) # Change center of the pie chart to (0, 0)
fig.title.text_font_size = '20pt'
source = ColumnDataSource(data)
labels = LabelSet(x='label_x_pos', y='label_y_pos', text='percentage', level='glyph', text_align='center', source=source, render_mode='canvas')
fig.add_layout(labels)
fig.axis.axis_label = None
fig.axis.visible = False
fig.grid.grid_line_color = None
return bokeh.embed.file_html(fig, bokeh.resources.CDN)
The result is the following:
I used the basic formula to convert polar coordinates to cartesian coordinates, see Wikipedia.

add labels and customize nodes in plotly sankey diagram

I am trying to customize a sankey diagram done with pyplot. I would simply like to make some nodes invisible (e.g. transparent) and add some labels to the connection between nodes.
I modified slightly the code given as an example in the website, without much success. The node properties seem to be the same for all the same. Adding a label to the links does not modify the output.
import plotly.plotly as py
data = dict(
type='sankey',
node = dict(
pad = 15,
thickness = 20,
line = dict(
color = "black",
width = 0.5
),
label = ["A1", "A2", "B1", "B2", "C1", "C2"],
color = ["blue", "blue", "blue", "blue",
"white", "white"]),#attempt to make it less visible
link = dict(
source = [0,1,0,2,3,3],
target = [2,3,3,4,4,5],
value = [8,4,2,8,4,2],
# attempt to add labels
label= [8,4,2,8,4,2]))
layout = dict(
title = "Basic Sankey Diagram",
font = dict(
size = 10
)
)
fig = dict(data=[data], layout=layout)
py.iplot(fig, validate=False)
Any suggestion on how to do it?
You could just add annotations for each of the link labels, the problem is, even if you add label to the links, it will get shown only on hover! below is an example of added annotations for your reference!
import plotly.graph_objs as go
import plotly.offline as py
py.init_notebook_mode()
import numpy as np
data = dict(
type='sankey',
node = dict(
pad = 15,
thickness = 20,
line = dict(
color = "black",
width = 0.5
),
label = ["A1", "A2", "B1", "B2", "C1", "C2"],
color = ["blue", "blue", "blue", "blue",
"gray", "white"]),#attempt to make it less visible
link = dict(
source = [0,1,0,2,3,3],
target = [2,3,3,4,4,5],
value = [8,4,2,8,4,2],
# attempt to add labels
label= [8,4,2,8,4,2]))
layout = dict(
title = "Basic Sankey Diagram",
font = dict(
size = 10
),
annotations=[
dict(
x=0.25,
y=0.75,
text='8',
showarrow=False
),
dict(
x=0.75,
y=0.25,
text='4',
showarrow=False
)
]
)
fig = dict(data=[data], layout=layout)
py.iplot(fig, validate=False)

Resources