Multiple x-axes with different ticks depending on zoom level

Hi all,

I’m trying to develop a dynamic bar chart for time series data. The chart has a stacked bar for every day–see picture.
I would like the information the x-axis to change depending on the horizontal zoom level (defined as the number of bars in the x-axis range). At the highest zoom level (say, less than 10 bars) the graph should look like the picture. At lower zoom levels (>10 bars) there would be no space for the day-labels so I would like to show week numbers (i.e. one number for every 7 bars, centered) and month names.

I created the included chart using multicategory axes. However, it seems that this feature is not meant for this kind of situation. For one, things go wrong when going into a subsequent year because the weeknumbers reset to 1. Also, the axis labels get messed up if the time-series doesn’t start at the beginning of a week.
Ideally, the x-data of the bars would be set to the actual date. This would also result in the actual date being shown in the hover-information–which is what I would like as well. Then I would manipulate the axes ticks and labels to get the desired result.
However, I’m not sure how to get two (or more) sets of x-axes labels, using a “standard” plot, i.e. not multicategory.
Is this at all possible?

Thanks,
Maarten

OK I managed to solve it myself, at least the part of showing multiple x-axes for day, and week number, without using multi-category.

See below for the code and an example. I created two additional dummy traces with associated x-axes: one for the week numbers and one for the dividers between weeks, for which I used long ticks. Also, I used a custom hovertemplate to assure that the full date is shown in the hover text.
Next step is to change the x-axes depending on the zoom-level, but that requires a dash-app, I think.

It works but it’s kind of hacky. If there’s a better way to do it I’d love to hear it.

cheers,
Maarten

import datetime as dt
import plotly.graph_objs as go
from scipy.stats import norm

nCategory = 5
nDays = 50

# generate bar data
base = dt.datetime.combine(dt.date.today(),dt.time(hour=12,minute=0))
date_list = [base + dt.timedelta(days=x) for x in range(nDays)]

barData = []
categoryLabels = []
barChart = []
for i in range(nCategory):
    barData = norm.rvs(4,1,nDays).tolist()
    barChart.append(go.Bar(x=date_list,y=barData, xaxis = 'x1',name = str(i), hovertemplate = '<b>%{x|%x}<br>%{y}<br></b>'))

# x-axis with day labels
xaxis1_vals = date_list
xaxis1_text = [d.strftime('%A')[0] for d in date_list]
xaxis1 = go.layout.XAxis(tickvals = xaxis1_vals,ticktext = xaxis1_text, showgrid = False)

# x-axis for the week numbers
dummyScatter1 = go.Scatter(x=date_list,y=[-1]*nDays, xaxis = 'x2',showlegend = False,hoverinfo='skip')

xaxis2_vals = [d for d in date_list if d.isoweekday()==3]
xaxis2 = go.layout.XAxis(matches = "x1",anchor="x1",overlaying="x1",ticklen = 30, 
                  ticks= 'outside',tickcolor = 'rgba(0,0,0,0)', tickformat = '%U',
                  tickvals = xaxis2_vals, showgrid = False)

# x-axis for the dividers between weeks
dummyScatter2 = go.Scatter(x=date_list,y=[-1]*nDays, xaxis = 'x3',showlegend = False,hoverinfo='skip')
xaxis3_vals = [d.date() for d in date_list if d.isoweekday()==7]
xaxis3_text = ['']*len(xaxis3_vals)
xaxis3 = go.layout.XAxis(matches = "x1",anchor="x1",overlaying="x1",ticklen = 50, 
                  ticks= 'outside', tickformat = '%U',

                  tickvals = xaxis3_vals, ticktext = xaxis3_text, showgrid = True, gridwidth=5)

yaxis = go.layout.YAxis(rangemode = 'nonnegative')

layout = go.Layout(xaxis=dict(), barmode = 'stack', xaxis1 = xaxis1, xaxis2 = xaxis2, xaxis3 = xaxis3, yaxis = yaxis, hovermode = 'x')

fig = go.Figure(data=barChart + [dummyScatter1,dummyScatter2],layout=layout)
fig.show()

Hi @MaartenB welcome to the forum! You can use tickformatstops to customize for different zoom levels.

Thanks @Emmanuelle! I considered this but tickformatstops won’t cut it for me, since completely different axes are needed. The code and chart below show more or less what I have mind.

cheers,
Maarten

import datetime as dt, numpy as np, plotly.graph_objs as go
from scipy.stats import norm
from calendar import monthrange


nCategory = 5
nDays = 100

# generate bar data
base = dt.datetime.combine(dt.date.today(),dt.time(hour=12,minute=0))
date_list = [base + dt.timedelta(days=x) for x in range(nDays)]

barData = []
categoryLabels = []
barChart = []
for i in range(nCategory):
    barData = norm.rvs(4,1,nDays).tolist()
    barChart.append(go.Bar(x=date_list,y=barData, xaxis = 'x1',name = str(i), hovertemplate = '<b>%{x|%x}<br>%{y}<br></b>'))

xaxis1_vals = date_list
xaxis1_text = ['' for d in date_list]
xaxis1 = go.layout.XAxis(tickvals = xaxis1_vals,ticktext = xaxis1_text, showgrid = False)

# x-axis for the week numbers
dummyScatter1 = go.Scatter(x=date_list,y=[-1]*nDays, xaxis = 'x2',showlegend = False,hoverinfo='skip')
xaxis2_vals = [d for d in date_list if d.isoweekday()==3]
xaxis2 = go.layout.XAxis(matches = "x1",anchor="x1",overlaying="x1", 
                  tickcolor = 'rgba(0,0,0,0)', tickformat = '%U',
                  tickvals = xaxis2_vals, showgrid = False)

# x-axis for the dividers between weeks
dummyScatter2 = go.Scatter(x=date_list,y=[-1]*nDays, xaxis = 'x3',showlegend = False,hoverinfo='skip')
xaxis3_vals = [d.date() for d in date_list if d.isoweekday()==7]
xaxis3_text = ['']*len(xaxis3_vals)
xaxis3 = go.layout.XAxis(matches = "x1",anchor="x1",overlaying="x1",ticklen = 10, 
                  ticks= 'outside',tickvals = xaxis3_vals, ticktext = xaxis3_text, showgrid = False, gridwidth=5)

# x-axis for the month names
dummyScatter3 = go.Scatter(x=date_list,y=[-1]*nDays, xaxis = 'x4',showlegend = False,hoverinfo='skip')
xaxis4_vals = [] 
for d in date_list:
    if d.day == np.floor(monthrange(d.year,d.month)[1]/2):
        xaxis4_vals.append(d + dt.timedelta(days=np.mod(monthrange(d.year,d.month)[1],2)/2))
xaxis4 = go.layout.XAxis(matches = "x1",anchor="x1",overlaying="x1", 
                  tickcolor = 'rgba(0,0,0,0)', tickformat = '%B',
                  tickvals = xaxis4_vals, ticks= 'outside', ticklen = 30, showgrid = False)

# x-axis for the dividers between months
dummyScatter4 = go.Scatter(x=date_list,y=[-1]*nDays, xaxis = 'x5',showlegend = False,hoverinfo='skip')
xaxis5_vals = [d.date() for d in date_list if d.day==monthrange(d.year,d.month)[1]]
xaxis5_text = ['']*len(xaxis5_vals)
xaxis5 = go.layout.XAxis(matches = "x1",anchor="x1",overlaying="x1",ticklen = 50, 
                  ticks= 'outside', tickvals = xaxis5_vals, ticktext = xaxis5_text, showgrid = True)

yaxis = go.layout.YAxis(rangemode = 'nonnegative')
layout = go.Layout(xaxis=dict(), barmode = 'stack', xaxis1 = xaxis1, xaxis2 = xaxis2, 
                  xaxis3 = xaxis3, xaxis4 = xaxis4, xaxis5 = xaxis5, yaxis = yaxis, hovermode = 'x')
fig = go.Figure(data=barChart + [dummyScatter1,dummyScatter2,dummyScatter3,dummyScatter4],layout=layout)

fig.show()

Hi @MaartenB,

I have been looking around to do something similar to you, although I do not want the dynamic zoom, however, I would like my second x-axis to have groupings that vary.


I haven’t come across a simple solution, I am dynamically creating multiple traces using a CSV file so it makes things a bit more complicated.

import plotly.graph_objects as go
import pandas as pd
import sys
import time
import re
from plotly.subplots import make_subplots

df=pd.read_csv("File.csv")
fig = go.Figure()
traces={}

for k, col in enumerate(df.columns):
 if k > 0:
            traces['trace_' + col]=go.Bar(
                                    x=df['Column1'],
                                    y=df[col],
                                    name=col,
                                    hoverinfo="text",
                                    )
df_plot=list(traces.values())

fig=go.Figure(df_plot)

fig.update_layout(title='Graph Title',
                    xaxis_title="X-axis Title",
                    xaxis_dtick=1,
                    yaxis_title="Total number",
                    yaxis_dtick=1,
                    font=dict(
                        family="Courier New, monospace",
                        size=18,
                        color="#000000"
                )
)
fig.show() 

Any advice would be appreciated!

Thanks in advance.