switch the card counts to a pie graph

This commit is contained in:
Damien Elmes 2020-08-12 18:58:21 +10:00
parent d079307536
commit 4086042970
3 changed files with 72 additions and 69 deletions

View file

@ -53,6 +53,7 @@
"@fluent/bundle": "^0.15.1", "@fluent/bundle": "^0.15.1",
"d3-array": "^2.4.0", "d3-array": "^2.4.0",
"d3-axis": "^1.0.12", "d3-axis": "^1.0.12",
"d3-interpolate": "^1.4.0",
"d3-scale": "^3.2.1", "d3-scale": "^3.2.1",
"d3-scale-chromatic": "^1.5.0", "d3-scale-chromatic": "^1.5.0",
"d3-selection": "^1.4.2", "d3-selection": "^1.4.2",

View file

@ -10,19 +10,14 @@
let svg = null as HTMLElement | SVGElement | null; let svg = null as HTMLElement | SVGElement | null;
let bounds = defaultGraphBounds(); let bounds = defaultGraphBounds();
bounds.height = 20; bounds.width = 225;
bounds.marginTop = 0; bounds.marginBottom = 0;
let activeIdx: null | number = null;
function onHover(idx: null | number): void {
activeIdx = idx;
}
let graphData = (null as unknown) as GraphData; let graphData = (null as unknown) as GraphData;
let tableData = (null as unknown) as TableDatum[]; let tableData = (null as unknown) as TableDatum[];
$: { $: {
graphData = gatherData(sourceData, i18n); 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); const total = i18n.tr(i18n.TR.STATISTICS_COUNTS_TOTAL_CARDS);
@ -33,8 +28,14 @@
transition: opacity 1s; transition: opacity 1s;
} }
.counts-outer {
display: flex;
justify-content: center;
}
.counts-table { .counts-table {
display: flex; display: flex;
flex-direction: column;
justify-content: center; justify-content: center;
} }
@ -45,26 +46,24 @@
.right { .right {
text-align: right; text-align: right;
} }
.bold {
font-weight: bold;
}
</style> </style>
<div class="graph" id="graph-card-counts"> <div class="graph" id="graph-card-counts">
<h1>{graphData.title}</h1> <h1>{graphData.title}</h1>
<div class="counts-outer">
<svg <svg
bind:this={svg} bind:this={svg}
viewBox={`0 0 ${bounds.width} ${bounds.height}`} viewBox={`0 0 ${bounds.width} ${bounds.height}`}
width={bounds.width}
height={bounds.height}
style="opacity: {graphData.totalCards ? 1 : 0}"> style="opacity: {graphData.totalCards ? 1 : 0}">
<g class="days" /> <g class="counts" />
</svg> </svg>
<div class="counts-table"> <div class="counts-table">
<table> <table>
{#each tableData as d, idx} {#each tableData as d, idx}
<tr class:bold={activeIdx === idx}> <tr>
<td> <td>
<span style="color: {d.colour};"></span> <span style="color: {d.colour};"></span>
{d.label} {d.label}
@ -74,7 +73,7 @@
</tr> </tr>
{/each} {/each}
<tr class:bold={activeIdx === null}> <tr>
<td> <td>
<span style="visibility: hidden;"></span> <span style="visibility: hidden;"></span>
{total} {total}
@ -82,8 +81,7 @@
<td class="right">{graphData.totalCards}</td> <td class="right">{graphData.totalCards}</td>
<td /> <td />
</tr> </tr>
</table> </table>
</div> </div>
</div>
</div> </div>

View file

@ -12,6 +12,8 @@ import { schemeGreens, schemeBlues } from "d3-scale-chromatic";
import "d3-transition"; import "d3-transition";
import { select } from "d3-selection"; import { select } from "d3-selection";
import { scaleLinear } from "d3-scale"; import { scaleLinear } from "d3-scale";
import { pie, arc } from "d3-shape";
import { interpolate } from "d3-interpolate";
import { GraphBounds } from "./graphs"; import { GraphBounds } from "./graphs";
import { cumsum } from "d3-array"; import { cumsum } from "d3-array";
import { I18n } from "../i18n"; import { I18n } from "../i18n";
@ -24,7 +26,6 @@ export interface GraphData {
} }
export function gatherData(data: pb.BackendProto.GraphsOut, i18n: I18n): GraphData { export function gatherData(data: pb.BackendProto.GraphsOut, i18n: I18n): GraphData {
// fixme: handle preview cards
const totalCards = data.cards.length; const totalCards = data.cards.length;
let newCards = 0; let newCards = 0;
let young = 0; let young = 0;
@ -116,8 +117,7 @@ export interface TableDatum {
export function renderCards( export function renderCards(
svgElem: SVGElement, svgElem: SVGElement,
bounds: GraphBounds, bounds: GraphBounds,
sourceData: GraphData, sourceData: GraphData
onHover: (idx: null | number) => void
): TableDatum[] { ): TableDatum[] {
const summed = cumsum(sourceData.counts, (d) => d[1]); const summed = cumsum(sourceData.counts, (d) => d[1]);
const data = Array.from(summed).map((n, idx) => { const data = Array.from(summed).map((n, idx) => {
@ -129,13 +129,42 @@ export function renderCards(
total: n, total: n,
} as SummedDatum; } as SummedDatum;
}); });
// ensuring a non-zero range makes a better animation // ensuring a non-zero range makes the percentages not break
// in the empty data case // in an empty collection
const xMax = Math.max(1, summed.slice(-1)[0]); const xMax = Math.max(1, summed.slice(-1)[0]);
const x = scaleLinear().domain([0, xMax]); const x = scaleLinear().domain([0, xMax]);
const svg = select(svgElem); 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; 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]); x.range([bounds.marginLeft, bounds.width - bounds.marginRight]);
const tableData = data.map((d, idx) => { const tableData = data.map((d, idx) => {
@ -148,30 +177,5 @@ export function renderCards(
} as TableDatum; } 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; return tableData;
} }