DataLens

Customizing layout (titles, legends, margins)

Control titles and subtitles, legend placement, size and margins, unified hover, and annotate the point of the chart with lines and notes.

Level: Intermediate · 8 min read

Practise this module

The questions for this lesson run in your browser and check your answer straight away.

The idea

Express gets the marks right. The layout (everything around the marks) decides whether the chart reads like a draft or a finished report: a title that states the finding, a legend that doesn't steal space, margins that fit a dashboard tile, and a note pointing at the spike everyone will ask about.

All of it goes through one method, fig.update_layout(...), plus a few helpers for lines and annotations.

import pandas as pd
import plotly.express as px

sales = pd.read_csv("sales.csv")
completed = sales[sales["status"] == "completed"]
monthly = (completed.assign(month=completed["order_date"].str[:7])
           .groupby(["month", "channel"], as_index=False)["revenue"].sum())
fig = px.line(monthly, x="month", y="revenue", color="channel", markers=True)
fig.show()

Titles that say something

A title can state the finding instead of the topic, with a smaller subtitle for the details:

fig.update_layout(
    title=dict(
        text="Web drove the April peak<br><sup>Completed revenue per month and channel, 2024 (Rp)</sup>",
        x=0, xanchor="left",
    )
)
fig.show()
print(fig.layout.title.x, fig.layout.title.xanchor)
0 left

Dot notation: magic underscores

Layout properties are nested: the title has a font, which has a size. You can write the nesting as a dict, or with underscores:

fig.update_layout(title_font_size=22, xaxis_title="Month", yaxis_title="Revenue (Rp)")
print(fig.layout.title.font.size, fig.layout.xaxis.title.text)
22 Month

title_font_size=22 is short for title=dict(font=dict(size=22)). Both styles are everywhere in Plotly code; underscores are handy for one or two settings.

The legend

By default the legend sits on the right and takes space from the plot. A horizontal legend above the chart is often cleaner:

fig.update_layout(
    legend=dict(orientation="h", yanchor="bottom", y=1.02, xanchor="right", x=1, title_text=""),
)
fig.show()
print(fig.layout.legend.orientation, fig.layout.legend.y)
h 1.02

The coordinates are fractions of the plot area: y=1.02 is just above the top, x=1 with xanchor="right" aligns it to the right edge. showlegend=False hides it entirely, which is right when there's only one series, or when you label the lines directly.

Size and margins

Plotly picks a size and leaves generous white space around the chart. For a report or a slide you might want a fixed size, and in a small dashboard tile you'll want much tighter margins so the chart itself gets the space:

fig.update_layout(width=700, height=400, margin=dict(l=40, r=20, t=80, b=40))
print(fig.layout.margin.l, fig.layout.height)
40 400

Leave width unset in dashboards and apps, so the chart fills its container. Tight margins matter in small dashboard tiles, where Plotly's defaults waste a lot of space.

Unified hover

For time series with several lines, one hover box listing all series at that x is easier to read than separate boxes:

fig.update_layout(hovermode="x unified")
fig.show()

Pointing at the story: lines, areas, notes

A chart shows what happened, but the reader still has to work out why it matters. Was April good? Compared with what? A target line, a shaded band for the promo period and a note on the big spike answer those questions on the chart itself:

fig.add_hline(y=2000000, line_dash="dash", line_color="gray",
              annotation_text="Monthly target", annotation_position="top left")
fig.add_vrect(x0="2024-03", x1="2024-05", fillcolor="#5669FD", opacity=0.08, line_width=0,
              annotation_text="Ramadan promo", annotation_position="top right")
fig.add_annotation(x="2024-04", y=2963000, text="USB-C Hub orders", showarrow=True, arrowhead=2)
fig.show()
print(len(fig.layout.shapes), len(fig.layout.annotations))
2 3

Each line or rectangle is a shape in the layout, and each text is an annotation.

Key idea

Annotate the one or two things a reader must notice. A chart with a highlighted promo period and a labelled spike answers the questions before the meeting starts.

update_layout vs update_traces

To changeUseExamples
things around the marksupdate_layouttitle, legend, size, margins, hovermode, fonts
the marks themselvesupdate_tracesline width, marker size, colours, hovertemplate
one axisupdate_xaxes / update_yaxestitles, ranges, tick formats, gridlines
fig.update_traces(line_width=3, marker_size=8)
print(fig.data[0].line.width, fig.data[0].marker.size)
3 8

Why this matters for clean code

Formatting code grows quickly. Keep it readable by grouping settings into a few update_* calls in a fixed order (layout, axes, traces, annotations), each with a clear purpose. When the same settings appear in every chart, move them into a function or a template, which are the next two modules.

Recap

Practise this module

The questions for this lesson run in your browser and check your answer straight away.