All files / graph / pie.ts

100.00% Branches 10/10
100.00% Functions 1/1
100.00% Lines 74/74
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
































































































// Imports
import { d3, type d3arg, type d3data, mkcolor, mkconfig, mksvg, type options } from "./_graph.ts"
import { pick } from "@std/collections"
export type { options }

/**
 * Renders a pie chart.
 *
 * ```ts
 * pie({
 *   A: { data: 0.6 },
 *   B: { data: 0.2 },
 *   C: { data: 0.1 },
 * }, { legend: true })
 * ```ts
 */
export function pie(datalist: Record<PropertyKey, { color?: string; data: number }>, options: Pick<options, "width" | "height" | "legend"> = {}): string {
  // Create SVG
  const config = mkconfig(pick(options, ["width", "height"]))
  const { margin, width, height } = config
  const radius = Math.min(width, height) / 2
  const svg = mksvg({ width, height })

  // Prepare data
  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)

  // Render graph areas
  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}`)

  // Render graph texts
  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)

  // Set legend
  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})`)
      })
  }

  // Render SVG
  return svg.node()!.outerHTML
}