pythonchartsstreamlitaltair

Altair chart mark_text background color/visual clarity


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.

Here's an image example of what I mean

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).

Failed example image explained by the above paragraph

Is there a convenient way to do this? Failing that, is there some other chart library I could use?


Solution

  • In addition to Joel's suggestion of using a background box, I have also had good results adding a copy of the text with a stroke behind the main text. This automatically adjusts the background to the right size.

    Chart with stroked background

    import altair as alt
    import pandas as pd
    
    # 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',
        dx=3  # Nudge the text to the right
    ).encode(
        x='value:Q',
        y='category:N',
        text=alt.Text('value:Q')
    )
    
    text_background = text.mark_text(
        align='left',
        baseline='middle',
        stroke='white',
        strokeWidth=5,
        strokeJoin='round',
        dx=3
    )
    
    # Combine the charts
    final_chart = chart + text_background + text
    
    final_chart