study-buddy / app /agents /d3 /templates /chart_multi_line.py
GitHub Actions
deploy d092bea3608b7a29952f16357fda39b7a29e399b
2e818da
Raw
History Blame Contribute Delete
4.08 kB
"""Multi-line chart template — fetch-and-adapt from d3-graph-gallery.com.
Adapted from d3-graph-gallery.com (Yan Holtz) —
https://d3-graph-gallery.com/graph/line_several_group.html
"""
from __future__ import annotations
from pydantic import BaseModel
from app.agents.d3.registry import D3Template, register
class Point(BaseModel):
x: float
y: float
class Series(BaseModel):
name: str
points: list[Point]
class MultiLineData(BaseModel):
title: str = ""
x_label: str = ""
y_label: str = ""
series: list[Series]
_HTML = """<!-- Adapted from d3-graph-gallery.com (Yan Holtz) — https://d3-graph-gallery.com/graph/line_several_group.html -->
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<script src="https://d3js.org/d3.v7.min.js"></script>
<style>
html, body { margin: 0; padding: 0; width: 100vw; height: 100vh; background: #FAF7F2; color: #1A3557; font-family: Georgia, 'Libre Caslon Text', serif; overflow: hidden; }
#my_dataviz { width: 100vw; height: 100vh; }
.axis text { fill: #1A3557; font-size: 12px; }
.axis path, .axis line { stroke: #1A3557; }
.chart-title { fill: #1A3557; font-size: 18px; text-anchor: middle; }
.axis-label { fill: #1A3557; font-size: 13px; text-anchor: middle; }
</style>
</head>
<body>
<div id="my_dataviz"></div>
<script>
const data = __DATA__;
// set the dimensions and margins of the graph
const margin = {top: 50, right: 30, bottom: 60, left: 60},
width = 960 - margin.left - margin.right,
height = 600 - margin.top - margin.bottom;
// append the svg object to the body of the page
const svg = d3.select("#my_dataviz")
.append("svg")
.attr("viewBox", `0 0 ${width + margin.left + margin.right} ${height + margin.top + margin.bottom}`)
.attr("preserveAspectRatio", "xMidYMid meet")
.style("width", "100%")
.style("height", "100%")
.append("g")
.attr("transform", `translate(${margin.left},${margin.top})`);
// Chart title
svg.append("text")
.attr("class", "chart-title")
.attr("x", width / 2)
.attr("y", -20)
.text(data.title);
// Flatten all points across series to compute shared scales
const allPoints = data.series.flatMap(s => s.points);
// Add X axis
const x = d3.scaleLinear()
.domain(d3.extent(allPoints, d => d.x))
.range([ 0, width ]);
svg.append("g")
.attr("class", "axis")
.attr("transform", `translate(0, ${height})`)
.call(d3.axisBottom(x));
svg.append("text")
.attr("class", "axis-label")
.attr("x", width / 2)
.attr("y", height + margin.bottom - 10)
.text(data.x_label);
// Add Y axis
const y = d3.scaleLinear()
.domain([0, d3.max(allPoints, d => d.y)])
.range([ height, 0 ]);
svg.append("g")
.attr("class", "axis")
.call(d3.axisLeft(y));
svg.append("text")
.attr("class", "axis-label")
.attr("transform", "rotate(-90)")
.attr("x", -height / 2)
.attr("y", -45)
.text(data.y_label);
// color palette = one color per series
const color = d3.scaleOrdinal()
.domain(data.series.map(s => s.name))
.range(['#4A7FB5', '#7FA9D6', '#3D6690', '#A7C6E8', '#2C4A66']);
// Draw one line per series
svg.selectAll(".line")
.data(data.series)
.join("path")
.attr("fill", "none")
.attr("stroke", d => color(d.name))
.attr("stroke-width", 2)
.attr("d", d => d3.line()
.x(p => x(p.x))
.y(p => y(p.y))
(d.points)
);
</script>
</body>
</html>
"""
golden_sample = MultiLineData(
title="Demo",
x_label="Time",
y_label="Value",
series=[
Series(name="Series A", points=[Point(x=0, y=1), Point(x=1, y=3), Point(x=2, y=2)]),
Series(name="Series B", points=[Point(x=0, y=2), Point(x=1, y=1), Point(x=2, y=4)]),
],
)
register(
D3Template(
id="multi_line",
family="chart",
title="Multi-line chart",
when_to_use="Compare several named series each changing over the same ordered/continuous axis.",
data_requirements="Two or more named series, each a sequence of (x, y) points.",
schema=MultiLineData,
html_template=_HTML,
golden_sample=golden_sample,
)
)