Skip to content

2022

"Bar chart made in Altair with Financial Times style"

"#30DayChartChallenge #Day24 Themeday: Financial times"

  • image: images/barchart_FT_style_altair.png
import pandas as pd
import altair as alt

The #30DayChartChallenge Day 24 calls for Financial Times themed charts. The bar chart that I will try to reproduce in Altair was published in the article: "Financial warfare: will there be a backlash against the dollar?"

This is the graph (without FT background) to we want to reproduce:

I digitized the heights of yhe bars with WebplotDigitizer:

data = """Bar0, 3.23
Bar1, 1.27
Bar2, 1.02
Bar3, 0.570
Bar4, 0.553
Bar5, 0.497
Bar6, 0.467
Bar7, 0.440
Bar8, 0.420
Bar9, 0.413
Bar10, 0.317
Bar11, 0.0433"""

data_values = [float(x.split()[1]) for x in data.splitlines()]

I put the values into a Pandas dataframe:

source = pd.DataFrame({
    'label': ['China', 'Japan', 'Switserland', 'India', 'Taiwan', 'Hong Kong', 'Russia', 'South Korea', 'Saudi Arabia', 'Singapore', 'Eurozone', 'US'],
    'val': data_values
})

Now we build the graph and alter it's style to resemble the Financial Times style:

square = alt.Chart().mark_rect(width=80, height=5, color='black', xOffset=-112, yOffset=10)

bars = alt.Chart(source).mark_bar(color='#174C7F', size=30).encode(
    x=alt.X('val:Q', title='', axis=alt.Axis(tickCount=6, domain=False, labelColor='darkgray'), scale=alt.Scale(domain=[0, 3.0])),
    y=alt.Y('label:N', title='', sort=alt.EncodingSortField(
            field="val:Q",  # The field to use for the sort
            op="sum",  # The operation to run on the field prior to sorting
            order="ascending"  # The order to sort in
        ), axis=alt.Axis(domainColor='lightgray',
                         labelFontSize=18, labelColor='darkgray', labelPadding=5,
                         labelFontStyle='Bold',
                         tickSize=18, tickColor='lightgray'))
).properties(title={
      "text": ["The biggest holders of FX reserves", ], 
      "subtitle": ["Official foreign exchange reserve (Jan 2022, $tn)"],
      "align": 'left',
      "anchor": 'start'
    },
    width=700,
    height=512
)

source_text = alt.Chart(
    {"values": [{"text": "Source: IMF, © FT"}]}
).mark_text(size=12, align='left', dx=-140, color='darkgrey').encode(
    text="text:N"
)

# from https://stackoverflow.com/questions/57244390/has-anyone-figured-out-a-workaround-to-add-a-subtitle-to-an-altair-generated-cha
chart = alt.vconcat(
    square,
    bars,
    source_text
).configure_concat(
    spacing=0
).configure(
    background='#fff1e5',
).configure_view(
    stroke=None, # Remove box around graph
).configure_title(
    # font='metricweb',
    fontSize=22,
    fontWeight=400,
    subtitleFontSize=18,
    subtitleColor='darkgray',
    subtitleFontWeight=400,
    subtitlePadding=15,
    offset=80,
    dy=40
)

chart

Trying to use the offical Financial Times fonts

The chart looks quit similar to the original. Biggest difference is the typography. The Financial times uses its own Metric Web and Financier Display Web fonts and Altair can only use fonts available in the browser.

The fonts could be made available via CSS:

@font-face {
    font-family: 'metricweb';
    src: url('https://www.ft.com/__origami/service/build/v2/files/o-fonts-assets@1.5.0/MetricWeb-Regular.woff2''
);
}
from IPython.display import HTML
from google.colab.output import _publish as publish
publish.css("""@font-face {
    font-family: 'metricweb', sans-serif;
    src: url('https://www.ft.com/__origami/service/build/v2/files/o-fonts-assets@1.5.0/MetricWeb-Regular.woff2') format('woff2');
}""")
square = alt.Chart().mark_rect(width=80, height=5, color='black', xOffset=-112, yOffset=10)

bars = alt.Chart(source).mark_bar(color='#174C7F', size=30).encode(
    x=alt.X('val:Q', title='', axis=alt.Axis(tickCount=6, domain=False), scale=alt.Scale(domain=[0, 3.0])),
    y=alt.Y('label:N', title='', sort=alt.EncodingSortField(
            field="val:Q",  # The field to use for the sort
            op="sum",  # The operation to run on the field prior to sorting
            order="ascending"  # The order to sort in
        ), axis=alt.Axis(domainColor='lightgray',
                         labelFontSize=18, labelColor='darkgray', labelPadding=5,
                         labelFontStyle='Bold',
                         tickSize=18, tickColor='lightgray'))
).properties(title={
      "text": ["The biggest holders of FX reserves", ], 
      "subtitle": ["Official foreign exchange reserve (Jan 2022, $tn)"],
      "align": 'left',
      "anchor": 'start'
    },
    width=700,
    height=512
)

source_text = alt.Chart(
    {"values": [{"text": "Source: IMF, © FT"}]}
).mark_text(size=12, align='left', dx=-140, color='darkgrey').encode(
    text="text:N"
)

# from https://stackoverflow.com/questions/57244390/has-anyone-figured-out-a-workaround-to-add-a-subtitle-to-an-altair-generated-cha
chart = alt.vconcat(
    square,
    bars,
    source_text
).configure_concat(
    spacing=0
).configure(
    background='#fff1e5',
).configure_view(
    stroke=None, # Remove box around graph
).configure_title(
    font='metricweb',
    fontSize=22,
    fontWeight=400,
    subtitleFont='metricweb',
    subtitleFontSize=18,
    subtitleColor='darkgray',
    subtitleFontWeight=400,
    subtitlePadding=15,
    offset=80,
    dy=40
)

chart

For the moment the font does not look at all to be Metric web :-(

A second minor difference are the alignment of the 0.0 and 3.0 labels of the x-axis. In the orginal, those labels are centered. Altair aligns 0.0 to the left and 3.0 to the right.


"Reconstructing Economist graph with Altair"

"#30DayChartChallenge #altair #day12"

  • image: images/Economist_stye%3B_30dayschartchallenge_day12.png

In an Economist article "The metamorphosis: How Jeremy Corbyn took control of Labour", the following graph appeared:

Later, Sarah Leo, data visualiser at The Economist, improved the graph to:

The rationale behind this improvement is discussed in her article: 'Mistakes, we made a few'.

In this article, I show how visualisation library Altair can be used to reconstruct the improved graph.

import numpy as np
import pandas as pd
import altair as alt

Read the data for the graph into a Pandas dataframe:

df = pd.read_csv('http://infographics.economist.com/databank/Economist_corbyn.csv').dropna()

This is how the data looks:

df
Page Average number of likes per Facebook post 2016
0 Jeremy Corbyn 5210.0
1 Labour Party 845.0
2 Momentum 229.0
3 Owen Smith 127.0
4 Andy Burnham 105.0
5 Saving Labour 56.0

A standard bar graph in Altair gives this:

alt.Chart(df).mark_bar().encode(
    x='Average number of likes per Facebook post 2016:Q',
    y='Page:O'
)

The message of the graph is that Jerermy Corbyn has by far the most likes per Facebook post in 2016. There are a number of improvements possible:

The number on the x-axis are multiple of thousands. In spirit of removing as much inkt as possible, let's rescale the x-asis with factor 1000. The label 'Page' on the y-axis is superfluous. Let's remove it.

df['page1k'] = df['Average number of likes per Facebook post 2016']/1000.0

After scaling the graphs looks like this:

alt.Chart(df).mark_bar().encode(
    x=alt.X('page1k', title='Average number of likes per Facebook post 2016'),
    y=alt.Y('Page:O', title='')
)

A third improvement is to sort the bars from high to low. This supports the message, Jeremy Corbyn has the most clicks.

alt.Chart(df).mark_bar().encode(
    x=alt.X('page1k:Q', title='Average number of likes per Facebook post 2016'),
    y=alt.Y('Page:O', title='', sort=alt.EncodingSortField(
            field="Average number of likes per Facebook post 2016:Q",  # The field to use for the sort
            op="sum",  # The operation to run on the field prior to sorting
            order="ascending"  # The order to sort in
        ))
)

Now, we see that we have to many ticks on the x-axis. We can add a scale and map the x-axis to integers to cope with that. While adding markup for the x-axis, we add orient='top'. That move the xlabel text to the top of the graph.

alt.Chart(df).mark_bar().encode(
    x=alt.X('page1k:Q', title='Average number of likes per Facebook post 2016',
            axis=alt.Axis(title='Average number of likes per Facebook post 2016', orient="top", format='d', values=[1,2,3,4,5,6]),
            scale=alt.Scale(round=True, domain=[0,6])),
    y=alt.Y('Page:O', title='', sort=alt.EncodingSortField(
            field="Average number of likes per Facebook post 2016:Q",  # The field to use for the sort
            op="sum",  # The operation to run on the field prior to sorting
            order="ascending"  # The order to sort in
        ))
)

Now, we want to remove the x-axis itself as it adds nothing extra. We do that by putting the stroke at None in the configure_view. We also adjust the x-axis title to make clear the numbers are multiples of thousands.

alt.Chart(df).mark_bar().encode(
    x=alt.X('page1k:Q', title="Average number of likes per Facebook post 2016  ('000)",
            axis=alt.Axis(title='Average number of likes per Facebook post 2016', orient="top", format='d', values=[1,2,3,4,5,6]),
            scale=alt.Scale(round=True, domain=[0,6])),
    y=alt.Y('Page:O', title='', sort=alt.EncodingSortField(
            field="Average number of likes per Facebook post 2016:Q",  # The field to use for the sort
            op="sum",  # The operation to run on the field prior to sorting
            order="ascending"  # The order to sort in
        ))
).configure_view(
    stroke=None, # Remove box around graph
)

Next we try to left align the y-axis labels:

alt.Chart(df).mark_bar().encode(
    x=alt.X('page1k:Q',
            axis=alt.Axis(title="Average number of likes per Facebook post 2016  ('000)", orient="top", format='d', values=[1,2,3,4,5,6]),
            scale=alt.Scale(round=True, domain=[0,6])),
    y=alt.Y('Page:O', title='', sort=alt.EncodingSortField(
            field="Average number of likes per Facebook post 2016:Q",  # The field to use for the sort
            op="sum",  # The operation to run on the field prior to sorting
            order="ascending"  # The order to sort in
        ))
).configure_view(
    stroke=None, # Remove box around graph
).configure_axisY(
    labelPadding=70, 
    labelAlign='left'
)

Now, we apply the Economist style:

square = alt.Chart().mark_rect(width=50, height=18, color='#EB111A', xOffset=-105, yOffset=10)

bars = alt.Chart(df).mark_bar().encode(
    x=alt.X('page1k:Q',
            axis=alt.Axis(title="", orient="top", format='d', values=[1,2,3,4,5,6], labelFontSize=14),
            scale=alt.Scale(round=True, domain=[0,6])),
    y=alt.Y('Page:O', title='', sort=alt.EncodingSortField(
            field="Average number of likes per Facebook post 2016:Q",  # The field to use for the sort
            op="sum",  # The operation to run on the field prior to sorting
            order="ascending"  # The order to sort in
        ),
        # Based on https://stackoverflow.com/questions/66684882/color-some-x-labels-in-altair-plot
        axis=alt.Axis(labelFontSize=14, labelFontStyle=alt.condition('datum.value == "Jeremy Corbyn"', alt.value('bold'), alt.value('italic'))))
).properties(title={
      "text": ["Left Click", ], 
      "subtitle": ["Average number of likes per Facebook post\n", "2016, '000"],
      "align": 'left',
      "anchor": 'start'
    }
)

source = alt.Chart(
    {"values": [{"text": "Source: Facebook"}]}
).mark_text(size=12, align='left', dx=-120, color='darkgrey').encode(
    text="text:N"
)

# from https://stackoverflow.com/questions/57244390/has-anyone-figured-out-a-workaround-to-add-a-subtitle-to-an-altair-generated-cha
chart = alt.vconcat(
    square,
    bars,
    source
).configure_concat(
    spacing=0
).configure(
    background='#D9E9F0'
).configure_view(
    stroke=None, # Remove box around graph
).configure_axisY(
    labelPadding=110,
    labelAlign='left',
    ticks=False,
    grid=False
).configure_title(
    fontSize=22,
    subtitleFontSize=18,
    offset=30,
    dy=30
)

chart

The only thing, I could not reproduce with Altair is the light bar around the the first label and bar. For those final touches I think it's better to export the graph and add those finishing touches with a tool such as Inkscape or Illustrator.