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 |
x2
x2
x2
x3
x3
x12
x3
x3
x12
x3
x3
x3
x3
x20
x3
x12
x12
x3
x3
x3
x3
x3
x3
x3
x3
x12
x12
x3
x3
x3
x3
x3
x36
x48
x5
x5
x5
x5
x5
x5
x5
x5
x5
x3
x3
x3
x3
x3
x3
x3
x3
x3
x3
x3
x3
x3
x3
x3
x3
x3 |
|
import { d3, type d3arg, mkconfig, mksvg, type options } from "./_graph.ts"
import { pick } from "@std/collections"
export type { options }
export function diff(datalist: Record<PropertyKey, { data: { date: Date; added: number; deleted: number; changed: number }[] }>, options: Pick<options, "title" | "width" | "height"> & { opacity?: number } = {}): string {
const margin = 5
const offset = 34
const config = mkconfig(pick(options, ["width", "height"]))
const { width, height, title } = config
const opacity = options.opacity ?? config.areas.opacity
const svg = mksvg({ width, height })
const K = Object.keys(datalist)
const V = Object.values(datalist).flatMap(({ data }) => data)
const start = new Date(Math.min(...V.map(({ date }) => Number(date))))
const end = new Date(Math.max(...V.map(({ date }) => Number(date))))
const extremum = Math.max(...V.flatMap(({ added, deleted, changed }) => [added + changed, deleted + changed]))
const x = d3.scaleTime()
.domain([start, end])
.range([margin + offset, width - (offset + margin)])
svg.append("g")
.attr("transform", `translate(0,${height - (offset + margin)})`)
.call(d3.axisBottom(x))
.selectAll("text")
.attr("transform", "translate(-5,5) rotate(-45)")
.style("text-anchor", "end")
.style("font-size", `${config.axis.fontsize}px`)
const y = d3.scaleLinear()
.domain([extremum, -extremum])
.range([margin, height - (offset + margin)])
svg.append("g")
.attr("transform", `translate(${margin + offset},0)`)
.call(d3.axisLeft(y).tickFormat(d3.format(".2s")))
.selectAll("text")
.style("font-size", `${config.axis.fontsize}px`)
for (const { type, sign, fill } of [{ type: "added", sign: +1, fill: "var(--diff-addition)" }, { type: "deleted", sign: -1, fill: "var(--diff-deletion)" }] as const) {
const values = Object.entries(datalist).flatMap(([name, { data }]) => data.flatMap(({ date, ...diff }) => ({ date, [name]: sign * (diff[type] + diff.changed) })))
svg
.append("g")
.selectAll("g")
.data(d3.stack().keys(K)(values as d3arg))
.join("path")
.attr("d", d3.area().x((d: d3arg) => x(d.data.date)).y0((d: number[]) => y(d[0] || 0)).y1((d: number[]) => y(d[1] || 0)) as d3arg)
.attr("fill", fill)
.attr("fill-opacity", opacity)
}
if (title) {
svg.append("text")
.attr("class", "title")
.attr("x", width / 2)
.attr("y", config.title.fontsize)
.attr("text-anchor", "middle")
.attr("font-family", "sans-serif")
.attr("stroke", "rgba(88, 166, 255, .05)")
.attr("stroke-linejoin", "round")
.attr("stroke-width", 4)
.attr("paint-order", "stroke fill")
.style("font-size", `${config.title.fontsize}px`)
.attr("fill", config.title.color)
.text(title)
}
return svg.node()!.outerHTML
}
|