altair
altair copied to clipboard
Is it possible to use alt.condition when passing options to text encodings?
Here's the chart I want to make. You can see that it has text placed above positive bars and below negative bars.
Here's the data I used.
import pandas as pd
import altair as alt
last_13 = pd.read_json("https://raw.githubusercontent.com/datadesk/cpi/master/notebooks/last_13.json", dtype={"date_label": pd.np.datetime64})
Here's the base chart I wrote.
base = alt.Chart(
last_13,
title="One-month percent change in CPI for All Urban Consumers (CPI-U), seasonally adjusted"
).properties(width=700)
bars = base.mark_bar().encode(
x=alt.X(
"date:O",
timeUnit="yearmonth",
axis=alt.Axis(title=None, labelAngle=0, format="%b %y"),
),
y=alt.Y(
"pct_change_rounded:Q",
axis=alt.Axis(title=None),
scale=alt.Scale(domain=[
last_13['pct_change'].min()-0.1,
last_13['pct_change'].max()+0.05
])
)
)
And here's how I handled the text.
text = base.encode(
x=alt.X("date:O", timeUnit="yearmonth"),
y="pct_change_rounded:Q",
text='pct_change_rounded'
)
textAbove = text.transform_filter(alt.datum.pct_change > 0).mark_text(
align='center',
baseline='middle',
fontSize=14,
dy=-10
)
textBelow = text.transform_filter(alt.datum.pct_change < 0).mark_text(
align='center',
baseline='middle',
fontSize=14,
dy=12
)
bars + textAbove + textBelow
It all works fine. But why can't I do this?
text = base.encode(
x=alt.X("date:O", timeUnit="yearmonth"),
y="pct_change_rounded:Q",
text='pct_change_rounded',
align='center',
baseline='middle',
fontSize=14,
dy=alt.condition(alt.datum.pct_change_rounded >= 0, 12, -10)
)
bars + text
I get this error:
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
<ipython-input-15-ab79ad07b15f> in <module>()
27 baseline='middle',
28 fontSize=14,
---> 29 dy=alt.condition(alt.datum.pct_change_rounded >= 0, 12, -10)
30 )
31
/home/palewire/.virtualenvs/cpi/src/altair/altair/vegalite/v2/api.py in condition(predicate, if_true, if_false, **kwargs)
333 if_true = {'shorthand': if_true}
334 if_true.update(kwargs)
--> 335 condition.update(if_true)
336
337 if isinstance(if_false, core.SchemaBase):
TypeError: 'int' object is not iterable
conditions are only supported for encodings, unfortunately.
FYI: for the power user, often the best way to answer "is this possible" questions is to look at the vega-lite schema. For example, here it's clear that dy
can only be a number, not any sort of conditional expression:
https://github.com/altair-viz/altair/blob/28378bd52efbf4eae5275b6fb598e74b51bf5f4c/altair/vegalite/v2/schema/vega-lite-schema.json#L4700-L4703
Thanks for the tip. Too bad Vega doesn't support this. In a perfect world such a feature would be nice.
Short of that, is the pattern I developed above the one you would recommend. If so, perhaps it's worth enshrining it in an example?
FYI, we do plan to support xOffset/yOffset as encoding channels in the future, but doing so require some design in terms of how the x/yOffset channels interact with x/y channels, so that's why it doesn't happen yet.
Also, maybe reading the type descriptions in Vega-Lite docs is less painful than reading the JSON schema, which is more like a format for machine to read IMHO.
Are there workarounds with conditional formatting mark_text? I was assuming I could create a separate feature that just displayed the information I wanted to have displayed, but I can't quite get it to appear in the place that I want.
Dummy data and my latest graph code below.
import pandas as pd
import altair as alt
fig1_example = pd.DataFrame({
'ami': ['0-50% AMI','50-80% AMI',
'80-120% AMI','120% AMI +', '0-50% AMI', '50-80% AMI',
'80-120% AMI', '120% AMI +'],
'variable': ['Permits','Permits','Permits','Permits',
'Target','Target','Target','Target'],
'value': [1127, 37, 12998, 14862,
1690, 1997, 10759, 9803],
'order':[1, 2, 3, 4, 1, 2, 3, 4],
'label':['1127', '37', '12998', '14862', '', '', '', '']})
bars = alt.Chart(fig1_example).mark_bar(stroke="#000000").encode(
alt.X('ami:O',title=" ",axis=alt.Axis( labelAngle=360),
sort=['0-50% AMI', '50-80% AMI',
'80-120% AMI', '120% AMI +']), alt.Y('value:Q',title=""),
color=alt.Color('variable:O',title="",
scale=alt.Scale(domain=['Permits', 'Target'],
range=["#FFB81D", "#FFFFFF"])), order=alt.Order(
'order', sort= 'ascending'))
text = alt.Chart(fig1_example).mark_text(fontSize = 14,
color='black', dy=-10).encode(
x=alt.X('ami:O',sort=['0-50% AMI', '50-80% AMI',
'80-120% AMI', '120% AMI +']),
y=alt.Y('value:Q'),
detail='label:O',
text=alt.Text('label:O'))
layer = alt.layer(bars, text).configure_legend(symbolStrokeWidth=1,
orient="bottom", direction="horizontal",
labelLimit= 0, titleLimit=0, titleFontSize=18,
labelFontSize=20, labelFont="Georgia",
titleFont="Georgia").configure_axis(domain=False,
labelFont='Georgia', titleFont='Georgia',
titleFontWeight="normal", labelFontSize=15,
titleFontSize=15).properties(width=400)
layer
The current output of the above code looks like this
I want to have the right two bar charts text above the bars, like in this chart
Any advice on how to implement this would be really appreciated, been struggling with it.
Once the offset encoding is available, this will be easier. I think that should be in Altair once #2528 is merged and released.
@qunderriner A workaround for this issue is to create text labels in all the positions where you want them for all the bars and then encode their opacity with a condition as in this SO answer. Another workaround would be to create a custom dataframe with the y-values in the right positions for where you want the text to be for each bar.
Btw, I don't think the offset functionality is part of a Vega-Lite release yet, so it might only come to Altair after #2528
@joelostblom That worked perfectly - thanks so much!
@palewire @qunderriner This is now possible to do via expressions https://altair-viz.github.io/user_guide/interactions.html#expressions. Using the same setup as above:
text = base.encode(
x=alt.X("date:O", timeUnit="yearmonth"),
y="pct_change_rounded:Q",
text='pct_change_rounded',
).mark_text(
fontSize=14,
dy=alt.expr(alt.expr.if_(alt.datum.pct_change_rounded >= 0, -10, 10))
)
bars + text
I think it would be helpful to have an example of this in the docs, maybe under the text mark label section https://altair-viz.github.io/user_guide/marks/text.html#labels? Happy to review a PR if someone wants to take that on.
Another approach would be to use the yOffset channel like this yOffset=alt.datum(alt.expr(alt.expr.if_(alt.datum.pct_change_rounded >= 0, -10, 10)))
but then the offset is in axis units rather than pixels so that it often less desirable. I don't think condition
will work as it requires one of the outcomes to be a field encoding and not a fixed value