Consider the following setup. We have set of distances. For each distance we generate several samples of randomly distributed points. For fixed distance samples are indexed by integer values k. So, for example, at distance 600 we have sample #1, sample #2, sample #3, … (each sample is a collection of points). And at distance 800 we have sample #1, sample #2, sample #3, … (but of course they are different from the samples at distance 600). To visualize them I use plotly.graph_objects.Box with boxmode=‘group’ which works perfectly. Now to the hindrance. I would like to draw bars that would form something like “acceptable deviation intervals”. Heights of bars don’t depend on distance, but they depend on k. And I decided to use plotly.graph_objects.Bar do draw bars in alignment with boxes (that were previously plotted based on samples). To align bars and boxes I tried to tinker with offsetgroup, but the resulting alignment is far from perfect (especially when I turn off some elements on the figure). The question is: what did I do wrong and what should I do to attain good-looking alignment (without adjusting offsets and widthes manually)? By good alignment I mean that corresponding box and bar should have the same width and be simmetric with respect to the same vertical line.
import itertools
import numpy as np
import plotly
xs = list(range(400, 1000, 100))
ks = list(range(1, 7))
BASE_DIST = 120
median_ok_interval = (-1.5, 1.5)
mean_range = (0.001, 0.5)
scale_range = (0.5, 2)
sample_size_range = (100, 1000)
DEBUG_SEED = 23938
def get_random_generator(seed=None):
return np.random.Generator(np.random.PCG64(seed))
def plot_boxplot_0(fig):
'''
Plot boxplot using boxmode='group' (one trace per group by k), no manual widthes, offsets and colors
'''
gen = get_random_generator(DEBUG_SEED)
for k in ks:
k_dist = BASE_DIST * k
if k > 3:
effective_xs = xs[3:]
else:
effective_xs = xs
k_xs = []
k_ys = []
for x in effective_xs:
sample_size = gen.integers(sample_size_range[0], sample_size_range[1], 1)[0]
sample_mean = gen.uniform(mean_range[0], mean_range[1], 1)[0]
sample_std = gen.uniform(scale_range[0], scale_range[1], 1)[0]
kx_xs = np.array([x] * sample_size)
kx_ys = gen.normal(loc=sample_mean, scale=sample_std, size=sample_size)
k_xs.extend(kx_xs)
k_ys.extend(kx_ys)
fig.add_trace(plotly.graph_objects.Box(
x=k_xs,
y=k_ys,
name=f"k={k}",
offsetgroup=f"offset_k={k}",
legendgroup=f"legend_k={k}",
))
fig.update_layout(boxmode='group')
def plot_error_limits_1(fig):
'''
Plot error limits (without data distribution) using bar chart with barmode='group' (no manual width or offsets).
We add bar for entire k-group
'''
for k in ks:
k_dist = BASE_DIST * k
if k > 3:
effecitve_xs = xs[3:]
else:
effecitve_xs = xs
median_upper_error_limit = median_ok_interval[1] + 0.3 * k_dist * 1e-3
median_upper_error_limits = [median_upper_error_limit] * len(effecitve_xs)
fig.add_trace(plotly.graph_objects.Bar(
x=effecitve_xs,
y=median_upper_error_limits,
offsetgroup=f"offset_k={k}",
legendgroup=f'legend_k={k}',
hovertemplate=f"{k_dist}: err_limit={median_upper_error_limit:.4}<extra></extra>",
showlegend=False
))
median_lower_error_limit = median_ok_interval[0] - 0.3 * k_dist * 1e-3
median_lower_error_limits = [median_lower_error_limit] * len(effecitve_xs)
fig.add_trace(plotly.graph_objects.Bar(
x=effecitve_xs,
y=median_lower_error_limits,
offsetgroup=f"offset_k={k}",
legendgroup=f'legend_k={k}',
hovertemplate=f"{k_dist}: err_limit={median_lower_error_limit:.4}<extra></extra>",
showlegend=False
))
fig.update_traces(marker_color='rgb(158,202,225)', opacity=0.6, selector=dict(type='bar'))
fig.update_layout(barmode='group')
def plot_combined():
fig = plotly.graph_objects.Figure()
plot_boxplot_0(fig)
plot_error_limits_1(fig)
fig.show()
plot_combined()