Line Chart¶
A line chart connects ordered points to show how a value moves along a continuous axis, usually time: trends, growth, seasons, and how the trajectories of several series compare. This guide shows how to create line charts with the datachart.charts.LineChart function, starting with the basics and building up to worked examples on real data.
Looking for a specific customization? Jump straight to the quick reference, which maps common tasks to the parameter or style attribute that does the job.
from datachart.charts import LineChart
Basics¶
The examples in this guide share one dataset: the global mean surface temperature anomaly, the difference of each year's global average from the 1951–1980 mean in °C, from 1880 to 2024 (source: NASA GISS Surface Temperature Analysis, GISTEMP v4, rounded to two decimals). The data lives in a hidden cell. warming holds the yearly anomaly as one data point per year, and warming_by_hemisphere holds one series each for the Northern and Southern Hemisphere over the same years, with the year-to-year spread of each five-year window as yerr. The series is famous for a reason: it starts flat, wobbles for a century, and then climbs, and every customization below helps to read that.
Each data point is a dictionary with an x value (here the year) and a y value:
warming[:3]
[{'x': 1880, 'y': -0.17}, {'x': 1881, 'y': -0.09}, {'x': 1882, 'y': -0.11}]
Basic example. Only the data argument is required to draw the line chart:
LineChart(
# add the data to the chart
data=warming
).show()
Customizing the Line Chart¶
Every customization is either a keyword argument of LineChart or a plot_line_* / plot_area_* attribute of its style dictionary. The table maps common tasks to the one you need and links to the subsection that shows it.
| I want to… | Use | See |
|---|---|---|
| add a title and axis labels | title, xlabel, ylabel |
Title, axis labels and ticks |
| set custom tick positions and labels | xticks, xticklabels, yticks, yticklabels |
Title, axis labels and ticks |
| format the tick labels | xticks_format, yticks_format |
Title, axis labels and ticks |
| fix the axis range | xmin, xmax, ymin, ymax |
Title, axis labels and ticks |
| resize the figure | figsize |
Figure size and grid |
| show grid lines | show_grid |
Figure size and grid |
| fix the aspect ratio of the axes | aspect_ratio |
Figure size and grid |
| change the line color, width, or dashes | style={"plot_line_color": ..., "plot_line_style": ...} |
Line style |
| mark the data points | style={"plot_line_marker": ...} |
Line style |
| draw the line as steps | style={"plot_line_drawstyle": ...} |
Line style |
| fill the area under the line | show_area, style={"plot_area_color": ..., "plot_area_alpha": ...} |
Area under the line |
| print the value beside the points | show_values, value_format, value_step |
Value labels |
| mark a threshold or an event | hlines, vlines |
Reference lines |
| shade a period or a range | hspans, vspans |
Reference bands |
| put a note on the chart | texts |
Text annotations |
| compare several series in one chart | data as a list of lists, subtitle, show_legend |
Multiple Line Charts |
| highlight one series, mute the rest | emphasis, emphasis_rule |
Emphasis |
| title and place the legend | legend |
Legend |
| draw a confidence band | yerr in data, show_yerr |
Confidence interval |
| draw each series in its own subplot | subplots, sharex, sharey, max_cols |
Subplots |
| plot against real dates | datetime objects as x, xticks_format |
Datetime axis |
| use a logarithmic axis | scaley, scalex |
Axis scales |
| plot data with other key names | x, y, yerr |
Custom data keys |
| save the chart to a file | save_figure |
Saving Figures guide |
The parameters that accept a constant, with the class in datachart.constants that lists its values:
| Parameter | Constant |
|---|---|
emphasis |
EMPHASIS |
figsize |
FIG_SIZE |
legend={"location": ..., "alignment": ...} |
LEGEND_LOCATION, LEGEND_ALIGN |
show_grid |
SHOW_GRID |
value_format |
VALUE_FORMAT |
aspect_ratio |
ASPECT_RATIO |
scalex |
SCALE |
scaley |
SCALE |
xticks_format |
VALUE_FORMAT, DATE_FORMAT |
yticks_format |
VALUE_FORMAT, DATE_FORMAT |
The full list of style attributes is in the datachart.typings.LineStyleAttrs and datachart.typings.AreaStyleAttrs types; the full list of parameters is in the datachart.charts.LineChart reference.
Title, axis labels and ticks¶
A line without a title and axis labels is a shape, not a statement; title, xlabel and ylabel say what moves and in what unit. The ticks are the reader's ruler: xticks and yticks set their positions (xticklabels and yticklabels replace the labels), xticks_format and yticks_format format them (a VALUE_FORMAT member or a "{x:.1f}" style string on a value axis), and xtickrotate and ytickrotate tilt them. xmin, xmax, ymin and ymax fix the axis range, so the chart shows the span you mean rather than the span of the data. Here the years tick every twenty years, the anomaly prints with a sign, and the range is symmetric around zero, where the anomaly is defined.
LineChart(
data=warming,
# add the title
title="Global mean surface temperature anomaly",
# add the x and y axis labels
xlabel="Year",
ylabel="Anomaly (°C, vs. 1951–1980)",
# a tick every twenty years, values printed with a sign
xticks=list(range(1880, 2025, 20)),
yticks_format="{x:+.1f}",
# a range symmetric around zero
ymin=-0.6,
ymax=1.4,
xmin=1880,
xmax=2025,
).show()
Figure size and grid¶
A long time series wants a wide, short figure, so the years have room and the slope stays honest. figsize takes a (width, height) tuple in inches or one of the presets in datachart.constants.FIG_SIZE, sized for a full or half page width. Grid lines let the eye carry a point across to the axis; show_grid draws them with a SHOW_GRID member, Y for the value axis alone, X for the time axis, BOTH for both. aspect_ratio fixes the ratio of the axes rather than of the figure, one data unit the same length on both axes with ASPECT_RATIO.EQUAL; a time series never needs it, but the ROC curve below does.
from datachart.constants import FIG_SIZE, SHOW_GRID
LineChart(
data=warming,
title="Global mean surface temperature anomaly",
xlabel="Year",
ylabel="Anomaly (°C)",
xticks=list(range(1880, 2025, 20)),
yticks_format="{x:+.1f}",
# a wide, short figure
figsize=FIG_SIZE.FULL_SHORT,
# grid lines along both axes
show_grid=SHOW_GRID.BOTH,
).show()
Line style¶
The style dictionary sets the look of the line: its color and alpha, its width, its dash pattern from LINE_STYLE, a marker at each point from LINE_MARKER, and the draw style from LINE_DRAW_STYLE; the attributes are listed in datachart.typings.LineStyleAttrs, and any attribute left out keeps the value of the active theme. The look should follow the data. A hundred and forty-five yearly values are a dense series, so a thin line without markers reads best; the example draws it thin and in a warm color, and the next section shows where markers and steps earn their place.
from datachart.constants import LINE_STYLE, LINE_MARKER, LINE_DRAW_STYLE
LineChart(
data=warming,
# a thin line in a warm color
style={
"plot_line_color": "#c1121f",
"plot_line_width": 1.2,
},
title="Global mean surface temperature anomaly",
xlabel="Year",
ylabel="Anomaly (°C)",
xticks=list(range(1880, 2025, 20)),
yticks_format="{x:+.1f}",
figsize=FIG_SIZE.FULL_SHORT,
show_grid=SHOW_GRID.BOTH,
).show()
Averages over a period are one value per period rather than a continuous curve, and a step line says so. decades, the mean anomaly of each decade, is sparse enough for markers and better drawn as steps: LINE_DRAW_STYLE.STEPS_MID centers each step on its point, and a dashed pattern marks the series as derived.
# the mean anomaly of each full decade, placed at the middle of the decade
decades = [
{"x": start + 5, "y": round(sum(ANOMALY[i : i + 10]) / 10, 2)}
for i, start in enumerate(YEARS)
if start % 10 == 0 and i + 10 <= len(ANOMALY)
]
LineChart(
data=decades,
style={
# one value per decade: steps centered on the points, marked and dashed
"plot_line_drawstyle": LINE_DRAW_STYLE.STEPS_MID,
"plot_line_marker": LINE_MARKER.CIRCLE,
"plot_line_style": LINE_STYLE.DASHED,
},
title="Mean temperature anomaly by decade",
xlabel="Decade",
ylabel="Anomaly (°C)",
xticks=list(range(1880, 2025, 20)),
yticks_format="{x:+.1f}",
figsize=FIG_SIZE.FULL_SHORT,
show_grid=SHOW_GRID.BOTH,
).show()
Area under the line¶
Filling the area under a line turns a trajectory into a quantity: the fill draws the eye to how much has accumulated, not only where the line is. show_area fills between the line and the bottom of the axes in the color of the line at a lower alpha; plot_area_color, plot_area_alpha and plot_area_hatch from datachart.typings.AreaStyleAttrs override that. A fill measures from the bottom of the axes, so the axes must start where the measure starts: the anomaly has not been below zero since 1976, so warming_recent, the series from 1977 on, sits on a value axis pinned at zero and the fill measures the warming above the reference period.
# the years since the anomaly was last below zero
warming_recent = [point for point in warming if point["x"] >= 1977]
LineChart(
data=warming_recent,
style={
"plot_line_color": "#c1121f",
# a stronger fill than the theme default
"plot_area_alpha": 0.3,
},
title="Global mean surface temperature anomaly since 1977",
xlabel="Year",
ylabel="Anomaly (°C)",
xticks=list(range(1980, 2025, 10)),
yticks_format="{x:+.1f}",
figsize=FIG_SIZE.FULL_SHORT,
show_grid=SHOW_GRID.BOTH,
# fill down to the zero line
show_area=True,
ymin=0,
xmin=1977,
xmax=2024,
).show()
Value labels¶
Where the exact values matter, show_values prints the value beside each point; value_format formats it (a VALUE_FORMAT member or any "{x:.1f}", "{:.1f}%" or "%g" style string) and value_step labels every Nth point when the series is too dense to label them all; by default the step is chosen so that neighbouring labels stay apart. Each label sits above or below its point, wherever it overlaps least, and takes the plot_value_* style of the active theme (ValueLabelStyleAttrs). Labeling every yearly value would be noise; labeling the decade means, with the sign, states the trend in numbers.
from datachart.constants import VALUE_FORMAT
LineChart(
data=decades,
style={"plot_line_marker": LINE_MARKER.CIRCLE},
title="Mean temperature anomaly by decade",
xlabel="Decade",
ylabel="Anomaly (°C)",
xticks=list(range(1880, 2025, 20)),
yticks_format="{x:+.1f}",
figsize=FIG_SIZE.FULL_SHORT,
show_grid=SHOW_GRID.Y,
# print every decade's value, with its sign
show_values=True,
value_format="{x:+.2f}",
value_step=1,
ymin=-0.6,
ymax=1.4,
).show()
Reference lines¶
A reference line gives the reader something to measure the line against: a threshold it must not cross, or the moment something happened. hlines draws a horizontal line at a value and vlines a vertical one at an x position, each a dictionary or a list of them with the position, an optional label for the legend and a style; the keys are listed in HLineSettingAttrs and VLineSettingAttrs. The x position is in data coordinates, so a line can sit anywhere along the axis. The example marks the 1.5 °C of the Paris Agreement, which is measured against pre-industrial levels and lands near 1.2 °C on this 1951–1980 baseline, and the year the Agreement was adopted.
# 1.5 °C above pre-industrial, about 0.3 °C below the 1951-1980 mean used here
PARIS_LIMIT = 1.2
PARIS_YEAR = 2015
LineChart(
data=warming,
subtitle="anomaly",
# a dashed line at the limit
hlines={
"y": PARIS_LIMIT,
"label": "1.5 °C above pre-industrial",
"style": {"plot_hline_color": "#c1121f", "plot_hline_style": LINE_STYLE.DASHED},
},
# a dotted line at the year of the Agreement
vlines={
"x": PARIS_YEAR,
"label": "Paris Agreement",
"style": {"plot_vline_color": "#555555", "plot_vline_style": LINE_STYLE.DOTTED},
},
title="Global mean surface temperature anomaly",
xlabel="Year",
ylabel="Anomaly (°C)",
xticks=list(range(1880, 2025, 20)),
yticks_format="{x:+.1f}",
figsize=FIG_SIZE.FULL_SHORT,
show_grid=SHOW_GRID.Y,
show_legend=True,
).show()
Reference bands¶
Where a line marks a value, a band marks a range: a period, a tolerance, an interval the line should stay in. hspans shades between two values and vspans between two x positions, each a dictionary or a list of them with the bounds, an optional label and a style; the keys are listed in HSpanSettingAttrs and VSpanSettingAttrs. At least one bound is required and an omitted bound runs to the axis edge, so {"xmin": 2000} shades everything from 2000 to the right. The band sits over the grid and under the line, so the data stays readable through it. The example shades the reference period the anomaly is measured against and the range of the anomaly within it.
BASELINE = (1951, 1980)
baseline_values = [a for y, a in zip(YEARS, ANOMALY) if BASELINE[0] <= y <= BASELINE[1]]
LineChart(
data=warming,
subtitle="anomaly",
# shade the reference period
vspans={"xmin": BASELINE[0], "xmax": BASELINE[1], "label": "reference period"},
# and the range of the anomaly within it
hspans={
"ymin": min(baseline_values),
"ymax": max(baseline_values),
"label": "reference range",
"style": {"plot_hspan_color": "#2a9d8f"},
},
title="Global mean surface temperature anomaly",
xlabel="Year",
ylabel="Anomaly (°C)",
xticks=list(range(1880, 2025, 20)),
yticks_format="{x:+.1f}",
figsize=FIG_SIZE.FULL_SHORT,
show_grid=SHOW_GRID.Y,
show_legend=True,
).show()
Text annotations¶
A line has moments worth naming: a record, a turn, an outlier. texts places a note on the chart, with an optional target to draw a connector to a data point; the position is in data coordinates by default or in axes fractions with "coords": "axes", which keeps the note in place whatever the axis limits. The Text Annotations guide covers placement, connector looks and styling. The example names the warmest year on record and the last year below the reference mean.
RECORD = max(warming, key=lambda point: point["y"])
LAST_BELOW = max((point for point in warming if point["y"] < 0), key=lambda point: point["x"])
LineChart(
data=warming,
texts=[
{
"text": f"{RECORD['x']}: warmest year on record",
"x": 0.55,
"y": 0.85,
"coords": "axes",
"target": (RECORD["x"], RECORD["y"]),
},
{
"text": f"{LAST_BELOW['x']}: last year below the reference mean",
"x": 0.05,
"y": 0.62,
"coords": "axes",
"target": (LAST_BELOW["x"], LAST_BELOW["y"]),
},
],
title="Global mean surface temperature anomaly",
xlabel="Year",
ylabel="Anomaly (°C)",
xticks=list(range(1880, 2025, 20)),
yticks_format="{x:+.1f}",
figsize=FIG_SIZE.FULL_MEDIUM,
show_grid=SHOW_GRID.Y,
).show()
Multiple Line Charts¶
To compare several series, pass a list of lists to data: each inner list is one line, and the per-series attributes (subtitle, style, emphasis) become lists aligned with it. warming_by_hemisphere is such a list, one series per hemisphere, and subtitle names them for the legend. A single style dictionary applies to every line; a list styles each line separately, with None keeping the theme style for that line. The two hemispheres move together for a century and then part, the north warming faster; on one chart the gap is the message.
LineChart(
# one series per hemisphere
data=warming_by_hemisphere,
# named for the legend
subtitle=HEMISPHERES,
# one style per line
style=[
{"plot_line_color": "#c1121f"},
{"plot_line_color": "#1d3557"},
],
title="Temperature anomaly by hemisphere",
xlabel="Year",
ylabel="Anomaly (°C)",
xticks=list(range(1880, 2025, 20)),
yticks_format="{x:+.1f}",
figsize=FIG_SIZE.FULL_SHORT,
show_grid=SHOW_GRID.Y,
show_legend=True,
).show()
Emphasis¶
When a chart carries several series, the story is usually about one of them, and emphasis makes it visible. emphasis takes one role per series, aligned with data: "highlight" thickens a line and brings it to the front, "background" mutes it (the theme's muted color at a lower alpha, thinner, behind the others, and out of the legend), and None leaves it as it is. The roles are also available as the EMPHASIS constants, and the Highlighting guide covers emphasis across every chart type and theme. With the global series added as context, the example highlights the Northern Hemisphere against the other two.
LineChart(
data=[warming] + warming_by_hemisphere,
subtitle=["Global"] + HEMISPHERES,
# highlight the north, mute the rest
emphasis=["background", "highlight", "background"],
title="Temperature anomaly, the Northern Hemisphere against the rest",
xlabel="Year",
ylabel="Anomaly (°C)",
xticks=list(range(1880, 2025, 20)),
yticks_format="{x:+.1f}",
figsize=FIG_SIZE.FULL_SHORT,
show_grid=SHOW_GRID.Y,
show_legend=True,
).show()
emphasis_rule picks the lines from the data instead of naming them. It is a one-key dictionary, {"top": n}, {"bottom": n}, {"above": v}, {"below": v} (strict) or {"between": (lo, hi)} (inclusive), read against a summary of each line's own y values: the mean by default, or the "median", "min", "max" or "sum" named by an extra "by" key. The lines that match are highlighted and the rest muted, and an explicit emphasis role wins over the rule. Asking for the line that warmed least, the lowest mean, needs no knowledge of which one it is:
LineChart(
data=[warming] + warming_by_hemisphere,
subtitle=["Global"] + HEMISPHERES,
# the one line with the lowest mean
emphasis_rule={"bottom": 1},
title="Temperature anomaly, the series that warmed least",
xlabel="Year",
ylabel="Anomaly (°C)",
xticks=list(range(1880, 2025, 20)),
yticks_format="{x:+.1f}",
figsize=FIG_SIZE.FULL_SHORT,
show_grid=SHOW_GRID.Y,
show_legend=True,
).show()
Legend¶
show_legend lists the series; legend says where and how, with a title, a location from LEGEND_LOCATION, the number of columns ncols, and the alignment of the entries from LEGEND_ALIGN; a field left out falls back to the theme (LegendSettingAttrs). A rising series leaves its empty corner at the top left, which is where the legend goes.
from datachart.constants import LEGEND_LOCATION
LineChart(
data=warming_by_hemisphere,
subtitle=HEMISPHERES,
title="Temperature anomaly by hemisphere",
xlabel="Year",
ylabel="Anomaly (°C)",
xticks=list(range(1880, 2025, 20)),
yticks_format="{x:+.1f}",
figsize=FIG_SIZE.FULL_SHORT,
show_grid=SHOW_GRID.Y,
show_legend=True,
# a titled legend in the empty corner
legend={"title": "Hemisphere", "location": LEGEND_LOCATION.UPPER_LEFT},
).show()
Confidence interval¶
A mean without its spread overstates what is known. When each data point carries a yerr, show_yerr draws a band from y - yerr to y + yerr around the line, styled with the same plot_area_* attributes as the area under the line. The hemisphere series carry the spread of each five-year window as yerr, and the bands show that the two hemispheres are indistinguishable until the late twentieth century, when the bands part.
LineChart(
data=warming_by_hemisphere,
subtitle=HEMISPHERES,
style=[
{"plot_line_color": "#c1121f"},
{"plot_line_color": "#1d3557"},
],
title="Temperature anomaly by hemisphere, with the five-year spread",
xlabel="Year",
ylabel="Anomaly (°C)",
xticks=list(range(1880, 2025, 20)),
yticks_format="{x:+.1f}",
figsize=FIG_SIZE.FULL_SHORT,
show_grid=SHOW_GRID.Y,
show_legend=True,
# draw the spread as a band around each line
show_yerr=True,
).show()
Subplots¶
Lines that cross and overlap hide each other, and when the question is the shape of each series rather than the gap between them, subplots=True draws each in its own panel: subtitle titles the panels, title, xlabel and ylabel stay global, and max_cols limits the panels per row. sharex=True and sharey=True put the panels on common axes, so a slope in one panel is the same slope in the next; without them each panel scales to its own data, and the slower-warming south would look as steep as the north.
LineChart(
data=warming_by_hemisphere,
subtitle=HEMISPHERES,
style=[
{"plot_line_color": "#c1121f"},
{"plot_line_color": "#1d3557"},
],
title="Temperature anomaly by hemisphere",
xlabel="Year",
ylabel="Anomaly (°C)",
xticks=list(range(1880, 2025, 50)),
yticks_format="{x:+.1f}",
figsize=FIG_SIZE.FULL_SHORT,
show_grid=SHOW_GRID.Y,
# one panel per hemisphere, side by side, on common axes
subplots=True,
max_cols=2,
sharex=True,
sharey=True,
).show()
Additional Features¶
Datetime axis¶
Yearly data sits fine on a numeric axis, but daily or monthly data wants a real time axis, where points land at their elapsed time and the ticks label themselves sensibly. An x value that is a real temporal object (a datetime, a date, a numpy.datetime64 or a pandas Timestamp) does exactly that: the ticks pick a concise, non-repeating label for the visible span, with the part every label shares (such as 2024-Apr) as an offset. Date strings such as "2024-03-01" are not parsed; they draw as unordered categories, like any other string. daily_index holds sixty days of an illustrative index level.
from datetime import date, timedelta
# daily closing level of an index over one spring (illustrative)
start = date(2024, 3, 1)
daily_index = [
{"x": start + timedelta(days=i), "y": 100 + round(2.5 * i - 0.05 * i * i, 1)}
for i in range(60)
]
LineChart(
data=daily_index,
title="Daily index level",
xlabel="Date",
ylabel="Level",
figsize=FIG_SIZE.FULL_SHORT,
show_grid=SHOW_GRID.Y,
).show()
On a time axis everything that takes an x position takes a date: xticks, xmin and xmax, reference lines and bands, and annotation targets. xticks_format labels the ticks with a DATE_FORMAT member or any strftime pattern instead of the automatic labels. The example fixes the ticks to the first and the fifteenth of each month, marks a rebalancing date, and shades an earnings season.
from datachart.constants import DATE_FORMAT
LineChart(
data=daily_index,
title="Daily index level",
xlabel="Date",
ylabel="Level",
figsize=FIG_SIZE.FULL_SHORT,
show_grid=SHOW_GRID.Y,
# ticks, limits, lines and bands all take dates
xticks_format=DATE_FORMAT.MONTH_DAY,
xticks=[date(2024, 3, 1), date(2024, 3, 15), date(2024, 4, 1), date(2024, 4, 15)],
xmin=date(2024, 3, 1),
xmax=date(2024, 4, 20),
vlines={"x": date(2024, 3, 20), "label": "rebalance"},
vspans={"xmin": date(2024, 4, 1), "xmax": date(2024, 4, 10), "label": "earnings season"},
yticks_format=VALUE_FORMAT.INTEGER,
show_legend=True,
).show()
Axis scales¶
A quantity that grows by a constant factor looks like a hockey stick on a linear axis and like a straight line on a logarithmic one, and only the second lets the reader judge whether the growth rate changed. scaley and scalex take a SCALE member: LINEAR, LOG, SYMLOG (log on both sides of zero) or ASINH. transistors, defined in a hidden cell, holds the transistor count of a representative microprocessor per year from the Intel 4004 (1971) to the Apple M1 Ultra (2022), Moore's law in sixteen data points rounded from the manufacturers' figures. On a linear scale the first forty years collapse onto the x-axis; on a log scale the doubling every two years becomes the straight line it is famous for.
from datachart.constants import SCALE
for scale in [SCALE.LINEAR, SCALE.LOG]:
LineChart(
data=transistors,
style={"plot_line_marker": LINE_MARKER.CIRCLE},
title=f"Transistors per microprocessor on the '{scale}' scale",
xlabel="Year",
ylabel="Transistors",
figsize=FIG_SIZE.FULL_SHORT,
show_grid=SHOW_GRID.BOTH,
# the scale of the value axis
scaley=scale,
).show()
Custom data keys¶
Data that comes from a file or an API rarely uses the x, y and yerr keys, and renaming every record just to plot it is a chore. Instead, tell LineChart which keys to read with the x, y and yerr arguments. readings stores the Northern Hemisphere series the way a data export might, under year, anomaly and spread:
readings = [
{"year": point["x"], "anomaly": point["y"], "spread": point["yerr"]}
for point in warming_by_hemisphere[0]
]
readings[:3]
[{'year': 1880, 'anomaly': np.float64(-0.15), 'spread': 0.06},
{'year': 1881, 'anomaly': np.float64(-0.07), 'spread': 0.07},
{'year': 1882, 'anomaly': np.float64(-0.09), 'spread': 0.1}]
LineChart(
data=readings,
# the keys that hold the x, y and error values
x="year",
y="anomaly",
yerr="spread",
title="Northern Hemisphere temperature anomaly",
xlabel="Year",
ylabel="Anomaly (°C)",
xticks=list(range(1880, 2025, 20)),
yticks_format="{x:+.1f}",
figsize=FIG_SIZE.FULL_SHORT,
show_grid=SHOW_GRID.Y,
show_yerr=True,
).show()
Real-World Examples¶
The examples below put the features above to work on real or realistic data, each one answering a question. The data lives in hidden cells; each example says what its data is and where it comes from.
Example 1: A ROC Curve (Custom Data Keys and an Equal Aspect Ratio)¶
roc_curves holds the receiver operating characteristic of two illustrative binary classifiers: each point is the false positive rate (fp) and the true positive rate (tp) at one decision threshold, so the keys are mapped with the x and y arguments. The question is which classifier separates the classes better, and the answer is the area under each curve. A ROC curve is read against the diagonal of chance, so the axes keep an equal aspect ratio, and the area under each curve is filled with a hatch and its size printed in the subtitle; the subplots share both axes so the two areas compare.
from datachart.constants import ASPECT_RATIO, HATCH_STYLE
LineChart(
data=roc_curves,
# name each curve by its area
subtitle=[f"{name} (AUC {auc(points):.2f})" for name, points in ROC_POINTS.items()],
# the points are stored as "fp" and "tp", instead of "x" and "y"
x="fp",
y="tp",
# hatch the area under each curve (a single style applies to every chart)
style={"plot_area_hatch": HATCH_STYLE.DIAGONAL},
title="ROC curves",
xlabel="False positive rate",
ylabel="True positive rate",
figsize=FIG_SIZE.FULL_SHORT,
show_grid=SHOW_GRID.BOTH,
xmin=0,
xmax=1,
ymin=0,
ymax=1,
show_area=True,
subplots=True,
sharex=True,
sharey=True,
# one unit the same length on both axes
aspect_ratio=ASPECT_RATIO.EQUAL,
).show()
Example 2: Training Runs (A Confidence Band on a Log Scale, and the Best Run Picked by a Rule)¶
training_loss holds the validation loss of three illustrative training methods, evaluated every five steps over 200 steps and averaged over several runs, with the standard deviation across the runs as spread. The question is which method converges lowest, and whether the runs agree. The loss decays toward a floor, so a log value axis keeps the late-training differences readable; show_yerr draws the run-to-run spread as a band around each mean; and emphasis_rule highlights the method with the lowest final loss without naming it, so the same cell keeps working when the methods change.
LineChart(
data=training_loss,
subtitle=list(LOSS_CURVES),
# the points are stored as "step", "loss" and "spread"
x="step",
y="loss",
yerr="spread",
# the method with the lowest final loss
emphasis_rule={"bottom": 1, "by": "min"},
title="Validation loss during training",
xlabel="Training step",
ylabel="Validation loss",
figsize=FIG_SIZE.FULL_SHORT,
show_grid=SHOW_GRID.Y,
show_legend=True,
# draw the run-to-run spread as a band
show_yerr=True,
# a log value axis
scaley=SCALE.LOG,
).show()
Example 3: Did the Campaign Work? (Reference Lines, a Band, a Note, and a Panel)¶
weekly_visitors holds sixteen weeks of illustrative weekly unique visitors of a website, and weekly_signups the sign-ups of the same weeks. A marketing campaign ran from week 7 to week 10, and the hosting plan is sized for 60,000 weekly visitors. The chart has to answer two questions at once, whether the campaign moved the numbers and when the plan needs upgrading: a band shades the campaign weeks, a line marks the capacity, and a note names the week it was first exceeded. Visitors and sign-ups live on different scales, so Panel overlays the two line charts with the sign-ups on a second value axis; the reference marks, declared on the visitor chart, travel with it.
from datachart.utils import Panel
visitors = LineChart(
data=weekly_visitors,
subtitle="unique visitors (thousands)",
style={"plot_line_marker": LINE_MARKER.CIRCLE},
# shade the campaign weeks
vspans={"xmin": CAMPAIGN[0], "xmax": CAMPAIGN[1], "label": "campaign"},
# mark the hosting capacity
hlines={
"y": CAPACITY,
"label": "hosting capacity",
"style": {"plot_hline_color": "#c1121f", "plot_hline_style": LINE_STYLE.DASHED},
},
# name the week the capacity was first exceeded
texts={
"text": f"over capacity in week {OVER_CAPACITY['x']}",
"x": 0.05,
"y": 0.85,
"coords": "axes",
"target": (OVER_CAPACITY["x"], OVER_CAPACITY["y"]),
},
)
signups = LineChart(
data=weekly_signups,
subtitle="sign-ups",
style={"plot_line_color": "#2a9d8f", "plot_line_style": LINE_STYLE.DASHED},
)
Panel(
[
{"figure": visitors, "y_axis": "left"},
{"figure": signups, "y_axis": "right"},
],
title="Weekly website traffic around the campaign",
xlabel="Week",
ylabel_left="Visitors (thousands)",
ylabel_right="Sign-ups",
figsize=FIG_SIZE.FULL_SHORT,
show_grid=SHOW_GRID.Y,
show_legend=True,
ymin=0,
ymin_right=0,
).show()