I have this chart with mark_text
to denote the value of each bar. There is a slider to go back/forth in time.
I would like to change the positioning of the text (the numbers in the bars) depending on the Y
value: if it is larger than a certain value, it will go just below the top of the bar (baseline='line-top
), otherwise just above (baseline='line-bottom'
).
This way, as I slide through time the text will appear below/above the top of the bar depending on Y
.
I think I have to use some condition or expression, but I cannot figure out how.
import numpy as np
import pandas as pd
import altair as alt
np.random.seed(0)
################################################################################
model_keys = ['M1', 'M2']
scene_keys = ['S1', 'S2']
layer_keys = ['L1', 'L2']
time_keys = [1, 2, 3]
ys = []
models = []
dataset = []
layers = []
scenes = []
times = []
for sc in scene_keys:
for m in model_keys:
for l in layer_keys:
for s in range(10):
y = np.random.rand(10) / 10
if m == 'M1':
y *= 10
if l == 'L1':
y *= 5
for t in time_keys:
y += 1
ys += list(y)
scenes += [sc] * len(y)
models += [m] * len(y)
layers += [l] * len(y)
times += [t] * len(y)
# ------------------------------------------------------------------------------
df = pd.DataFrame({'Y': ys,
'Model': models,
'Layer': layers,
'Scenes': scenes,
'Time': times})
bars = alt.Chart(df, width=100, height=90).mark_bar().encode(
x=alt.X('Scenes:N',
title=None,
axis=alt.Axis(
grid=False,
title=None,
labels=False,
),
),
y=alt.Y('Y:Q',
aggregate='mean',
axis=alt.Axis(
grid=True,
title='Y',
titleFontWeight='normal',
),
),
)
text = alt.Chart(df).mark_text(align='center',
baseline='line-top',
color='black',
dy=5,
fontSize=13,
).encode(
x=alt.X('Scenes:N'),
y=alt.Y('mean(Y):Q'),
text=alt.Text('mean(Y):Q',
format='.1f'
),
)
bars = bars + text
bars = bars.facet(
row=alt.Row('Model:N',
title=None,
),
column=alt.Column('Layer:N',
title=None,
),
spacing={"row": 10, "column": 10},
)
slider = alt.binding_range(
min=1,
max=3,
step=1,
name='Time',
)
selector = alt.selection_single(
name='Selector',
fields=['Time'],
bind=slider,
init={'Time': 3},
)
bars = bars.add_selection(
selector
).transform_filter(
selector
)
bars.save('test.html')
Unfortunately, you can only use conditions with encodings, not (yet) with mark properties, so there is no way to dynamically change the text baseline. However, you could add a second text mark that is above the bars and then set the opacity encoding of both text marks to depend on the value of y, so that it is either the text above or below the bar that is visible.
I believe you need to do the calculation of the average y value in a separate transform, so that you can access the column name in the conditional expression. I also believe that the charts need to build on each other to filter correctly when the Time value changes, but I am not 100% on either of these two points.
Taking the above into consideration, something like this would work:
bars = alt.Chart(df, width=100, height=90).mark_bar().transform_aggregate(
y_mean = 'mean(Y)',
groupby=['Scenes']
).encode(
x=alt.X(
'Scenes:N',
title=None,
axis=alt.Axis(
grid=False,
title=None,
labels=False,
),
),
y=alt.Y(
'y_mean:Q',
axis=alt.Axis(
grid=True,
title='Y',
titleFontWeight='normal',
),
),
)
text = bars.mark_text(
align='center',
baseline='line-top',
color='black',
dy=5,
fontSize=13,
).encode(
text=alt.Text( 'y_mean:Q', format='.1f'),
opacity=alt.condition( 'datum.y_mean < 3', alt.value(1), alt.value(0))
)
text2 = text.mark_text(
align='center',
baseline='line-bottom',
fontSize=13,
).encode(
opacity=alt.condition( 'datum.y_mean >= 3', alt.value(1), alt.value(0))
)
bars = bars + text + text2
bars = bars.facet(
row=alt.Row('Model:N', title=None),
column=alt.Column('Layer:N', title=None),
spacing={"row": 10, "column": 10},
)
slider = alt.binding_range(
min=1,
max=3,
step=1,
name='Time',
)
selector = alt.selection_single(
name='Selector',
fields=['Time'],
bind=slider,
init={'Time': 2},
)
bars = bars.add_selection(
selector
).transform_filter(
selector
)
bars