I've been playing with Altair charts in the context of Streamlit for making simple calculators. In one case I wanted to plot 3 bar charts next to each other to make an explainer for how progressive tax brackets work.
In one column is the total income, in the next is a bar drawn between the start of the bracket and the point at which the rate is paid for that bracket. Finally I stack all those intervals together to give the total tax owed.
I can't figure a good way to help the "Total Owed" number not have visual conflict with the grid lines. This is partly because I can't figure a good way to relate the chart's rendered size to the font size to do conditional checks for overlapping. I also believe there is no way to control the rendering of gridlines on a per-column basis.
Ideally I want to keep the gridlines thick and visible without disturbing the text. I could place the text for that column inside the stacked bars, but for certain configurations the multicolored bars make the text just as unreadable.
Here's a MRE:
import altair as alt
import pandas as pd
import streamlit as st
# Example data
data = pd.DataFrame({
'category': ['A', 'B', 'C', 'D'],
'value': [10, 20, 15, 25]
})
# Base chart with gridlines
chart = alt.Chart(data).mark_bar().encode(
x='value:Q',
y=alt.Y('category:N', axis=alt.Axis(grid=True, gridWidth=2, gridColor='darkslategray'))
)
text = alt.Chart(data).mark_text(
align='left',
baseline='middle',
color='black'
).encode(
x='value:Q',
y='category:N',
text=alt.Text('value:Q')
)
# Combine the charts
final_chart = chart + text
st.altair_chart(final_chart)
I tried using a mark_rect
as a sort of backdrop for the text by adding this snippet to the above:
# Text with background
text_background = alt.Chart(data).mark_rect(
color='black',
opacity=0.7,
height=20,
width=20
).encode(
x='value:Q',
y='category:N'
)
and updating the composition of the final chart. However those rectangles are centered on the end of the bar and (again) with no way to relate the chart's size and the text font size I don't see a straightforward way to center it on the text (or even set an appropriate width to cover the whole text in response to its width).
Is there a convenient way to do this? Failing that, is there some other chart library I could use?
I think your idea with a background rectangle would work. You can align it like you did with the text, and set the width to depend on the number of digits in the label:
# Example data
data = pd.DataFrame({
'category': ['A', 'B', 'C', 'D', 'E'],
'value': [5, 20, 150, 250, 3000]
})
# Base chart with gridlines
base = alt.Chart(data).mark_bar().encode(
x=alt.X('value:Q').scale(domainMax=3500),
y=alt.Y('category:N').axis(grid=True, gridWidth=2, gridColor='darkslategray')
)
text = base.mark_text(
align='left',
dx=2,
color='black'
).encode(
text=alt.Text('value:Q')
)
text_bg = base.mark_rect(
color='white',
height=20,
# width=alt.expr('10 + 5 * length(toString(datum.value))'), # Equivalent
width=alt.expr(10 + 5 * alt.expr.length(alt.expr.toString((alt.datum.value)))),
align='left'
)
base + text_bg + text
If you want to experiment with having the text overlaid on bars with different color instead, you could set the text color conditionally on the luminance of the bar bg color as per https://github.com/vega/altair/pull/3614