Fixed bar width/thickness in plotly.express.timeline - python

I'm using Plotly Express to draw a Gantt graph.
I don't know in advance the number of horizontal lines (the number of "tasks").
I would like the bars to be always the same thickness. (I believe the attribute is width, but since the bars are horizontal, "width" might be ambiguous.)
When plotting the figure directly, it takes the whole space on the page and when resizing the page, the bars change size accordingly.
import datetime as dt
import plotly.express as px
data = [
{'Task': '1-2', 'Start': '2021-09-26', 'Finish': '2021-09-27', 'Resource': 'R1'},
{'Task': '2-1', 'Start': '2021-09-23', 'Finish': '2021-09-24', 'Resource': 'R2'},
{'Task': '2-2', 'Start': '2021-09-26', 'Finish': '2021-09-27', 'Resource': 'R2'},
{'Task': '3-1', 'Start': '2021-09-30', 'Finish': '2021-10-01', 'Resource': 'R3'},
{'Task': '3-2', 'Start': '2021-11-26', 'Finish': '2021-11-27', 'Resource': 'R3'},
]
fig = px.timeline(
data,
x_start="Start",
x_end="Finish",
y="Resource",
)
fig.show()
There is a width attribute on each bar but it controls how much of the allowed space the bar uses, so it doesn't actually fix the width.
A cheap trick is to set the graph height according to the number of lines but one must take into account the surroundings (legend, title, etc.) so it is not really linear. And anyway, it would be at best a sorry workaround.
When I integrate the graph in a page like
Python code
return plotly.io.to_json(fig)
HTML page
<script src="https://cdn.plot.ly/plotly-2.0.0.min.js"></script>
<script type="text/javascript">
let graph = {{ gantt_json|safe }};
Plotly.newPlot('gantt_graph', graph, {});
</script>
the graph doesn't take the whole page, but still the bar width changes when the number of bar changes.
So I can control the total graph size, and the width of each bar relative to its allowed space, but I can't seem to find a way to get a fixed width in pixels.
(I thought this issue and closing PR would help but they didn't.)
Am I missing something?

thinking laterally this can be achieved by controlling domain of yaxis
have simulated data, which will generate a random number of timelines
based on number of timelines set domain min value
import pandas as pd
import numpy as np
import plotly.express as px
T = 8
data = pd.DataFrame(
{
"Start": pd.date_range("1-jan-2021", freq="2D", periods=T),
"Finish": pd.date_range("14-jan-2021", freq="2D", periods=T),
"Resource": np.random.choice(
[chr(i) for i in range(ord("A"), ord("A") + T)], T
),
}
)
color_map = {chr(ord("A") + i): px.colors.qualitative.Alphabet[i%len(px.colors.qualitative.Alphabet)] for i in range(T)}
fig = px.timeline(
data,
x_start="Start",
x_end="Finish",
y="Resource",
color="Resource",
color_discrete_map=color_map,
# height=10 * len(data) # This sucks
)
fig.update_xaxes(
side="top",
dtick="D1",
tickformat="%e\n%b\n%Y",
ticklabelmode="period",
tickangle=0,
fixedrange=True,
)
fig.update_yaxes(
title="",
tickson="boundaries",
fixedrange=True,
)
BARHEIGHT = .1
fig.update_layout(
yaxis={"domain": [max(1 - (BARHEIGHT * len(fig.data)), 0), 1]}, margin={"t": 0, "b": 0}
)

Related

Plot bar charts on a map in plotly

I want to plot a bar chart on a map created with plotly, similar to the QGIS plot here. Ideally, the bar chart would be stacked and grouped instead of just grouped. So far, I only found examples for pie charts on plotly maps, for instance here.
with plotly mapbox you can add layers
with plotly you can generate images from figures
using above two facts you can add URI encoded images to a mapbox figure
you have not provided any sample geometry or data. Have used a subset geopandas sample geometry plus generated random data for each country (separate graph)
the real key to this solution is layer-coordinates
get centroid of a country
add a buffer around this and get envelope (bounding rectangle)
arrange co-ordinates of envelope to meet requirements stated in link
import geopandas as gpd
import plotly.express as px
import numpy as np
import base64, io
# create an encocded image of graph...
# change to generate graph you want
def b64image(vals=np.random.randint(1, 25, 5)):
fig = px.bar(
pd.DataFrame({"y": vals}).pipe(
lambda d: d.assign(category=d.index.astype(str))
),
y="y",
color="category",
).update_layout(
showlegend=False,
xaxis_visible=False,
yaxis_visible=False,
bargap=0,
margin={"l": 0, "r": 0, "t": 0, "b": 0},
autosize=False,
height=100,
width=100,
paper_bgcolor="rgba(0,0,0,0)",
plot_bgcolor="rgba(0,0,0,0)",
)
b = io.BytesIO(fig.to_image(format="png"))
b64 = base64.b64encode(b.getvalue())
return "data:image/png;base64," + b64.decode("utf-8"), fig
# get some geometry
world = gpd.read_file(gpd.datasets.get_path("naturalearth_lowres"))
# let's just work with a bounded version of europe
eur = world.loc[
lambda d: d["continent"].eq("Europe")
& ~d["iso_a3"].isin(["RUS", "NOR", "FRA", "ISL"])
]
px.choropleth_mapbox(
eur,
geojson=eur.__geo_interface__,
locations="iso_a3",
featureidkey="properties.iso_a3",
color_discrete_sequence=["lightgrey"],
).update_layout(
margin={"l": 0, "r": 0, "t": 0, "b": 0},
showlegend=False,
mapbox_style="carto-positron",
mapbox_center={
"lon": eur.unary_union.centroid.x,
"lat": eur.unary_union.centroid.y,
},
mapbox_zoom=3,
# add a plotly graph per country...
mapbox_layers=[
{
"sourcetype": "image",
# no data provided, use random values for each country
"source": b64image(vals=np.random.randint(1, 25, 5))[0],
# https://plotly.com/python/reference/layout/mapbox/#layout-mapbox-layers-items-layer-coordinates
# a few hops to get 4 cordinate pairs to meet mapbox requirement
"coordinates": [
list(p) for p in r.geometry.centroid.buffer(1.1).envelope.exterior.coords
][0:-1][::-1],
}
for i, r in eur.iterrows()
],
)
output

how to add image to plot mplfinance python

Trying to add image and price label and add more space on time and it seems like ylim= takes care of that but when i add it my whole graph disappears.
market_colors = mpf.make_marketcolors(
base_mpf_style="charles"
)
rc = {
"axes.labelcolor": "none",
"axes.spines.bottom": True,
"axes.spines.left": False,
"axes.spines.right": False,
"axes.spines.top": False,
"font.size": 10,
}
styles = mpf.make_mpf_style(
base_mpf_style="nightclouds",
marketcolors=market_colors,
gridstyle="",
rc=rc
)
filledShape = {
"y1": df['Close'].values,
"facecolor": "#2279e4"
}
(mpf.plot(df, type='line',
title='Test',
linecolor='white',
style=styles,
volume=True,
figsize=(8, 6),
figscale=0.5,
fill_between=filledShape, tight_layout=True,
scale_padding={'left': 1, 'top': 5, 'right': 1, 'bottom': 2}
))
There are three techniques that I know of to display an image on a matplotlib plot:
Axes.imshow()
Figure.figimage()
Putting the image in an AnnotationBbox
In terms of working with mplfinance, I would say that technique one, calling Axes.imshow() is probably simplest:
Step 1:
For all three of the above techniques, when you call mpf.plot() set kwarg returnfig=True:
fig axlist = mpf.plot(df,...,returnfig=True)
This will give you access to the mplfinance Figure and Axes objects.
Step 2:
Now create a new Axes object where you want the image/logo:
# Note: [left,bottom,width,height] are in terms of fraction of the Figure.
# For example [0.05,0.08,0.10,0.06] means:
# the lower/left corner of the Axes will be located:
# 5% of the way in from the left
# 8% down from the top,
# and the Axes will be
# 10% of the Figure wide and
# 6% of the Figure high.
logo_axes = fig.add_axes([left,bottom,width,height])
Step 3:
Read in the image:
import Image
im = Image.open('image_file_name.png')
Step 4:
Call imshow() on the newly created Axes, and turn of the axis lines:
logo_axes.imshow(im)
logo_axes.axis('off')
Step 5:
Since returnfig=True causes mplfinance to not show the Figure, call mpf.show()
mpf.show()
I'm not sure if this answer will help you or not since I'm not sure what kind of images you want to add. I assume you want to add a corporate logo or something like that, so I did some research and found an answer to whether you can add a watermark to an mpf. I used this answer as a guide and added the icons used on stackoveflow.com to the graph. However, it was not possible to add them to the axes, so I had to add them to the fig. I have changed the style to add the image.
img = plt.imread('./data/se-icon.png')
market_colors = mpf.make_marketcolors(
base_mpf_style="charles"
)
rc = {
"axes.labelcolor": "none",
"axes.spines.bottom": True,
"axes.spines.left": False,
"axes.spines.right": False,
"axes.spines.top": False,
"font.size": 10,
}
styles = mpf.make_mpf_style(
base_mpf_style="yahoo",# nightclouds
marketcolors=market_colors,
gridstyle="",
rc=rc
)
filledShape = {
"y1": df['Close'].values,
"facecolor": "#2279e4"
}
fig, axes = mpf.plot(df, type='line',
title='Test',
linecolor='white',
style=styles,
volume=True,
figsize=(8, 6),
figscale=0.5,
fill_between=filledShape,
tight_layout=True,
scale_padding={'left': 1, 'top': 5, 'right': 1, 'bottom': 2},
returnfig=True
)
#axes[0].imshow(img)
#height = img.shape[1]
fig.figimage(img, 0, fig.bbox.ymax - height*1.5)
plt.show()

Deal with overlapping in multiple x-axes in plotly python

I am trying to create a plot using plotly with multiple axes. And for this, I am using the following code:
#Plotly libraries and options for graphic logic
from plotly.io import to_html
import plotly.io as pio
pio.renderers.default='browser'
import plotly.graph_objects as go
#Generic libraries
import pandas as pd
import numpy as np
from datetime import datetime
input_df = pd.read_csv('https://raw.githubusercontent.com/plotly/datasets/master/finance-charts-apple.csv')
threshold =2.8
name_yaxis="Gap"
input_df["AAPL.High"] = (input_df["AAPL.High"]-min(input_df["AAPL.High"]))*(threshold)/(max(input_df["AAPL.High"])-min(input_df["AAPL.High"]))+np.random.uniform(0.3,0.4,1)
ID_TAIL = "ID_1"
fig = go.Figure()
fig.add_trace(go.Scatter(x=input_df['Date'], y=input_df['AAPL.High'],
mode='lines+markers',
marker_size=12,
line = dict(color="#C4C4C4"),
marker=dict(color=( (0 < input_df['AAPL.High']) & (input_df['AAPL.High'] < threshold)).astype('int'),
colorscale=[[0, '#A51890'], [1, '#3BBFFE']]
),
showlegend=False,
xaxis="x1",
name = ""
)
)
my_x = [ID_TAIL + "_" +format(i, '04d') + "_0" for i in range(1,input_df.shape[0])]
fig.add_trace(go.Scatter(x=my_x, y=input_df['AAPL.High'],
mode='lines+markers',
marker_size=12,
line = dict(color="#C4C4C4"),
marker=dict(color=( (0 < input_df['AAPL.High']) & (input_df['AAPL.High'] < threshold)).astype('int'),
colorscale=[[0, '#A51890'], [1, '#3BBFFE']]
),
showlegend=False,
xaxis="x2",
name = ""
)
)
#== Add title boxes ==#
# Add title legend for box status
fig.add_annotation( text="<b>Health status<b>", xref="paper", yref="paper",
x=1.02, xanchor="left",
y=0.9, yanchor="bottom", # Same y as legend below
showarrow=False,
font = dict(family = "Roboto", size = 10))
#== End ==#
My problem is that as you can see in the following image, the ticks are overlapping:
So, my question is, how to create space between them?
Thanks in advance.
Here's a quick fix. Pop this line at the bottom of your code, and it will move xaxis2 to the top of the graph:
fig.update_layout({'xaxis2': {'side': 'top', 'tickangle': 45, 'nticks': 50}})
Output:
Shifting the secondary xaxis to the top will look like this.
Another Option:
Another approach would be to concatenate the axis titles into a single string, and display the concatenated string on the x-axis. This SO answer demonstrates this logic.
You can reduce the number of ticks by adding the following line
fig.update_layout(xaxis={'nticks': 8, 'tickangle': 90}, xaxis2={'nticks': 8, 'tickangle': 90})
Depending on the size of the plot, ticks may still overlap. In that case, you can either further reduce the tick number or hardcode the tick positions:
tickvalsX = ['2015-07', '2016-01', '2016-07', '2017-01']
tickvalsY = ['ID_1_0001_0', 'ID_1_00100_0', 'ID_1_0200_0', 'ID_1_0300_0', 'ID_1_0400_0', 'ID_1_0500_0']
fig.update_layout(xaxis={'tickmode': 'array', 'tickangle': 90, 'tickvals': tickvalsX}, xaxis2={'tickmode': 'array', 'tickangle': 90, 'tickvals': tickvalsY})
Further style elements of the axis you can find in the Plotly reference.

Python Dash graph legend covering x-axis labels

My Python web app has a Plotly Dash "Graph" whose legend covers the x-axis labels. I've tried adjusting the following elements, with no success and no visible changes at all:
legend style 'margin-top'
margin 'b'
padding 'b'
Here's the code:
import dash
import dash_core_components as dcc
import dash_html_components as html
graph = dcc.Graph(
figure = {
'data': data,
'layout': dict(
hovermode = "closest",
height = 400, # 500 is a bit too big on a smartphone
legend = dict(
font=dict(color='#7f7f7f'),
orientation="h", # Looks much better horizontal than vertical
style={'margin-top': 100},
),
font = {
'family': 'Segoe UI',
'color': "#7f7f7f"
},
# Added more margin on the left side to fix the cutoff True/False labels on the booleans
margin = dict(l=40, r=25, b=10, t=10),
padding = dict(l=0, r=0, b=10, t=0),
)
}
)
Here's what it looks like, showing the legend overlapping the x-axis labels:
I found the solution here in the documentation.
y
Parent: layout.legend
Type: number between or equal to -2 and 3
Sets the y position (in normalized coordinates) of the legend. Defaults to "1" for
vertical legends, defaults to "-0.1" for horizontal legends on graphs w/o range sliders and defaults to "1.1" for horizontal legends on graph with one or multiple range sliders.
It defaults to -0.1 so I set it to -0.15, which is a little bit lower, to give the x-axis labels some more room.
import dash
import dash_core_components as dcc
import dash_html_components as html
graph = dcc.Graph(
figure = {
'data': data,
'layout': dict(
hovermode = "closest",
height = 400, # 500 is a bit too big on a smartphone
legend = dict(
font=dict(color='#7f7f7f'),
orientation="h", # Looks much better horizontal than vertical
y=-0.15
),
)
}
)
Result:

Plotly: How to select graph source using dropdown?

I'm trying to embed multiple, selectable graphs in a single figure using Plotly, using a dropdown figure. I followed the dropdown example from Plotly, but they only show how to change graph characteristics (like visible, or type), not the underlying data. In my situation, I have a static X-axis and want to change the Y-values. Here's a minimal working example that can be run in a jupyter notebook:
import plotly
from plotly import graph_objs as go, offline as po, tools
po.init_notebook_mode()
import numpy as np
import json
x = list(np.linspace(-np.pi, np.pi, 100))
values_1 = list(np.sin(x))
values_2 = list(np.tan(x))
line = go.Scatter(
x=x,
y=values_1
)
updatemenus = [
{
'buttons': [
{
'method': 'restyle',
'label': 'Val 1',
'args': [
{'y': json.dumps(values_1)},
]
},
{
'method': 'restyle',
'label': 'Val 2',
'args': [
{'y': json.dumps(values_2)},
]
}
],
'direction': 'down',
'showactive': True,
}
]
layout = go.Layout(
updatemenus=updatemenus,
)
figure = go.Figure(data=[line], layout=layout)
po.iplot(figure)
However, while the approach seems to work like advertised for general graph attributes (like 'visible'), when I use 'y', it produces a straight line, where y goes from 0 to len(y), instead of the actual data I gave it. Here are images of the initial render, and then what happens when I select the dropdown item for the Tan(X) graph, then go back to the Sin(X):
How do I embed the data for multiple graphs into a single figure so that the user can select which one they want to view?
Updated answer using graph_objects:
As of version 4, you don't have to worry about offline versus online functionality. So drop the from plotly import graph_objs as go, offline as po and po.init_notebook_mode(), and just use import plotly.graph_objects as go. I've updated my original answer with a complete code snippet that shows the whole approach with multiple traces using plotly.graph_objects at the end. The solution to the question as it still stands will still be the same, namely:
'y' in updatemenus does not take a single list as an argument, but rather a list of lists like in 'y' = [values_1] where values_1 is a list in itself. So just replace your lines
{'y': json.dumps(values_1)}, and {'y': json.dumps(values_2)},
with
{'y': [values_1]}, and {'y': [values_2]},
to get these plots for the different options Val 1 and Val 2:
Some Details:
Values_1 is, unsurprisingly, a list of length 100 where each element is of type numpy.float. Replacing json.dumps(values_1) with values_1, and json.dumps(values_2) with values_2 will render the same plots as in your question. The reason why these plots are just straight lines, seems to be that it's the length of your lists that are being plotted, and not the values contained in that list. Or something to that effect.
Setting 'y' = values_1 is the same thing as assigning a single list to 'y'. But 'y' in updatemenus does not take a single list as an argument, but rather a list of lists like in 'y' = [values_1]. Why? Because you might want to plot multiple lists in the same figure like 'y' = [values_1, values_1b]. Have a look:
Plot for dropdown option Var 1:
Plot for dropdown option Var 2
Complete original code:
import plotly
from plotly import graph_objs as go, offline as po, tools
po.init_notebook_mode()
import numpy as np
import json
x = list(np.linspace(-np.pi, np.pi, 100))
values_1 = list(np.sin(x))
values_1b = [elem*-1 for elem in values_1]
values_2 = list(np.tan(x))
values_2b = [elem*-1 for elem in values_2]
line = go.Scatter(
x=x,
y=values_1
)
line2 = go.Scatter(
x=x,
y=values_1b
)
updatemenus = [
{
'buttons': [
{
'method': 'restyle',
'label': 'Val 1',
'args': [
{'y': [values_1, values_1b]},
]
},
{
'method': 'restyle',
'label': 'Val 2',
'args': [
{'y': [values_2, values_2b]},
]
}
],
'direction': 'down',
'showactive': True,
}
]
layout = go.Layout(
updatemenus=updatemenus,
)
figure = go.Figure(data=[line, line2], layout=layout)
po.iplot(figure)
Complete updated code:
# imports
import plotly.graph_objects as go
import numpy as np
# data
x = list(np.linspace(-np.pi, np.pi, 100))
values_1 = list(np.sin(x))
values_1b = [elem*-1 for elem in values_1]
values_2 = list(np.tan(x))
values_2b = [elem*-1 for elem in values_2]
# plotly setup]
fig = go.Figure()
# Add one ore more traces
fig.add_traces(go.Scatter(x=x, y=values_1))
fig.add_traces(go.Scatter(x=x, y=values_1b))
# construct menus
updatemenus = [{'buttons': [{'method': 'update',
'label': 'Val 1',
'args': [{'y': [values_1, values_1b]},]
},
{'method': 'update',
'label': 'Val 2',
'args': [{'y': [values_2, values_2b]},]}],
'direction': 'down',
'showactive': True,}]
# update layout with buttons, and show the figure
fig.update_layout(updatemenus=updatemenus)
fig.show()
Plot with version 4 default layout:

Categories

Resources