admin: draw the overview charts with visx
the hand-written scale, tick, stacking and arc math is replaced by visx 4.0.0 primitives; rendering, colours and themes stay the app's own. all four charts share one hover treatment: the client chart gains the tooltip and dimming the query timeline had, the donuts gain both, an open tooltip follows a data refresh instead of going stale, and it retires when the window rolls. the timeline's third series is named allowed instead of other, and the client chart's other aggregate disappears from a window where it counted nothing. licenses gain the isc text for the bundled d3 modules.
This commit is contained in:
@@ -0,0 +1,149 @@
|
||||
import { render } from "@testing-library/react";
|
||||
import {
|
||||
CHART_HEIGHT,
|
||||
ChartFrame,
|
||||
bandPaddingInner,
|
||||
bandScale,
|
||||
labelTickValues,
|
||||
plotArea,
|
||||
useMeasuredWidth,
|
||||
valueScale,
|
||||
valueTicks,
|
||||
} from "./chartKit";
|
||||
|
||||
describe("useMeasuredWidth", () => {
|
||||
/**
|
||||
* jsdom lays nothing out, so `clientWidth` is 0 there and a chart scaled to it
|
||||
* would draw a zero-width plot. Every chart test depends on this fallback.
|
||||
*/
|
||||
test("an unmeasurable container falls back to 640", () => {
|
||||
function Probe() {
|
||||
const [ref, width] = useMeasuredWidth();
|
||||
return <div ref={ref} data-testid="probe" data-width={width} />;
|
||||
}
|
||||
const { getByTestId } = render(<Probe />);
|
||||
expect(getByTestId("probe").getAttribute("data-width")).toBe("640");
|
||||
});
|
||||
});
|
||||
|
||||
describe("valueScale", () => {
|
||||
test("the data maximum is nicened outward and the ticks land on round numbers", () => {
|
||||
const scale = valueScale(1780, [240, 0]);
|
||||
expect(scale.domain()).toEqual([0, 1800]);
|
||||
expect(scale.ticks(5)).toEqual([0, 500, 1000, 1500]);
|
||||
expect(valueTicks(scale)).toEqual([0, 500, 1000, 1500]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("bandPaddingInner", () => {
|
||||
/**
|
||||
* The gap is a constant number of pixels, so the fraction it takes of one slot
|
||||
* has to fall as the slots get wider. 24 hourly columns in a 1388px plot is
|
||||
* the 24h window on a full-width panel.
|
||||
*/
|
||||
test("the fraction is two pixels per column of the plot's width", () => {
|
||||
expect(bandPaddingInner(24, 1388)).toBeCloseTo((2 * 24) / 1388, 12);
|
||||
expect(bandPaddingInner(24, 1388)).toBeCloseTo(0.034582, 6);
|
||||
});
|
||||
|
||||
/**
|
||||
* Past the cap the gap would be most of the slot and the columns would vanish
|
||||
* into slivers, so it stops at half the slot and the bars stay visible.
|
||||
*/
|
||||
test("the gap never takes more than half a slot", () => {
|
||||
expect(bandPaddingInner(1000, 100)).toBe(0.5);
|
||||
});
|
||||
|
||||
test("no columns and no width mean no gap to compute", () => {
|
||||
expect(bandPaddingInner(0, 640)).toBe(0);
|
||||
expect(bandPaddingInner(24, 0)).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("labelTickValues", () => {
|
||||
/** A week of hourly buckets in a panel the width of a laptop window. */
|
||||
test("labels thin out to one every 21 buckets at 748px of plot", () => {
|
||||
const timestamps = Array.from({ length: 168 }, (_, i) => i * 3600);
|
||||
expect(labelTickValues(timestamps, 748)).toEqual([0, 75600, 151200, 226800, 302400, 378000, 453600, 529200]);
|
||||
});
|
||||
|
||||
test("every bucket is labelled when they all fit", () => {
|
||||
expect(labelTickValues([0, 3600, 7200], 748)).toEqual([0, 3600, 7200]);
|
||||
});
|
||||
});
|
||||
|
||||
/** The frame on its own, at the width the charts draw at in jsdom. */
|
||||
function renderFrame() {
|
||||
const plot = plotArea(640);
|
||||
const timestamps = [0, 3600, 7200];
|
||||
const yScale = valueScale(100, [plot.bottom, plot.y]);
|
||||
const yTicks = valueTicks(yScale);
|
||||
const { container } = render(
|
||||
<svg width={640} height={CHART_HEIGHT}>
|
||||
<ChartFrame
|
||||
plot={plot}
|
||||
yScale={yScale}
|
||||
yTicks={yTicks}
|
||||
xScale={bandScale(timestamps, plot)}
|
||||
xTickValues={timestamps}
|
||||
bucketSeconds={3600}
|
||||
/>
|
||||
</svg>,
|
||||
);
|
||||
return { container, plot, yTicks };
|
||||
}
|
||||
|
||||
describe("ChartFrame", () => {
|
||||
test("the value axis is bare: no axis line, no tick marks, labels 6px left of the plot", () => {
|
||||
const { container, plot, yTicks } = renderFrame();
|
||||
|
||||
const axis = container.querySelector(".visx-axis-left") as SVGGElement;
|
||||
expect(axis.getAttribute("transform")).toBe(`translate(${plot.x}, 0)`);
|
||||
expect(axis.querySelector("line")).toBeNull();
|
||||
|
||||
const labels = Array.from(axis.querySelectorAll("text"));
|
||||
expect(labels.map((label) => label.textContent)).toEqual(yTicks.map(String));
|
||||
for (const label of labels) {
|
||||
expect(label.getAttribute("x")).toBe("0");
|
||||
expect(label.getAttribute("dx")).toBe("-6px");
|
||||
expect(label.getAttribute("text-anchor")).toBe("end");
|
||||
// The label sits on the tick, centred: visx's own 0.25em nudge is
|
||||
// cancelled so it does not double up with the middle baseline.
|
||||
expect(label.getAttribute("dy")).toBe("0");
|
||||
expect(label.getAttribute("dominant-baseline")).toBe("middle");
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* The time axis keeps its baseline and drops its tick marks, and the labels
|
||||
* are placed by the group transform plus `dy` rather than by the font-size
|
||||
* guess visx would otherwise make.
|
||||
*/
|
||||
test("the time axis keeps only its baseline, and its labels sit 16px below it", () => {
|
||||
const { container, plot } = renderFrame();
|
||||
|
||||
const axis = container.querySelector(".visx-axis-bottom") as SVGGElement;
|
||||
expect(axis.getAttribute("transform")).toBe(`translate(0, ${plot.bottom})`);
|
||||
|
||||
const lines = Array.from(axis.querySelectorAll("line"));
|
||||
expect(lines).toHaveLength(1);
|
||||
expect(lines[0].getAttribute("class")).toContain("visx-axis-line");
|
||||
|
||||
for (const label of Array.from(axis.querySelectorAll("text"))) {
|
||||
expect(label.getAttribute("y")).toBe("0");
|
||||
expect(label.getAttribute("dy")).toBe("16px");
|
||||
expect(label.getAttribute("text-anchor")).toBe("middle");
|
||||
}
|
||||
});
|
||||
|
||||
test("every grid line has a labelled tick on it and no tick floats without a line", () => {
|
||||
const { container } = renderFrame();
|
||||
|
||||
const gridY = Array.from(container.querySelectorAll(".visx-rows line")).map((line) => line.getAttribute("y1"));
|
||||
const labelY = Array.from(container.querySelectorAll(".visx-axis-left text")).map((label) =>
|
||||
label.getAttribute("y"),
|
||||
);
|
||||
expect(gridY.length).toBeGreaterThan(0);
|
||||
expect(gridY).toEqual(labelY);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user