Imports name this capability’s declared dependencies, which fune builds next to it in your project; each one links to its page.
from .charts_axis_grid_lines import grid_lines
from .charts_axis_number_axis import number_axis
from .charts_axis_time_axis import time_axis
from .charts_extent import extent ← from charts.extent ^1.0.0 · built alongside by fune
from .charts_format_format_tick import format_tick
from .charts_layout_legend_rows import legend_rows
from .charts_layout_plot_area import plot_area
from .charts_layout_types import LegendItem, Margins
from .charts_line_chart_types import LineChart, LineChartSpec, SeriesPath
from .charts_palette import palette ← from charts.palette ^1.0.0 · built alongside by fune
from .charts_scale_linear_scale import linear_scale
from .charts_scale_nice_domain import nice_domain
from .charts_scale_time_scale import time_scale
from .charts_shape_line_path import line_path
from .charts_shape_monotone_curve import monotone_curve
from .charts_shape_step_path import step_path
from .charts_shape_types import Point
from .charts_ticks_nice_ticks import nice_ticks
from .charts_ticks_tick_step import tick_step
from .math_round_float import round_float ← from math.round-float ^1.0.0 · built alongside by fune
# Fixed layout constants; the README explains each.
CHAR_WIDTH = 7
SWATCH = 10
LEGEND_GAP = 16
LEGEND_ROW = 18
EDGE = 8
TICK_SIZE = 6
LABEL_PADDING = 3
TOP_WITHOUT_LEGEND = 12
RIGHT = 20
BOTTOM = 30
def line_chart(spec: LineChartSpec) -> LineChart:
"""A whole line chart as geometry, by the fixed rules in the README."""
width, height, series = spec.width, spec.height, spec.series
if len(series) == 0:
raise ValueError("a line chart needs at least one series")
if (spec.x is None) == (spec.dates is None):
raise ValueError("give exactly one of x and dates")
count = len(spec.x) if spec.x is not None else len(spec.dates)
for s in series:
if len(s.values) != count:
raise ValueError(
'series "%s" has %d values but there are %d x values' % (s.name, len(s.values), count)
)
legend = []
top = TOP_WITHOUT_LEGEND
if len(series) > 1:
rows = legend_rows([s.name for s in series], width - 2 * EDGE, CHAR_WIDTH, SWATCH, LEGEND_GAP, LEGEND_ROW)
legend = [
LegendItem(
label=it.label,
row=it.row,
x=round_float(it.x + EDGE, 2),
y=round_float(it.y + EDGE, 2),
width=it.width,
text_x=round_float(it.text_x + EDGE, 2),
)
for it in rows
]
top = EDGE + (rows[-1].row + 1) * LEGEND_ROW + EDGE
values = [v for s in series for v in s.values]
found = extent(values)
if found is None:
raise ValueError("a line chart needs at least one value to plot")
lo, hi = found
if spec.y_zero:
lo = min(lo, 0.0)
hi = max(hi, 0.0)
if lo == hi:
lo -= 1
hi += 1
y_count = max(2, int((height - top - BOTTOM) // 50))
y_domain = nice_domain([lo, hi], y_count).domain
y_step = tick_step(y_domain[0], y_domain[1], y_count)
widest = 0
for v in nice_ticks(y_domain[0], y_domain[1], y_count):
widest = max(widest, len(format_tick(v, y_step)))
left = TICK_SIZE + LABEL_PADDING + widest * CHAR_WIDTH + EDGE
plot = plot_area(width, height, Margins(top=top, right=RIGHT, bottom=BOTTOM, left=left))
bottom_y = plot.y + plot.height
y_range = [bottom_y, plot.y]
y_axis = number_axis(y_domain, y_range, y_count, "left", plot.x, TICK_SIZE)
x_range = [plot.x, plot.x + plot.width]
x_count = max(2, int(plot.width // 80))
if spec.x is not None:
x_domain = extent(spec.x)
if x_domain is None or x_domain[0] == x_domain[1]:
raise ValueError("a line chart needs at least two different x values")
x_axis = number_axis(x_domain, x_range, x_count, "bottom", bottom_y, TICK_SIZE)
xs = [linear_scale(x_domain, x_range, v, False) for v in spec.x]
else:
dates = spec.dates
if len(dates) == 0 or min(dates) == max(dates):
raise ValueError("a line chart needs at least two different x values")
first, last = min(dates), max(dates)
x_axis = time_axis([first, last], x_range, x_count, "bottom", bottom_y, TICK_SIZE)
xs = [time_scale([first, last], x_range, d, False) for d in dates]
colors = palette("okabe-ito", len(series), True)
lines = []
for i, s in enumerate(series):
points = [
Point(x=xs[j], y=None if v is None else linear_scale(y_domain, y_range, v, False))
for j, v in enumerate(s.values)
]
if spec.curve == "monotone":
path = monotone_curve(points)
elif spec.curve == "step":
path = step_path(points, "middle")
else:
path = line_path(points)
lines.append(SeriesPath(name=s.name, color=colors[i], path=path))
return LineChart(
width=round_float(width, 2),
height=round_float(height, 2),
plot=plot,
x_axis=x_axis,
y_axis=y_axis,
grid_lines=grid_lines(y_axis, plot.width),
lines=lines,
legend=legend,
)