How to put fraction labels in normalized bar chart in Altair?

Viewed 274
import altair as alt
from vega_datasets import data

# get iris dataset and take 120 observations from it
source = data.iris()[10:130]
# add one more feature to dataset
colors = ['red', 'orange', 'blue', 'red'] * 30
source['colors'] = colors

chart = alt.Chart(source).mark_bar().encode(
    alt.Y('species:N', sort='x'),
    alt.X('count():Q', stack='zero'),
    color=alt.Color('colors', sort=colors), 
    order="colors",
    
)

text = alt.Chart(source).mark_text(dx=-7, dy=3, color='white')\
    .encode(
    alt.Y("species:N", sort='x'),
    alt.X('count():Q', stack="zero"),
    order="colors",
    text=alt.Text('count():Q')
    )

chart + text

Result is bar chart with labels showing count for each 'color' feature

Now I want to normalize this chart and labels should represent fraction of a whole.

import altair as alt
from vega_datasets import data

source = data.iris()[10:130]
colors = ['red', 'orange', 'blue', 'red'] * 30
source['colors'] = colors


chart = alt.Chart(source).mark_bar().encode(
    alt.Y('species:N', sort='x'),
    alt.X('count():Q', stack='normalize'),
    color=alt.Color('colors', sort=colors), 
    order="colors",
    
)

text = alt.Chart(source).mark_text(dx=-7, dy=3, color='white')\
    .encode(
    alt.Y("species:N", sort='x'),
    alt.X('count():Q', stack="normalize"),
    order="colors",
    text=alt.Text('count():Q')
    )

chart + text

Normalized bar chart

So in this example instead of 8, 7, 15 for 1st bar I want to see 0.27, 0.23, 0.5 (rounded to 2 digits after comma). How can I achieve this?

update This is how I managed to progress:

import altair as alt
from vega_datasets import data

source = data.iris()[10:130]
colors = ['red', 'orange', 'blue', 'red'] * 30
sort_order=['blue', 'orange', 'red']
source['colors'] = colors

chart = alt.Chart(source).mark_bar().encode(
    alt.Y('species:N', sort='x'),
    alt.X('count():Q', stack='normalize'),
    color=alt.Color('colors', sort=sort_order), 
    order = 'colors'
)

text = alt.Chart(source).transform_aggregate(count='count()', groupby=['species', 'colors'])\
    .transform_joinaggregate(total='sum(count)', groupby=["species"])\
    .transform_calculate(frac=alt.datum.count / alt.datum.total)\
    .mark_text(align='right', dx=-7, dy=3, color='white')\
    .encode(
    alt.Y("species:N", sort='x'),
    alt.X('count():Q', stack="normalize"),
    text=alt.Text('frac:Q', format='.0%'),
    order = 'colors'
    )

chart + text

enter image description here

But labels are not aligned correctly (I need them to be just at the end of each color, just how in first two pictures). Any ideas how to fix it?

1 Answers

You can use a series of joinaggregate and calculate transforms to compute the values you wish to display. You can use the format text property to adjust the format using a d3-format string, and the align mark property to adjust the alignment. For example:

import altair as alt
from vega_datasets import data

source = data.iris()[10:130]
colors = ['red', 'orange', 'blue', 'red'] * 30
source['colors'] = colors
sort_order=['blue', 'orange', 'red']


chart = alt.Chart(source).mark_bar().encode(
    alt.Y('species:N', sort='x'),
    alt.X('count():Q', stack='normalize'),
    color=alt.Color('colors', sort=sort_order), 
    order="colors",
    
)

text = chart.transform_joinaggregate(
    count='count()',
    groupby=['species', 'colors']
).transform_joinaggregate(
    total='count()',
    groupby=['species']
).transform_calculate(
    fraction='datum.count / datum.total'   
).mark_text(
    dx=-7, dy=3, align='right'
).encode(
    color=alt.ColorValue('white'),
    text=alt.Text('fraction:Q', format='.2')
)

chart + text

enter image description here

(Note: I also made the script a bit more concise by sharing the data and encodings between the two charts)

Related