Ribbon Chart in Plotly

Abhinav Kumar
7 min readApr 11, 2023

--

Introduction

Ribbon Charts are a type of data visualization tool that is used to represent and compare categorical data. They are similar to stacked area charts, but instead of using filled areas, Ribbon Charts use thin, vertical ribbons that are arranged side-by-side to show changes in values over time or across different categories.

Each ribbon in the chart represents a different category, and the height of the ribbon corresponds to the value or percentage of that category for a particular time period or data point. Ribbons are usually colour-coded to help distinguish between categories.

Ribbon Charts are especially useful for comparing the overall trend and distribution of data across multiple categories. They can help identify patterns, trends, and outliers that may not be apparent in other types of charts.

We will implement a ribbon chart in Plotly. In just three simple steps we will get the results.

  • Create Ribbon
  • Create Bar
  • Combine both

Data

An overview of the market share of different electric vehicle brands for each quarter from Q2 2021 to Q3 2022. It includes 5 brands: BYD Auto, Tesla, Wuling, Volkswagen, GAC Motor, and Others. This data is a part of Makeover Monday Week 7.

Coding

First, let's get our data, it’s 6 Rows and 7 Columns table.

text = """brands q2_2021 q3_2021 q4_2021 q1_2022 q2_2022 q3_2022
BYD Auto 0.07 0.11 0.12 0.14 0.16 0.2
Tesla 0.15 0.15 0.14 0.16 0.12 0.13
Wuling 0.07 0.06 0.05 0.06 0.05 0.05
Volkswagen 0.07 0.06 0.05 0.04 0.04 0.04
GAC Motor 0.02 0.02 0.02 0.02 0.03 0.03
Others 0.62 0.60 0.62 0.58 0.6 0.55"""

data = [i.split('\t') for i in text.split("\n")]
df = pd.DataFrame(data[1:], columns=data[0])
to_join = df.iloc[:,1:].astype('float')*100
df = pd.concat([df['brands'],to_join], axis=1)

We have a table where the rows represent different brands and the columns represent different time periods. The values in the table are in percentages, and when you sum the values in each column, the total should add up to 100%.

Step1 — Create Ribbon

We will use a line chart to create the ribbon-like effect. For this, we need these points(see in Fig below), which is basically a bar’s upper and lower point over the period.

A part of the final chart

We first create a table with only the brand's name. Then we loop over all the columns to calculate the corresponding points.

final = pd.DataFrame(df.brands)

for i, col in enumerate(['q2_2021' ,'q3_2021', 'q4_2021', 'q1_2022', 'q2_2022', 'q3_2022']):
ch1 = df.loc[:, ['brands', col]]
ch1.sort_values(col, inplace=True)
ch1[f'y{i}_upper'] = ch1[col].cumsum()
ch1[f'y{i}_lower'] = ch1[f'y{i}_upper'].shift(1)
ch1 = ch1.fillna(0)
ch1[f'y{i}'] = ch1.apply(lambda x: (x[f'y{i}_upper']+x[f'y{i}_lower'])/2, axis=1)
final = final.merge(ch1.iloc[:, [0, 2, 3, 4]], on='brands')

That looks like what we wanted. For a different time, we will get the upper and lower values of the bar. And simply y is the midpoint of the upper and lower so we will put brand annotation over that point.

Now we get the list of the upper, lower and midpoint values for every brand.

def getupperlower(brand):
ch1 = final.query('brands == @brand')
upper_col = [i for i in ch1.columns if 'upper' in i]
lower_col = [i for i in ch1.columns if 'lower' in i]
upper_data = ch1[upper_col].values.tolist()[0]
lower_data = ch1[lower_col].values.tolist()[0]
annotate_place = ch1['y0'].values.tolist()[0]
return upper_data, lower_data, annotate_place

y_upper, y_lower, annt = getupperlower('Tesla')
print(f'y_upper - {y_upper}\ny_lower - {y_lower}\nmid point - {annt}')

Now we have all the data let’s create the line chart.

# speicified different color for different brands.
colors={'BYD Auto':'#72c6e8', 'Tesla':'#E41A37', 'Wuling':'#5c606d', 'Volkswagen':'#12618F', 'GAC Motor':'#d9871b', 'Others':'rgba(0,0,0,0.1)'}
colors2 =[i for i in colors.values()]

# creating x axis value for the line chart
x = (np.arange(6)+1).tolist() ## [1,2,3,4,5,6]
x_rev = x[::-1] ## [6,5,4,3,2,1]

fig = go.Figure()

for i, brand in enumerate(df.brands):

upper_col, lower_col,_ = getupperlower(brand) #not focusing on annotations
y_upper = upper_col
y_lower = lower_col
y_lower = y_lower[::-1] # same thing we did in x axis.
fig.add_trace(go.Scatter(
x=x+x_rev,
y=y_upper+y_lower,
fill='toself',
fillcolor=colors2[i],
opacity=0.4,
line_color='grey',
showlegend=False,
name=brand,
line_shape='spline'
))
At 6 ribbon is colliding

Now we got our ribbon, but there was a problem after 5, ribbons collided with each other. This happens because we put line shape as ‘spline’. To get rid of this problem, let’s extend this line chart to 7 with the same y value. We will then update the x and y value in go.Scatter.


x = x + [x[-1]+1, x[-1]+1] + x_rev ## [1,2,3,4,5,6,7,7,6,5,4,3,2,1]
y = y_upper + [y_upper[-1], y_upper[-1]] + y_lower

Now at 6, we are not getting any collisions. Next, get rid of this 7, we specify a range to show in update_xaxes. We also provide the required label of the x-axis.

fig.update_xaxes(range=[0.85,6.15],tickmode = 'array',showticklabels=True,
ticktext = ['q2_2021', 'q3_2021', 'q4_2021', 'q1_2022', 'q2_2022', 'q3_2022'],
tickvals = [1, 2, 3, 4, 5, 6],fixedrange=True)
Ribbon Ready

Step2 — Create Bar

A simple sorted stack bar chart, well we will not discuss it in deep, but for more information on bar, charts do check out my previous post, Spine plot chart or bar chart. In short, first, we will manipulate the table such that the column contains brands with a single row for each period. Then we will use ploty express to create a bar chart for each period. Next, we need to add the data of each chart to the Plotly Figure object. Finally, we specify the bar mode to stack and set the bar gap to create the ribbon-like effect in the chart.

colors={'BYD Auto':'#72c6e8', 'Tesla':'#E41A37', 'Wuling':'#5c606d', 'Volkswagen':'#12618F', 'GAC Motor':'#d9871b', 'Others':'rgba(0,0,0,0.1)'}

ch1 = df.set_index('brands')

fig = go.Figure()

for i,j in enumerate(['q2_2021', 'q3_2021', 'q4_2021', 'q1_2022', 'q2_2022', 'q3_2022']):

ch2 = pd.DataFrame(ch1.T.iloc[i])
ch2.columns = [i+1]
ch2.sort_values(i+1, ascending=True, inplace=True)
fig_px = px.bar(ch2.T, color_discrete_map=colors, opacity=0.7, text_auto=True)
fig_px.update_traces(hovertemplate='<b>%{x}</b>, %{value}%')
fig_px.update_traces(textfont=dict(size=10,color='black'), textposition='auto', cliponaxis=False, texttemplate='%{value}%')
for trace in fig_px['data']:
fig.add_trace(trace)

fig.update_layout(barmode='stack', bargap=0.7, showlegend=False)
Our Bar Graph

Step3 — Combining Both

Let’s combine the ribbon and bar chart to complete our ribbon chart. We define the figure object, then add the ribbon first with annotation and after that bar chart.


colors={'BYD Auto':'#72c6e8', 'Tesla':'#E41A37', 'Wuling':'#5c606d', 'Volkswagen':'#12618F', 'GAC Motor':'#d9871b', 'Others':'rgba(0,0,0,0.1)'}
colors2 =[i for i in colors.values()]

fig = go.Figure()

# Ribbon
x = (np.arange(df.shape[1]-1)+1).tolist()
x_rev = x[::-1]

annotations = []

for i, brand in enumerate(df.brands):

upper_col, lower_col, annotate_place = getupperlower(brand)
y_upper = upper_col
y_lower = lower_col
y_lower = y_lower[::-1]
fig.add_trace(go.Scatter(
x=x+[x[-1]+1, x[-1]+1]+x_rev,
y=y_upper+[y_upper[-1], y_lower[0]]+y_lower,
fill='toself',
fillcolor=colors2[i],
opacity=0.5,
line_color='rgba(0,0,0,0.2)',
showlegend=False,
name=brand,
line_shape='spline',
mode='lines',#+markers
marker=dict(size=20),
hovertemplate=' '
))
annotations.append(dict(xref='paper', yref='y',
x=-0.005, y=annotate_place,
text=brand,align="right",xanchor='right',
font=dict(family='Arial', size=14,
color=colors2[i]),
showarrow=False))


# bar chart
ch1 = df.set_index('brands')
for i,j in enumerate(['q2_2021', 'q3_2021', 'q4_2021', 'q1_2022', 'q2_2022', 'q3_2022']):

ch2 = pd.DataFrame(ch1.T.iloc[i])
ch2.columns = [i+1]
ch2.sort_values(i+1, ascending=True, inplace=True)
fig_px = px.bar(ch2.T, color_discrete_map=colors, opacity=0.7, text_auto=True)
fig_px.update_traces(hovertemplate='<b>%{x}</b>, %{value}%')
fig_px.update_traces(textfont=dict(size=10,color='black'), textposition='auto', cliponaxis=False, texttemplate='%{value}%')
for trace in fig_px['data']:
fig.add_trace(trace)



fig.update_layout(barmode='stack', bargap=0.7,showlegend=False)

fig.update_layout(plot_bgcolor='#f2f3f4', paper_bgcolor='#f2f3f4', margin=dict(l=100,b=10, r=100))
fig.update_xaxes(range=[0.85,6.15],tickmode = 'array',showticklabels=True,
ticktext = ['q2_2021', 'q3_2021', 'q4_2021', 'q1_2022', 'q2_2022', 'q3_2022'],
tickvals = [1, 2, 3, 4, 5, 6],fixedrange=True)
fig.update_yaxes(range=[0,101],showticklabels=False, showgrid=False, fixedrange=True)
fig.update_layout(annotations=annotations)
fig.update_layout(title='Global Passenger Electric Vehicle Market Share, Q2 2021 - Q3 2022')
Tesla Ribbon Overlapping with Others

Well congrats, we get our ribbon chart. But if you look closely you will see some overlapping. For example tesla in q2_2022. Let’s set go.Scatter mode to lines + markers to see what’s going on.

Well, we need something like the graph(refer below). Four points at every corner of the bar. Let’s create a function that will do that.

def get_val(x,sub=0.1, axis='x'):
if axis != 'x':
a = [[i, i]for i in x]
else:
a = [[i-sub, i+sub ]for i in x]
return list(itertools.chain.from_iterable(a))

x = (np.arange(6)+1).tolist()
y_upper, y_lower, annt = getupperlower('Tesla')
print(f'y_upper - {get_val(y_upper, axis="y")}\ny_lower - {get_val(y_lower, axis="y")}\nx - {get_val(x, sub=0.15)}')

Now update these y_upper, y_lower and x in the above plot.

Cool, now change the mode back to lines only.

Ribbon Chart

Conclusion

Ribbon Charts can be a powerful tool for visualizing and analyzing categorical data and can help provide insights and inform decisions in various industries and applications.

The code can be found in a Kaggle link.

If you enjoyed this chart, then be sure to check out some of my previous creations, like my awesome bar chart, Waffle chart and spine chart. Trust me, you won’t be disappointed!

I hope you’ve found this guide informative and helpful in creating visually appealing data visualizations with Plotly.

Hit that follow button and let’s embark on a journey to data visualization bliss. I promise it’ll be more fun than watching the paint dry. And who knows, you might just learn a thing or two along the way. Let’s do this, amigos!

--

--

Abhinav Kumar

Data enthusiast deeply passionate about power of data. Sifting through datasets and bringing insights to life through visualization is my idea of a good time.