From 408604297021592ec3732ec568238f152db73909 Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Wed, 12 Aug 2020 18:58:21 +1000 Subject: [PATCH] switch the card counts to a pie graph --- ts/package.json | 1 + ts/src/stats/CardCounts.svelte | 76 +++++++++++++++++----------------- ts/src/stats/card-counts.ts | 64 ++++++++++++++-------------- 3 files changed, 72 insertions(+), 69 deletions(-) diff --git a/ts/package.json b/ts/package.json index ee3564634..2e9f5cef7 100644 --- a/ts/package.json +++ b/ts/package.json @@ -53,6 +53,7 @@ "@fluent/bundle": "^0.15.1", "d3-array": "^2.4.0", "d3-axis": "^1.0.12", + "d3-interpolate": "^1.4.0", "d3-scale": "^3.2.1", "d3-scale-chromatic": "^1.5.0", "d3-selection": "^1.4.2", diff --git a/ts/src/stats/CardCounts.svelte b/ts/src/stats/CardCounts.svelte index d04cf0613..4c9284905 100644 --- a/ts/src/stats/CardCounts.svelte +++ b/ts/src/stats/CardCounts.svelte @@ -10,19 +10,14 @@ let svg = null as HTMLElement | SVGElement | null; let bounds = defaultGraphBounds(); - bounds.height = 20; - bounds.marginTop = 0; - - let activeIdx: null | number = null; - function onHover(idx: null | number): void { - activeIdx = idx; - } + bounds.width = 225; + bounds.marginBottom = 0; let graphData = (null as unknown) as GraphData; let tableData = (null as unknown) as TableDatum[]; $: { graphData = gatherData(sourceData, i18n); - tableData = renderCards(svg as any, bounds, graphData, onHover); + tableData = renderCards(svg as any, bounds, graphData); } const total = i18n.tr(i18n.TR.STATISTICS_COUNTS_TOTAL_CARDS); @@ -33,8 +28,14 @@ transition: opacity 1s; } + .counts-outer { + display: flex; + justify-content: center; + } + .counts-table { display: flex; + flex-direction: column; justify-content: center; } @@ -45,45 +46,42 @@ .right { text-align: right; } - - .bold { - font-weight: bold; - }

{graphData.title}

- - - +
+ + + +
+ + {#each tableData as d, idx} + + + + + + {/each} -
-
+ + {d.label} + {d.count}{d.percent}
- {#each tableData as d, idx} - + - - + + - {/each} - - - - - - -
- - {d.label} + + {total} {d.count}{d.percent}{graphData.totalCards}
- - {total} - {graphData.totalCards} -
+ +
-
diff --git a/ts/src/stats/card-counts.ts b/ts/src/stats/card-counts.ts index 22fc614e1..833588d5c 100644 --- a/ts/src/stats/card-counts.ts +++ b/ts/src/stats/card-counts.ts @@ -12,6 +12,8 @@ import { schemeGreens, schemeBlues } from "d3-scale-chromatic"; import "d3-transition"; import { select } from "d3-selection"; import { scaleLinear } from "d3-scale"; +import { pie, arc } from "d3-shape"; +import { interpolate } from "d3-interpolate"; import { GraphBounds } from "./graphs"; import { cumsum } from "d3-array"; import { I18n } from "../i18n"; @@ -24,7 +26,6 @@ export interface GraphData { } export function gatherData(data: pb.BackendProto.GraphsOut, i18n: I18n): GraphData { - // fixme: handle preview cards const totalCards = data.cards.length; let newCards = 0; let young = 0; @@ -116,8 +117,7 @@ export interface TableDatum { export function renderCards( svgElem: SVGElement, bounds: GraphBounds, - sourceData: GraphData, - onHover: (idx: null | number) => void + sourceData: GraphData ): TableDatum[] { const summed = cumsum(sourceData.counts, (d) => d[1]); const data = Array.from(summed).map((n, idx) => { @@ -129,13 +129,42 @@ export function renderCards( total: n, } as SummedDatum; }); - // ensuring a non-zero range makes a better animation - // in the empty data case + // ensuring a non-zero range makes the percentages not break + // in an empty collection const xMax = Math.max(1, summed.slice(-1)[0]); const x = scaleLinear().domain([0, xMax]); const svg = select(svgElem); + const paths = svg.select(".counts"); + const pieData = pie()(sourceData.counts.map((d) => d[1])); + const radius = bounds.height / 2 - bounds.marginTop - bounds.marginBottom; + const arcGen = arc().innerRadius(0).outerRadius(radius); const trans = svg.transition().duration(600) as any; + paths + .attr("transform", `translate(${radius},${radius + bounds.marginTop})`) + .selectAll("path") + .data(pieData) + .join( + (enter) => + enter + .append("path") + .attr("fill", function (d, i) { + return barColour(i); + }) + .attr("d", arcGen as any), + function (update) { + return update.call((d) => + d.transition(trans).attrTween("d", (d) => { + const interpolator = interpolate( + { startAngle: 0, endAngle: 0 }, + d + ); + return (t): string => arcGen(interpolator(t)) as string; + }) + ); + } + ); + x.range([bounds.marginLeft, bounds.width - bounds.marginRight]); const tableData = data.map((d, idx) => { @@ -148,30 +177,5 @@ export function renderCards( } as TableDatum; }); - const updateBar = (sel: any): any => { - return sel - .on("mousemove", function (this: any, d: SummedDatum) { - onHover(d.idx); - }) - .transition(trans) - .attr("x", (d: SummedDatum) => x(d.total - d.count)) - .attr("width", (d: SummedDatum) => x(d.count) - x(0)); - }; - - svg.select("g.days") - .selectAll("rect") - .data(data) - .join( - (enter) => - enter - .append("rect") - .attr("height", 10) - .attr("y", bounds.marginTop) - .attr("fill", (d: SummedDatum): any => barColour(d.idx)) - .on("mouseout", () => onHover(null)) - .call((d) => updateBar(d)), - (update) => update.call((d) => updateBar(d)) - ); - return tableData; }