File size: 4,452 Bytes
2e818da
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
"""Bubble chart template — flow family.

Substitution note: d3-graph-gallery.com's "circular packing" pages
(https://d3-graph-gallery.com/graph/circularpacking_basic.html and
siblings, confirmed by re-checking https://d3-graph-gallery.com/
circularpacking.html's link list) are all `d3.forceSimulation()` bubble
demos with flat dummy data, not `d3.pack()`/`d3.hierarchy()` layouts — the
same substitution note hier_circle_pack.py already made for its own nested
version of this problem. `flow_bubble` needs a FLAT, already-packed bubble
chart (not the nested tree hier_circle_pack.py uses), so this is adapted
instead from a different, real, verified flat `d3.pack()` bubble-chart
source: https://multimedia.report/classes/coding/2018/exercises/basicbubblepackchart/
— which already builds its hierarchy from a flat list exactly the way this
template needs (`d3.hierarchy({children: data}).sum(d => d.value)` then
`d3.pack().size([...]).padding(...)`, one circle + centered label per leaf),
so no nested-to-flat adaptation was even needed here. Field names are wired
to this template's own flat `BubbleData` schema (`label`/`value` instead of
the fetched source's CSV `Fruit`/`Amount` columns); circle fill color and
the radius-proportional label truncation follow the same conventions
already used by hier_circle_pack.py for its own leaf circles.
"""
from __future__ import annotations
from pydantic import BaseModel
from app.agents.d3.registry import D3Template, register


class Bubble(BaseModel):
    label: str
    value: float


class BubbleData(BaseModel):
    title: str = ""
    data: list[Bubble]


_HTML = """<!-- Adapted from https://multimedia.report/classes/coding/2018/exercises/basicbubblepackchart/ (flat-list d3.pack() bubble chart) -->
<!-- d3-graph-gallery.com's circular-packing pages are force-simulation demos, not hierarchy layouts; see module docstring -->
<!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; }
  .chart-title { fill: #1A3557; font-size: 18px; text-anchor: middle; }
  .node-circle { stroke: #1A3557; stroke-width: 1px; }
  .node-label { fill: #1A3557; text-anchor: middle; }
</style>
</head>
<body>
<div id="my_dataviz"></div>
<script>

const data = __DATA__;

const margin = {top: 50, right: 10, bottom: 10, left: 10},
    width = 960 - margin.left - margin.right,
    height = 600 - margin.top - margin.bottom;

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%");

svg.append("text")
  .attr("class", "chart-title")
  .attr("x", (width + margin.left + margin.right) / 2)
  .attr("y", 25)
  .text(data.title);

const g = svg.append("g")
  .attr("transform", `translate(${margin.left},${margin.top})`);

const root = d3.hierarchy({ children: data.data })
  .sum(d => d.value);

d3.pack()
  .size([width - 4, height - 4])
  .padding(3)
  (root);

const palette = ['#4A7FB5', '#7FA9D6', '#3D6690', '#A7C6E8', '#2C4A66', '#B5764A'];

const node = g.selectAll("g.node")
  .data(root.leaves())
  .join("g")
    .attr("class", "node")
    .attr("transform", d => `translate(${d.x},${d.y})`);

node.append("circle")
  .attr("class", "node-circle")
  .attr("r", d => d.r)
  .style("fill", (d, i) => palette[i % palette.length]);

node.append("text")
  .attr("class", "node-label")
  .attr("dy", "0.31em")
  .style("font-size", d => Math.min(14, d.r / 2.5) + "px")
  .text(d => d.data.label.substring(0, d.r / 3));

</script>
</body>
</html>
"""

golden_sample = BubbleData(
    title="Demo",
    data=[
        Bubble(label="A", value=10),
        Bubble(label="B", value=20),
        Bubble(label="C", value=5),
    ],
)

register(
    D3Template(
        id="flow_bubble",
        family="flow",
        title="Bubble chart",
        when_to_use="Show relative size of a flat set of named quantities as packed circles (not a hierarchy).",
        data_requirements="A flat list of labeled values (no nesting).",
        schema=BubbleData,
        html_template=_HTML,
        golden_sample=golden_sample,
    )
)