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 |
x2
x2
x2
x2
x2
x2
x2
x2
x2
x2
x2
x2
x2
x2
x2
x2
x2
x2
x2
x2
x2
x2
x2
x2
x2
x2
x2
x2
x2
x2
x2
x2
x2
x2
x2
x2
x2
x2
x7
x7
x2
x2
x2
x2
x2
x2
x2
x1
x1
x1
x1
x3
x1
x1
x3
x3
x3
x3
x3
x3
x3
x3
x3
x3
x3
x3
x3
x3
x3
x3
x1
x1
x2
x2 |
|
import { d3, type d3arg, type d3data, mkcolor, mkconfig, mksvg, type options } from "./_graph.ts"
import { pick } from "@std/collections"
export type { options }
export function pie(datalist: Record<PropertyKey, { color?: string; data: number }>, options: Pick<options, "width" | "height" | "legend"> = {}): string {
const config = mkconfig(pick(options, ["width", "height"]))
const { margin, width, height } = config
const radius = Math.min(width, height) / 2
const svg = mksvg({ width, height })
const K = Object.keys(datalist)
const V = Object.values(datalist)
const I = d3.range(K.length).filter((i: number) => !Number.isNaN(V[i].data))
const D = d3.pie().padAngle(1 / radius).sort(null).value((i: number | { valueOf(): number }) => V[+i].data)(I)
const labels = d3.arc().innerRadius(radius / 2).outerRadius(radius / 2)
svg.append("g")
.attr("transform", `translate(${(width - (options.legend ? config.legend.width : 0)) / 2},${height / 2})`)
.attr("stroke", "white")
.attr("stroke-width", 1)
.attr("stroke-linejoin", "round")
.selectAll("path")
.data(D)
.join("path")
.attr("fill", (d: d3data) => V[+d.data].color ?? mkcolor(K[+d.data]))
.attr("d", d3.arc().innerRadius(0).outerRadius(radius) as d3arg)
.append("title")
.text((d: d3data) => `${K[+d.data]}\n${V[+d.data].data}`)
svg.append("g")
.attr("transform", `translate(${(width - (options.legend ? config.legend.width : 0)) / 2},${height / 2})`)
.attr("font-family", "sans-serif")
.attr("font-size", `${config.texts.fontsize}px`)
.attr("text-anchor", "middle")
.attr("fill", "white")
.attr("stroke", "rbga(0,0,0,.9)")
.attr("paint-order", "stroke fill")
.selectAll("text")
.data(D)
.join("text")
.attr("transform", (d: d3data) => `translate(${labels.centroid(d)})`)
.selectAll("tspan")
.data((d: d3data) => {
const lines = `${K[+d.data]}\n${V[+d.data].data}`.split(/\n/)
return (d.endAngle - d.startAngle) > 0.25 ? lines : lines.slice(0, 1)
})
.join("tspan")
.attr("x", 0)
.attr("y", (_: unknown, i: number) => `${i * 1.1}em`)
.attr("font-weight", (_: unknown, i: number) => i ? null : "bold")
.text((d: d3data) => d)
if (options.legend) {
svg.append("g")
.attr("class", "legend")
.attr("transform", `translate(${width - margin.right - config.legend.width},${margin.top})`)
.selectAll("g")
.data(Object.keys(datalist).map(([name]) => ({ name, value: datalist[name].data, color: datalist[name].color ?? mkcolor(name) })))
.enter()
.each(function (this: d3arg, d: d3data, i: number) {
d3.select(this)
.append("rect")
.attr("x", 0)
.attr("y", i * (config.legend.fontsize + config.legend.margin) + (config.legend.fontsize - config.legend.rect[1]) / 2)
.attr("width", config.legend.rect[0])
.attr("height", config.legend.rect[1])
.attr("fill", d.color)
d3.select(this)
.append("text")
.attr("x", config.legend.rect[0] + 5)
.attr("y", i * (config.legend.fontsize + config.legend.margin))
.attr("text-anchor", "start")
.attr("dominant-baseline", "hanging")
.attr("fill", d.color)
.style("font-size", `${config.legend.fontsize}px`)
.text(`${d.name} (${d.value})`)
})
}
return svg.node()!.outerHTML
}
|