How to set annotation of hrect referenced to a secondary y axis - python

I'm using plotly to create some charts. I have 2 y axis available and I'm creating some horizontal rectangle referenced to a secondary y axis. Which is working fine, hrect is displayed where it has to be. But problem is that annotation linked to that hrect is not being referenced to that secondary axis. So I get a mess in representation.
This is how I define hrect
fig.add_hrect(y0=current_quantile, y1=current_quantile, fillcolor="black",
annotation_position="bottom right", annotation_text="Current performance",
line_width=1, secondary_y=True)
And this is what I get
If I modify html code and change annotation yref to y2 then it works as expected.
In my point of view this looks like a bug from plotly, if you are referring hrect to secondary axis, it has no sense that its annotation references to another axis. But I was wondering if there is some workaround which could allow me to fix this. I've tried to define annotate=dict(yref="y2") but no luck, the same with annotation_yref="y2"
According to official documentation, not sure if yref can be changed in hrect call
Thanks a lot for your help
Edit for adding a while chart representation to focus over 2 y-axis information being displayed.

started by creating a figure with two yaxes
I did find that yref="y2" is being ignored by add_hrect()
hence resolved by moving to add_shape() and add_annotation()
import pandas as pd
import plotly.express as px
import numpy as np
r = np.random.RandomState(22)
df = pd.DataFrame(
{
"x": np.linspace(0, 100, 50),
"y1": r.uniform(1, 10, 50),
"y2": r.uniform(30, 50, 50),
}
)
# create a figure with two yaxes
fig = (
px.line(df, x="x", y=["y1", "y2"])
.update_traces(yaxis="y2", selector={"name": "y2"})
.update_layout(yaxis2={"side": "right", "range": [0, 50], "overlaying":"y"})
)
current_quantile = 25
# wrong place for text
# fig.add_hrect(
# y0=current_quantile,
# y1=current_quantile,
# annotation_position="bottom right",
# annotation_text="Current performance",
# yref="y2",
# xref="paper"
# )
# correct position for text
fig.add_shape(
type="rect",
yref="y2",
xref="paper",
y0=current_quantile,
y1=current_quantile,
x0=0,
x1=1,
)
fig.add_annotation(
yref="y2",
xref="paper",
y=current_quantile,
yshift=10,
x=1,
text="Current performance",
showarrow=False
)
fig

Related

Fully display the amount in horizontal bar chart

With fig.update_traces(textposition="outside", textangle=0 ), chart plus text over the background will not able to fully display.
With fig.update_traces(textposition="inside", textangle=0 ), chart too short will not fully display the text amount as well.
So, is there any way to make it win-win situation?
fig = px.bar(pie_bar_gp, x='Amount', y='Product', title='Pie-Bar' ,orientation='h'
,text='Amount', text_auto=",.2f"
)
fig.update_layout(barmode="group")
fig.update_layout({
'paper_bgcolor': 'rgba(0, 0, 0, 0)',
})
chart with:
fig.update_traces(textposition="inside", textangle=0 )
fig.update_traces(textposition="outside", textangle=0 )
Yes, you can do this if you provide a list of text positions for each bar.
positions = ['inside','inside','inside','outside','inside', 'inside']
fig = px.bar(pie_bar_gp, x='Amount', y='Product',
title='Pie-Bar', orientation='h', text='Amount', text_auto=",.2f")
fig.update_traces(textposition=positions)
fig.show()
See the answer here:
https://stackoverflow.com/a/68337253/10487273
In such cases, you can pass a list of the positions you wish to display for each value. The example in the reference is forced to limit the range of the x-axis, creating the same situation as your assignment. I have set my threshold as 25, anything below that is outside and everything else is inside.
import plotly.express as px
data_Rwanda = px.data.gapminder().query("country == 'Rwanda'")
txt_position = ['outside' if x <= 25 else 'inside' for x in data_canada['lifeExp']]
fig = px.bar(data_Rwanda, x='lifeExp', y='year', orientation='h',text='lifeExp',text_auto=',.2f')
fig.update_xaxes(range=[23,50])
fig.update_traces(textposition=txt_position, textfont=dict(color='red', size=14))
fig.update_layout(autosize=True, height=600)
fig.show()

Show name of a trace on scatterpolar chart all the time without needing to hover over it

I am trying to figure out if there is a way to have the name of a Python plotly scatterpolar trace to always be visible, rather than having to hover over each trace on the graph. This is what I have so far in terms of code.
import plotly.graph_objects as go
categories = ['Passing', 'Dribbling', 'Shooting', 'Defense', 'Fitness']
fig = go.Figure()
fig.add_traces(go.Scatterpolar(
r=[6.33, 3.71, 0, 5.45, 5],
theta=categories,
fill='toself',
name='Team Average'
))
fig.add_traces(go.Scatterpolar(
r=[9.38, 2.86, 0, 5.0, 5.6],
theta=categories,
fill='toself',
name='Player Average'
))
fig.update_layout(
polar=dict(
radialaxis=dict(
visible=False,
range=[0,10]
)
),
showlegend=False
)
fig.show()
And this is what the current scatterpolar chart looks like when I run it. As you can see, it does not show the names of each of the traces, and only appears when I hover over each trace.
With a go.Scatterpolar chart, text annotations are difficult because you will need to specify the cartesian x- and y-coordinates for the text annotations. Polar coordinates for text annotations inside the chart are not yet available, at least according to the linked Plotly forum post. While you could convert the polar coordinates of each point to x- and y-coordinates, and then add text at each of these locations, this seems like a heavy handed solution unless it's really necessary.
One compromise would be to use px.line_polar to draw the chart, and use the text argument to specify what text gets added for each point. Unfortunately you can only choose one field from your data (in your case, you can choose to display the value that you are passing to parameter r, or the category that you are passing to parameter theta).
To make px.line_polar look like go.Scatterpolar, you will want to add filling between the lines. In addition, to add the second px.line_polar chart on top of the first one, you'll need to create a new figure, then add that figure's data as a trace. You will also need to manually specify the color of the second px.line_polar chart.
import plotly.express as px
import plotly.graph_objects as go
categories = ['Passing', 'Dribbling', 'Shooting', 'Defense', 'Fitness']
fig = go.Figure()
fig = px.line_polar(
{'Team Average':[6.33, 3.71, 0, 5.45, 5], 'direction':categories},
r="Team Average",
theta="direction",
start_angle=360,
line_close=True,
text="Team Average",
)
fig2 = px.line_polar(
{'Player Average':[9.38, 2.86, 0, 5.0, 5.6], 'direction':categories},
r="Player Average",
color_discrete_sequence=["salmon"]*5,
theta="direction",
start_angle=360,
line_close=True,
text="Player Average",
)
## add fig2 to fig
fig.add_trace(fig2.data[0])
fig.update_traces(textposition='top center', fill='toself')
fig.update_layout(
polar=dict(
radialaxis=dict(
visible=False,
range=[0,10]
)
),
showlegend=False
)
fig.show()

Plotly: How to set a fill color between two vertical lines?

Using matplotlib, we can "trivially" fill the area between two vertical lines using fill_between() as in the example:
https://matplotlib.org/3.2.1/gallery/lines_bars_and_markers/fill_between_demo.html#selectively-marking-horizontal-regions-across-the-whole-axes
Using matplotlib, I can make what I need:
We have two signals, and I''m computing the rolling/moving Pearson's and Spearman's correlation. When the correlations go either below -0.5 or above 0.5, I want to shade the period (blue for Pearson's and orange for Spearman's). I also darken the weekends in gray in all plots.
However, I'm finding a hard time to accomplish the same using Plotly. And it will also be helpful to know how to do it between two horizontal lines.
Note that I'm using Plotly and Dash to speed up the visualization of several plots. Users asked for a more "dynamic type of thing." However, I'm not a GUI guy and cannot spend time on this, although I need to feed them with initial results.
BTW, I tried Bokeh in the past, and I gave up for some reason I cannot remember. Plotly looks good since I can use either from Python or R, which are my main development tools.
Thanks,
Carlos
I don't think there is any built-in Plotly method that that is equivalent to matplotlib's fill_between() method. However you can draw shapes so a possible workaround is to draw a grey rectangle and set the the parameter layer="below" so that the signal is still visible. You can also set the coordinates of the rectangle outside of your axis range to ensure the rectangle extends to the edges of the plot.
You can fill the area in between horizontal lines by drawing a rectangle and setting the axes ranges in a similar manner.
import numpy as np
import plotly.graph_objects as go
x = np.arange(0, 4 * np.pi, 0.01)
y = np.sin(x)
fig = go.Figure()
fig.add_trace(go.Scatter(
x=x,
y=y
))
# hard-code the axes
fig.update_xaxes(range=[0, 4 * np.pi])
fig.update_yaxes(range=[-1.2, 1.2])
# specify the corners of the rectangles
fig.update_layout(
shapes=[
dict(
type="rect",
xref="x",
yref="y",
x0="4",
y0="-1.3",
x1="5",
y1="1.3",
fillcolor="lightgray",
opacity=0.4,
line_width=0,
layer="below"
),
dict(
type="rect",
xref="x",
yref="y",
x0="9",
y0="-1.3",
x1="10",
y1="1.3",
fillcolor="lightgray",
opacity=0.4,
line_width=0,
layer="below"
),
]
)
fig.show()
You haven't provided a data sample so I'm going to use a synthetical time-series to show you how you can add a number of shapes with defined start and stop dates for several different categories using a custom function bgLevel
Two vertical lines with a fill between them very quickly turns into a rectangle. And rectangles can easily be added as shapes using fig.add_shape. The example below will show you how to find start and stop dates for periods given by a certain critera. In your case these criteria are whether or not the value of a variable is higher or lower than a certain level.
Using shapes instead of traces with fig.add_trace() will let you define the position with regards to plot layers using layer='below'. And the shapes outlines can easily be hidden using line=dict(color="rgba(0,0,0,0)).
Plot 1: Time series figure with random data:
Plot 2: Background is set to an opaque grey when A > 100 :
Plot 2: Background is also set to an opaque red when D < 60
Complete code:
import numpy as np
import pandas as pd
import plotly.graph_objects as go
import plotly.express as px
import datetime
pd.set_option('display.max_rows', None)
# data sample
nperiods = 200
np.random.seed(123)
df = pd.DataFrame(np.random.randint(-10, 12, size=(nperiods, 4)),
columns=list('ABCD'))
datelist = pd.date_range(datetime.datetime(2020, 1, 1).strftime('%Y-%m-%d'),periods=nperiods).tolist()
df['dates'] = datelist
df = df.set_index(['dates'])
df.index = pd.to_datetime(df.index)
df.iloc[0] = 0
df = df.cumsum().reset_index()
# function to set background color for a
# specified variable and a specified level
# plotly setup
fig = px.line(df, x='dates', y=df.columns[1:])
fig.update_xaxes(showgrid=True, gridwidth=1, gridcolor='rgba(0,0,255,0.1)')
fig.update_yaxes(showgrid=True, gridwidth=1, gridcolor='rgba(0,0,255,0.1)')
def bgLevels(fig, variable, level, mode, fillcolor, layer):
"""
Set a specified color as background for given
levels of a specified variable using a shape.
Keyword arguments:
==================
fig -- plotly figure
variable -- column name in a pandas dataframe
level -- int or float
mode -- set threshold above or below
fillcolor -- any color type that plotly can handle
layer -- position of shape in plotly fiugre, like "below"
"""
if mode == 'above':
m = df[variable].gt(level)
if mode == 'below':
m = df[variable].lt(level)
df1 = df[m].groupby((~m).cumsum())['dates'].agg(['first','last'])
for index, row in df1.iterrows():
#print(row['first'], row['last'])
fig.add_shape(type="rect",
xref="x",
yref="paper",
x0=row['first'],
y0=0,
x1=row['last'],
y1=1,
line=dict(color="rgba(0,0,0,0)",width=3,),
fillcolor=fillcolor,
layer=layer)
return(fig)
fig = bgLevels(fig = fig, variable = 'A', level = 100, mode = 'above',
fillcolor = 'rgba(100,100,100,0.2)', layer = 'below')
fig = bgLevels(fig = fig, variable = 'D', level = -60, mode = 'below',
fillcolor = 'rgba(255,0,0,0.2)', layer = 'below')
fig.show()
I think that fig.add_hrect() and fig.add_vrect() are the simplest approaches to reproducing the MatPlotLib fill_between functionality in this case:
https://plotly.com/python/horizontal-vertical-shapes/
For your example, add_vrect() should do the trick.

Hoverinformation for shapes in plotly

I know there is the hovertemplate/hover_text/ option for traces (marker/line) but I cannot find such a thing for shapes.
Is there a way to have a hover text pop up when moving over a shape? Maybe a workaround?
Example:
import plotly.graph_objects as go
fig = go.Figure()
fig.add_trace(go.Scatter(
x=[1.5, 3],
y=[2.5, 2.5],
text=["Rectangle reference to the plot",
"Rectangle reference to the axes"],
mode="markers",
))
fig.add_shape(
# Rectangle reference to the plot
type="rect",
xref="paper",
yref="paper",
x0=0.25,
y0=0,
x1=0.5,
y1=0.5,
line=dict(
color="LightSeaGreen",
width=3,
),
fillcolor="PaleTurquoise",
)
When I hover over the two points, I get a hover-template with information. How can I get something similar for the shape?
It seems that it's not possible to add hoverinfo to shapes directly. But you can obtain something very close to what seems to be the desired effect through the right combination of shapes and traces. The following plot is made from specifying two rectangles in a list like:
shapes = [[2,6,2,6],
[4,7,4,7]]
The rest of the code snippet is set up to be flexible with regards to the number of shapes, and the colors assigned to them and the corresponding traces to make that little dot in the lower right corners of the shapes.
Plot:
If this is something you can use, we can discuss ways to edit what is being displayed in the hoverinfo.
Complete code:
# Imports
import pandas as pd
#import matplotlib.pyplot as plt
import numpy as np
import plotly.graph_objects as go
import plotly.express as px
# shape definitions
shapes = [[2,6,2,6],
[4,7,4,7]]
# color management
# define colors as a list
colors = px.colors.qualitative.Plotly
# convert plotly hex colors to rgba to enable transparency adjustments
def hex_rgba(hex, transparency):
col_hex = hex.lstrip('#')
col_rgb = list(int(col_hex[i:i+2], 16) for i in (0, 2, 4))
col_rgb.extend([transparency])
areacol = tuple(col_rgb)
return areacol
rgba = [hex_rgba(c, transparency=0.4) for c in colors]
colCycle = ['rgba'+str(elem) for elem in rgba]
# plotly setup
fig = go.Figure()
# shapes
for i, s in enumerate(shapes):
fig.add_shape(dict(type="rect",
x0=s[0],
y0=s[2],
x1=s[1],
y1=s[3],
layer='above',
fillcolor=colCycle[i],
line=dict(
color=colors[i],
width=3)))
# traces as dots in the lower right corner for each shape
for i, s in enumerate(shapes):
fig.add_trace(go.Scatter(x=[s[1]], y=[s[2]], name = "Hoverinfo " +str(i + 1),
showlegend=False,
mode='markers', marker=dict(color = colors[i], size=12)))
# edit layout
fig.update_layout(yaxis=dict(range=[0,8], showgrid=True),
xaxis=dict(range=[0,8], showgrid=True))
fig.show()
I thought of a solution I am happy with.
Simply draw a shape. You won't be able to see a hover text. However, if you add a trace with a fill on top of the shape, then set the trace to opacity=0 you will see the hover text from the trace pop up when moving over the shape.
Again, thanks for your responses!
import plotly.graph_objects as go
# Draw shape (you won't be able to add a hover text for it)
fig = go.Figure()
fig.add_shape(
type="rect",
x0=0, y0=0,
x1=4, y1=3,
fillcolor='LightSkyBlue',
line_color='Blue',
name='Shape 1'
)
# Adding a trace with a fill, setting opacity to 0
fig.add_trace(
go.Scatter(
x=[0,0,4,4,0],
y=[0,3,3,0,0],
fill="toself",
mode='lines',
name='',
text='Custom text on top of shape',
opacity=0
)
)
fig.show()

Plotly: Is there a way to only change one of the add_trace elements rather than all?

In python3, I am trying to edit a specific add_trace() plot. For context, I create a Plotly graph with dropdown menus to change/update the plot itself.
I am plotting a 3D scatter which takes x, y, z. But then I added a planar graph to the same figure, go.Surface, which is the planar estimation for the data.
The problem with this is that when I change an attribute, say 'x' for the X-axis, it also changes the 'x' attribute on the go.Surface, which should only be the estimation of the data rather than the data itself.
Is there a way to separate specific add_trace() attributes, so that updating a data parameter won't effect the go.Surface parameter?
Here is an example is used for editing the left and right y axis
in the arg of the menu you need to add a list with a index. [0] is the first trace added [1] the second etc.
ps I'm not a 'code' expert just 'learned' python last week for ChemE thesis so my code works but it probably isn't the best/most efficient
import plotly.graph_objects as go
from plotly.subplots import make_subplots
import pandas as pd
import numpy as np
# Data
x = np.linspace(-np.pi, np.pi, 200)
y1 = np.sin(x) # f(x) = sin(x*pi)
y2 = np.cos(x) #f(x) = cos(x*pi)
y3 = np.tan(x) #f(x) = tan(x*pi)
d = {'x': x, 'y1': y1,'y2': y2,'y3': y3}
df = pd.DataFrame(data=d)
name = 'Test'
def Graph(df,name):
#Make fig
fig = make_subplots(specs=[[{"secondary_y": True}]])
#Add first trace
fig.add_trace(
go.Scatter(x=df['x'],
y=df['y1'],
),
secondary_y=False,)
#Add second trace
fig.add_trace(
go.Scatter(x=df['x'],
y=df['y2'],
),
secondary_y=True,)
# buttons for menu 1, contolling first trace
buttons1=[]
#Dynamic list based on df keys
for dfk in df.keys():
buttons1.append(dict(method='restyle',
label=dfk,
visible=True,
args=[{'y':[df[dfk].values]}, [0]], #The [0] 'locks' it to the first trace
)
)
# buttons for menu 2, contolling seccond trace
buttons2=[]
#Dynamic list based on df keys
for dfk in df.keys():
buttons2.append(dict(method='restyle',
label=dfk,
visible=True,
args=[{'y':[df[dfk].values]}, [1]], #The [1] 'locks' it to the second trace
)
)
#Delete x-axis from dropdown
del buttons1[0]
del buttons2[0]
#List for menus
updatemenu=[]
#add dict for buttons
your_menu1=dict()
updatemenu.append(your_menu1)
your_menu2=dict()
updatemenu.append(your_menu2)
#Fill dict
updatemenu[0]['buttons']=buttons1
updatemenu[0]['x']=0.15 #Some styling
updatemenu[0]['y']=1.12 #Some styling
updatemenu[1]['buttons']=buttons2
updatemenu[1]['x']=0.33 #Some styling
updatemenu[1]['y']=1.12 #Some styling
# add dropdown menus to the figure
fig.update_layout(showlegend=True, updatemenus=updatemenu)
# add notations to the dropdown menus
fig.update_layout(
annotations=[
dict(text="Left y-axis:", x=0, xref="paper", y=1.10, yref="paper",
align="left", showarrow=False),
dict(text="Right y-axis::", x=0.17, xref="paper", y=1.10,
yref="paper", showarrow=False)])
name = str(name)+'.html'
fig.write_html(name, auto_open=True)
Graph(df,name)

Categories