← Files DataARCHIVED FILE
templates/data-app/base/tests/chart-annotation-layout.test.mjs
30.9 KB · Sep 30, 2026 · 23:19 UTC
import assert from "node:assert/strict";
import test from "node:test";
import { chartAnnotationLayoutTokens as tokens, layoutChartAnnotations, chartAnnotationArrow } from "../src/charting/chart-annotation-layout.js";
const chartAnnotationReferenceArrow = (box, axis, labels = [], obstacles = []) =>
chartAnnotationArrow(box, labels, obstacles, axis);
const measureText = (text) => [...text].length * 6;
const layout = (items, plot) => layoutChartAnnotations(items, plot, { measureText });
const item = (id, x, y, label = `Reviewed ${id}`, kind = "point", preferred) =>
({ id, kind, label, anchor: { x, y }, ...(preferred ? { preferred } : {}) });
const desktop = { x: 40, y: 20, width: 720, height: 280 };
test("reference connectors stay placeable across the stretched-card height feedback boundary", () => {
// Recorded House chart geometry: showing a fallback note shrinks its SVG
// from 250px to 220px. Both sizes fit the same complete two-line label, but
// only the taller size rejects the center arrowhead's contact with a bar.
const threshold = 305.9384615384615;
const box = { x: 342.893075, y: 10, width: 57, height: 42, anchor: { x: threshold, y: 31 } };
const before = structuredClone(box);
for (const [height, barY, barHeight, valueY] of [
[220, [31.6, 121.6], 47, [47.86, 137.86]],
[250, [35.2, 140.2], 55, [55.46, 160.46]],
]) {
const obstacles = [
{ x: 98, y: barY[0], width: 209.8462, height: barHeight },
{ x: 98, y: barY[1], width: 205.0769, height: barHeight },
{ x: threshold, y: 10, width: 0, height: height - 40 },
{ x: 312.8462, y: valueY[0], width: 22.046875, height: 14 },
{ x: 308.0769, y: valueY[1], width: 20.2265625, height: 14 },
];
const preferred = chartAnnotationArrow(box, [], obstacles);
assert.equal(Boolean(preferred), height === 220, "The original single-point connector reproduces the reversal");
const arrow = chartAnnotationReferenceArrow(box, "y", [], obstacles);
assert.ok(arrow, "Both responsive heights keep the full label on the plot instead of toggling the note");
assert.equal(arrow.end.x, threshold, "The target remains the exact reviewed threshold line");
assert.ok(arrow.end.y >= box.y && arrow.end.y <= box.y + box.height);
assert.deepEqual(arrow, chartAnnotationArrow({ ...box, anchor: arrow.end }, [], obstacles),
"Alternate reference points pass the same evidence and arrowhead clearance checks");
if (preferred) assert.deepEqual(arrow, preferred, "Already-safe connectors retain their exact geometry");
}
assert.deepEqual(box, before, "The label, reviewed anchor, and caller geometry remain unchanged");
});
test("reference fallback preserves horizontal thresholds, other labels, and genuinely blocked notes", () => {
const box = { x: 60, y: 80, width: 80, height: 42, anchor: { x: 20, y: 101 } };
const line = { x: 20, y: 0, width: 0, height: 200 };
const blocker = { x: 30, y: 98, width: 20, height: 6 };
assert.equal(chartAnnotationArrow(box, [], [line, blocker]), null);
const vertical = chartAnnotationReferenceArrow(box, "y", [], [line, blocker]);
assert.ok(vertical);
const transpose = ({ x, y, width, height, anchor }) => ({ x: y, y: x, width: height, height: width,
...(anchor ? { anchor: { x: anchor.y, y: anchor.x } } : {}) });
const horizontal = chartAnnotationReferenceArrow(transpose(box), "x", [], [transpose(line), transpose(blocker)]);
assert.ok(horizontal);
assert.equal(horizontal.end.y, 20, "Horizontal references preserve the exact value coordinate too");
assert.equal(horizontal.end.x, vertical.end.y);
const wall = { x: 30, y: 70, width: 20, height: 70 };
assert.equal(chartAnnotationReferenceArrow(box, "y", [], [line, wall]), null,
"No safe reference connector still requires the complete fallback note");
assert.equal(chartAnnotationReferenceArrow(box, "y", [wall], [line]), null,
"Alternative connectors cannot pass through another annotation");
assert.equal(chartAnnotationReferenceArrow({ ...box, anchor: { x: 40, y: 101 } }, "y"), null,
"Nearby text remains undecorated rather than gaining unnecessary arrows");
});
function assertSafe(result, inputs, plot) {
for (const box of result) {
assert.ok([box.x, box.y, box.width, box.height, box.anchor.x, box.anchor.y].every(Number.isFinite));
assert.ok(box.x >= plot.x && box.y >= plot.y);
assert.ok(box.x + box.width <= plot.x + plot.width + 1e-7);
assert.ok(box.y + box.height <= plot.y + plot.height + 1e-7);
assert.deepEqual(box.anchor, inputs.find(({ id }) => id === box.id).anchor, "The connector retains the exact reviewed anchor");
assert.equal(Object.hasOwn(box, "collapsed"), false, "There are no compact markers");
assert.ok(box.lines.length >= 1 && box.lines.length <= 3);
assert.ok(box.width <= tokens.maxWidth);
assert.equal(box.height, box.lines.length * tokens.lineHeight + tokens.paddingY * 2);
assert.ok(box.lines.every((line) => measureText(line) + tokens.paddingX * 2 <= box.width));
}
for (let index = 0; index < result.length; index++) for (const other of result.slice(index + 1)) {
const box = result[index];
assert.ok(box.x + box.width + tokens.gap <= other.x + 1e-7
|| other.x + other.width + tokens.gap <= box.x + 1e-7
|| box.y + box.height + tokens.gap <= other.y + 1e-7
|| other.y + other.height + tokens.gap <= box.y + 1e-7,
`Placements ${box.id} and ${other.id} must not overlap`);
}
}
test("desktop places concise full labels for every annotation kind", () => {
assert.equal(tokens.fontSize, 14);
const items = [item("target", 710, 160, "Operating target", "benchmark"),
item("launch", 160, 25, "Release recorded", "event"),
item("window", 355, 28, "Review window", "range"),
item("high", 530, 240, "Observed high", "point")];
const result = layout(items, desktop);
assert.equal(result.length, 4);
assert.deepEqual(result.map(({ lines }) => lines.join(" ")), items.map(({ label }) => label));
assertSafe(result, items, desktop);
});
test("horizontal benchmark explanations and unequal text lines align to the plot edge",()=>{
const plot={x:40,y:20,width:500,height:240};
const entry=item("capacity",528,120,"Above 180 parcels/day requires written approval.","benchmark");
const line={x:plot.x,y:120,width:plot.width,height:0};
const run=obstacles=>layoutChartAnnotations([entry],plot,{measureText,obstacles:[line,...obstacles]});
const [right]=run([]);
assert.equal(right.x+right.width,plot.x+plot.width);
assert.equal(right.textAnchor,"end");
assert.equal(right.lines.length,2);assert.notEqual(measureText(right.lines[0]),measureText(right.lines[1]));
assert.deepEqual(right.anchor,entry.anchor);
const [left]=run([{x:plot.x+plot.width-220,y:plot.y,width:220,height:plot.height}]);
assert.equal(left.x,plot.x);assert.equal(left.textAnchor,"start");
const [nearLeft]=run([{x:plot.x+plot.width-220,y:plot.y,width:220,height:180}]);
assert.equal(nearLeft.textAnchor,"start","Nearby left context beats a right-edge pocket far below its threshold");
assert.deepEqual(run([{x:plot.x,y:plot.y,width:20,height:plot.height},
{x:plot.x+plot.width-20,y:plot.y,width:20,height:plot.height}]),[],
"When neither edge fits, use the full note rather than a floating middle column");
});
test("vertical benchmark lines retain their threshold association rather than forced horizontal edge placement",()=>{
const entry={...item("vertical",260,32,"Approval threshold","benchmark","right"),horizontal:true};
const [box]=layoutChartAnnotations([entry],{x:40,y:20,width:500,height:240},
{measureText,obstacles:[{x:260,y:20,width:0,height:240}]});
assert.ok(box.x>entry.anchor.x&&box.x+box.width<540);
assert.equal(box.textAnchor,undefined);
assert.deepEqual(box.anchor,entry.anchor);
});
test("preferred sides support horizontal and vertical chart orientation", () => {
const plot = { x: 0, y: 0, width: 500, height: 300 };
const left = item("left", 250, 150, "Target", "point", "left");
const below = item("below", 250, 150, "Launch", "event", ["bottom", "right"]);
const [leftBox] = layout([left], plot);
const [belowBox] = layout([below], plot);
assert.ok(leftBox.x + leftBox.width < left.anchor.x);
assert.ok(belowBox.y > below.anchor.y);
assertSafe([leftBox], [left], plot);
assertSafe([belowBox], [below], plot);
});
test("a concise contextual phrase prefers balanced two-line text beside its reference", () => {
const event = item("desk", 240, 150, "Second intake desk opened", "event");
const [box] = layout([event], desktop);
assert.deepEqual(box.lines, ["Second intake", "desk opened"]);
assert.equal(box.x - event.anchor.x, tokens.anchorGap);
assert.equal(chartAnnotationArrow(box), null, "Nearby two-line text does not need an arrow");
assertSafe([box], [event], desktop);
});
test("range text starts 12px inside the band's leading edge near the plot top", () => {
const entry = { ...item("maintenance",300,160,"Planned maintenance window","range"),
range: {axis:"x",start:200,end:400} };
const before = structuredClone(entry);
const [box] = layout([entry],desktop);
assert.equal(box.x+tokens.paddingX,212,"The text starts inside the band, not centered around it");
assert.equal(box.y,desktop.y+12);
assert.equal(box.lines.join(" "),entry.label);
assert.equal(box.lines.length,2);
assert.deepEqual(entry,before,"Layout does not revise the reviewed range or its center anchor");
assertSafe([box],[entry],desktop);
});
test("horizontal and reversed projected ranges use the physical upper leading edge", () => {
for (const [axis,start,end,x,y] of [
["x",400,200,212,32],
["y",120,220,52,132],
["y",220,120,52,132],
]) {
const entry={...item("range",300,160,"Reviewed maintenance window","range"),range:{axis,start,end}};
const [box]=layout([entry],desktop);
assert.equal(box.x,x);
assert.equal(box.y,y);
assert.equal(box.lines.join(" "),entry.label);
assertSafe([box],[entry],desktop);
}
});
test("a narrow band keeps the text start inside while allowing complete text beyond the band", () => {
const entry={...item("narrow",206,160,"Planned maintenance window","range"),
range:{axis:"x",start:200,end:212}};
const [box]=layout([entry],desktop);
assert.equal(box.x,206,"For a band narrower than24px, use half its width as the inset");
assert.ok(box.x+box.width>212,"Do not truncate the label to the shaded band's width");
assert.equal(box.lines.join(" "),entry.label);
assertSafe([box],[entry],desktop);
});
test("range text searches upward along its leading edge before accepting a centered label", () => {
// The warehouse browser fixture's two cubic shoulders surround a two-day
// closure. Its76px three-line label fits above the right shoulder only when
// moved upward from the usual12px inset; the119px two-line shape cannot fit.
const plot={x:65,y:10,width:641,height:260};
const entry={...item("closure",385.5,140,"Warehouse closed for stock count","range"),
range:{axis:"x",start:356.364,end:414.636}};
const curves=[[[298.091,21.375],[317.515,21.375],[336.939,270],[356.364,270]],
[[414.636,270],[434.061,270],[453.485,30.583],[472.909,26.25]]];
const obstacles=curves.flatMap((points)=>{
const sample=(t)=>[0,1].map((axis)=>(1-t)**3*points[0][axis]+3*(1-t)**2*t*points[1][axis]
+3*(1-t)*t**2*points[2][axis]+t**3*points[3][axis]);
return Array.from({length:80},(_,index)=>{
const a=sample(index/80),b=sample((index+1)/80);
return {x:Math.min(a[0],b[0])-2,y:Math.min(a[1],b[1])-2,
width:Math.abs(a[0]-b[0])+4,height:Math.abs(a[1]-b[1])+4};
});
});
obstacles.push({x:354.364,y:268,width:62.272,height:4},{x:381.5,y:136,width:8,height:8});
const widths={Warehouse:72.365234375,"Warehouse closed":118.822265625,
"closed for":64.3671875,"stock count":75.7626953125,"for stock count":97.4599609375,
"closed for stock":103.3525390625,count:36.77734375};
const [box]=layoutChartAnnotations([entry],plot,{measureText:(text)=>widths[text]??text.length*7,obstacles});
assert.equal(box.x,entry.range.start+12);
assert.deepEqual(box.lines,["Warehouse","closed for","stock count"]);
assert.equal(box.width,76);
assert.equal(box.height,60);
assert.ok(box.y>=plot.y && box.y<plot.y+12,"Move along the leading edge, not back toward the band's center");
assert.deepEqual(box.anchor,entry.anchor);
for(const mark of obstacles) assert.ok(box.x+box.width+tokens.gap<=mark.x+1e-7
|| mark.x+mark.width+tokens.gap<=box.x+1e-7 || box.y+box.height+tokens.gap<=mark.y+1e-7
|| mark.y+mark.height+tokens.gap<=box.y+1e-7,"Full text must remain clear of the actual curved shoulders");
});
test("horizontal range text searches along the upper edge without shifting its band inset", () => {
const plot={x:40,y:20,width:400,height:220};
const entry={...item("horizontal",240,130,"Recorded inspection window","range"),
range:{axis:"y",start:100,end:160}};
const obstacles=[{x:40,y:100,width:110,height:80}];
const [box]=layoutChartAnnotations([entry],plot,{measureText,obstacles});
assert.equal(box.y,112);
assert.ok(box.x>=158-1e-7,"Search along the edge past the blocking mark");
assertSafe([box],[entry],plot);
});
test("an occluded or right-edge range preference falls back without covering data or leaving the plot", () => {
const entry={...item("range",300,160,"Planned maintenance window","range"),
range:{axis:"x",start:200,end:400}};
const obstacles=[{x:200,y:20,width:180,height:100}];
const [box]=layoutChartAnnotations([entry],desktop,{measureText,obstacles});
assert.ok(box.x!==212 || box.y!==32,"The preferred band position is not mandatory when occluded");
const [mark]=obstacles;
assert.ok(box.x+box.width+tokens.gap<=mark.x+1e-7 || mark.x+mark.width+tokens.gap<=box.x+1e-7
|| box.y+box.height+tokens.gap<=mark.y+1e-7 || mark.y+mark.height+tokens.gap<=box.y+1e-7);
assertSafe([box],[entry],desktop);
const edge={...entry,range:{axis:"x",start:744,end:758},anchor:{x:751,y:160}};
const [edgeBox]=layout([edge],desktop);
assert.notEqual(edgeBox.x,751,"A preferred origin with no room for full text is rejected, not allowed to overflow");
assert.equal(edgeBox.lines.join(" "),edge.label);
assertSafe([edgeBox],[edge],desktop);
assert.deepEqual(layoutChartAnnotations([entry],desktop,{measureText,obstacles:[{...desktop}]}),[],
"When no full label fits anywhere, retain the existing full-text figure-note fallback");
});
test("range preferences respect earlier labels and leave point placement unchanged", () => {
const point=item("point",110,150,"Measured peak","point");
const first={...item("first",300,160,"Planned maintenance window","range"),range:{axis:"x",start:200,end:400}};
const second={...first,id:"second",label:"Inspection window recorded"};
const inputs=[point,first,second];
const result=layout(inputs,desktop);
assert.equal(result.length,3);
assert.deepEqual(result[0],layout([point],desktop)[0]);
assertSafe(result,inputs,desktop);
assert.ok(result[2].x!==result[1].x || result[2].y!==result[1].y);
assert.deepEqual(layout([{...point,range:first.range}],desktop),layout([point],desktop),
"Only ranges can opt into the projected-band placement preference");
for(const range of [{axis:"x",start:NaN,end:400},{axis:"z",start:200,end:400},
{axis:"x",start:200,end:200},{axis:"x",start:800,end:900}])
assert.deepEqual(layout([{...first,range}],desktop),layout([{...first,range:undefined}],desktop),
"Invalid or out-of-plot band geometry cannot force an unsafe preferred origin");
});
test("a one-line-only slot preserves the complete phrase rather than forcing two lines outside", () => {
const plot = { x: 10, y: 20, width: 300, height: 26 };
const event = item("short", 60, 30, "Recorded maintenance", "event");
const [box] = layout([event], plot);
assert.deepEqual(box.lines, [event.label]);
assertSafe([box], [event], plot);
});
test("a compact nearby wrap wins over a roomier distant placement", () => {
const plot = { x: 0, y: 0, width: 500, height: 220 };
const event = item("near", 260, 100, "Second intake desk opened", "event");
const obstacles = [{ x: 256, y: 0, width: 8, height: 220 }, { x: 366, y: 0, width: 134, height: 220 }];
const [box] = layoutChartAnnotations([event], plot, { measureText, obstacles });
assert.deepEqual(box.lines, ["Second intake", "desk opened"]);
assert.equal(box.x, 272);
assert.equal(chartAnnotationArrow(box), null);
assertSafe([box], [event], plot);
});
test("a complete two-line phrase gets an interior search before a three-line nearby wrap", () => {
const plot = { x: 0, y: 0, width: 500, height: 220 };
const event = item("period", 260, 100, "Warehouse closed for stock count", "event");
const obstacles = [{ x: 256, y: 0, width: 8, height: 220 }, { x: 140, y: 40, width: 116, height: 140 },
{ x: 346, y: 0, width: 154, height: 220 }];
const [box] = layoutChartAnnotations([event], plot, { measureText, obstacles });
assert.equal(box.lines.length, 2);
assert.equal(box.lines.join(" "), event.label);
assert.ok(box.x + box.width <= 132);
assertSafe([box], [event], plot);
});
test("natural two-line text wins over breaking a word beside the anchor", () => {
const plot = { x: 0, y: 0, width: 320, height: 200 };
const event = item("words", 68, 91, "Operational rollout", "event");
const obstacles = [{ x: 169, y: 131, width: 88, height: 64 }, { x: 65, y: 52, width: 62, height: 71 },
{ x: 151, y: 34, width: 25, height: 70 }, { x: 130, y: 72, width: 56, height: 28 }];
const [box] = layoutChartAnnotations([event], plot, { measureText, obstacles });
assert.deepEqual(box.lines, ["Operational", "rollout"]);
assertSafe([box], [event], plot);
});
test("colliding narrow-plot anchors place only full text and leave the rest for figure notes", () => {
const plot = { x: 7, y: 11, width: 120, height: 160 };
const items = Array.from({ length: 8 }, (_, index) => item(String(index), 67, 91,
`Reviewed observation ${index}`, ["benchmark", "event", "range", "point"][index % 4]));
const result = layout(items, plot);
assert.ok(result.length > 0 && result.length < 8, "Unplaceable labels go to full-text figure notes");
assertSafe(result, items, plot);
assert.deepEqual(layout(items, plot), result, "Greedy placement is deterministic");
});
test("eight separated desktop anchors retain full labels", () => {
const plot = { x: 0, y: 0, width: 960, height: 420 };
const items = Array.from({ length: 8 }, (_, index) => item(String(index),
110 + (index % 4) * 240, 105 + Math.floor(index / 4) * 210, `Observation ${index}`));
const result = layout(items, plot);
assert.equal(result.length, 8);
assertSafe(result, items, plot);
});
test("wrapping preserves complete words and long tokens within three lines", () => {
const items = [item("words", 180, 150, "A reviewed comparison period"),
item("token", 540, 150, "ABCDEFGHIJKLMNOPQRSTUVWXYZ")];
const result = layout(items, desktop);
assert.equal(result.length, 2);
assert.ok(result.every(({ lines }) => lines.length >= 1 && lines.length <= 3));
assert.equal(result[0].lines.join(" "), items[0].label);
assert.equal(result[1].lines.join(""), items[1].label);
assertSafe(result, items, desktop);
const long = [item("long", 300, 100, "x".repeat(160))];
const collapsed = layout(long, desktop);
assert.deepEqual(collapsed, [], "Long text moves to the full-text figure note, never an ellipsis or number");
assertSafe(collapsed, long, desktop);
});
test("unplaceable full labels are omitted from SVG, not replaced by badges", () => {
const plot = { x: 0, y: 0, width: 170, height: 70 };
const items = Array.from({ length: 8 }, (_, index) => item(String(index), 85, 35, "x".repeat(160)));
assert.deepEqual(layout(items, plot), []);
assert.deepEqual(layout([item("one", 13, 14)], { x: 3, y: 4, width: 20, height: 20 }), []);
});
test("failed full-plot searches preserve smaller wraps and reset with painted geometry", () => {
const plot = { x: 0, y: 0, width: 300, height: 100 };
const large = item("large", 150, 70, "A supported story about the business today");
for (const [width, text] of [[50, "Fact"], [300, "Short contextual fact"]]) {
const obstacles = [{ x: 0, y: 32, width: 300, height: 68 },
{ x: width + tokens.gap, y: 0, width: 300, height: 100 }];
const small = item("small", 20, 20, text);
const options = { measureText, obstacles };
assert.deepEqual(layoutChartAnnotations([large], plot, options), []);
const expected = layoutChartAnnotations([small], plot, options);
assert.equal(expected.length, 1, "A shorter or narrower complete label still fits");
assert.deepEqual(layoutChartAnnotations([large, small], plot, options), expected);
assertSafe(expected, [small], plot);
}
assert.equal(layout([large], plot).length, 1, "A later render searches its own painted geometry");
});
test("invalid geometry never leaks nonfinite or out-of-plot positions", () => {
const valid = item("valid", 50, 50, "Valid");
const plot = { x: 0, y: 0, width: 100, height: 100 };
for (const invalid of [null, {}, { ...plot, x: NaN }, { ...plot, width: Infinity },
{ ...plot, width: 19 }, { ...plot, height: -1 }, { ...plot, x: 1e308, width: 1e308 }])
assert.deepEqual(layout([valid], invalid), []);
const inputs = [item("outside", -1, 50), item("nan", NaN, 10), item("inf", 10, Infinity),
{ ...valid, id: "empty", label: " " }, valid, valid];
const result = layout(inputs, plot);
assert.deepEqual(result.map(({ id }) => id), ["valid"]);
assertSafe(result, inputs, plot);
});
test("varied narrow-plot anchors keep every placed full label finite and separated", () => {
let seed = 17;
const random = () => ((seed = (seed * 1664525 + 1013904223) >>> 0) / 4294967296);
const plot = { x: 7, y: 11, width: 120, height: 160 };
for (let trial = 0; trial < 32; trial++) {
const items = Array.from({ length: 8 }, (_, index) => item(String(index),
plot.x + random() * plot.width, plot.y + random() * plot.height, `Reviewed observation ${index}`));
const result = layout(items, plot);
assert.ok(result.length <= 8);
assertSafe(result, items, plot);
assert.deepEqual(layout(items, plot), result);
}
});
test("custom text measurement and frozen inputs stay pure", () => {
const plot = Object.freeze({ x: 0, y: 0, width: 400, height: 200 });
const entry = Object.freeze({ ...item("pure", 200, 100, "Measured label"), anchor: Object.freeze({ x: 200, y: 100 }) });
const items = Object.freeze([entry]);
const expected = layout(items, plot);
assert.deepEqual(layoutChartAnnotations(items, plot, { measureText: (text) => ({ width: measureText(text) }) }), expected);
const fallback = layoutChartAnnotations(items, plot, { measureText: () => NaN });
assert.ok(fallback.every(({ x, y, width, height }) => [x, y, width, height].every(Number.isFinite)));
assert.deepEqual(layout(items, plot), expected);
});
test("full labels avoid painted bars and their anchors", () => {
const plot = { x: 50, y: 10, width: 270, height: 240 };
const obstacles = [
{ x: 50, y: 70, width: 250, height: 38 },
{ x: 50, y: 136, width: 240, height: 38 },
{ x: 50, y: 202, width: 230, height: 38 },
];
for (const label of ["Returns shift added", "Weekend intake began"]) {
const items = [item("search", 280, 221, label)];
const result = layoutChartAnnotations(items, plot, { measureText, obstacles });
assert.equal(result.length, 1);
assertSafe(result, items, plot);
for (const mark of obstacles) {
const box = result[0];
assert.ok(box.x + box.width <= mark.x || mark.x + mark.width <= box.x
|| box.y + box.height <= mark.y || mark.y + mark.height <= box.y);
}
}
});
test("reserved chart whitespace preserves a label when data fills the plot", () => {
const bounds = { x: 40, y: 0, width: 300, height: 252 };
const marks = [{ x: 40, y: 52, width: 300, height: 200 }];
const items = [item("filled", 190, 200, "A supported finding")];
const [box] = layoutChartAnnotations(items, bounds, { measureText, obstacles: marks });
assert.ok(box.lines.length > 0);
assert.ok(box.y + box.height < 52);
assertSafe([box], items, bounds);
assert.deepEqual(layoutChartAnnotations(items, bounds, { measureText,
obstacles: [{ ...bounds }] }), [], "Never cover the data just to fit an annotation");
});
test("interior search finds an off-grid pocket after the quick candidates miss", () => {
const plot = { x: 0, y: 0, width: 310, height: 190 };
const label = item("pocket", 20, 20, "A contextual fact");
const obstacles = [
{ x: 0, y: 0, width: 310, height: 53 },
{ x: 0, y: 113, width: 310, height: 77 },
{ x: 0, y: 53, width: 113, height: 60 },
{ x: 253, y: 53, width: 57, height: 60 },
];
// More than16 obstacles exercises the exact fallback independently of the
// quick search's nearby-edge shortcut.
const far = Array.from({length:20},(_,index)=>({x:-50,y:-50-index,width:1,height:1}));
const result = layoutChartAnnotations([label], plot, {measureText,obstacles:[...far,...obstacles]});
assert.equal(result.length,1);
assert.ok(result[0].x>=121-1e-7 && result[0].x+result[0].width<=245+1e-7);
assert.ok(result[0].y>=61-1e-7 && result[0].y+result[0].height<=105+1e-7);
assertSafe(result,[label],plot);
});
test("exact and tolerance-sized tangent pockets remain legal during the event sweep", () => {
const plot = Object.freeze({x:0,y:0,width:310,height:190});
const label = item("tangent",20,20,"A contextual fact");
for (const overlap of [0, 1.5e-7]) {
const marks = [
{x:0,y:0,width:310,height:53},
{x:0,y:93-overlap,width:310,height:97+overlap},
{x:0,y:53,width:113,height:40},
{x:231-overlap,y:53,width:79+overlap,height:40},
];
// Duplicated intervals must remain covered until both marks leave. Keep the
// actual boundaries beyond the quick search's first 16 obstacle edges.
const obstacles = Object.freeze([
...Array.from({length:20},(_,index)=>({x:-50,y:-50-index,width:1,height:1})),
...marks,...marks.map((mark)=>({...mark})),
].map(Object.freeze));
const before = JSON.stringify(obstacles);
const result = layoutChartAnnotations([label],plot,{measureText,obstacles});
assert.equal(result.length,1,"The sweep must find even an exact tangent pocket");
const [box] = result;
assert.equal(box.width,102);
assert.equal(box.height,24);
assert.ok(Math.abs(box.x-121)<=1.01e-7 && Math.abs(box.y-61)<=1.01e-7);
for (const mark of marks) assert.ok(box.x+box.width+tokens.gap<=mark.x+1e-7
|| mark.x+mark.width+tokens.gap<=box.x+1e-7
|| box.y+box.height+tokens.gap<=mark.y+1e-7
|| mark.y+mark.height+tokens.gap<=box.y+1e-7,"Use the exact renderer separation test");
assertSafe(result,[label],plot);
assert.equal(JSON.stringify(obstacles),before,"Search never changes painted geometry");
}
});
test("full-text wrapping uses a 100px interior pocket rather than a figure note", () => {
const plot={x:0,y:0,width:310,height:190};
const label=item("narrow-pocket",20,20,"Alpha beta gamma delta");
const obstacles=[
...Array.from({length:20},(_,index)=>({x:-50,y:-50-index,width:1,height:1})),
{x:0,y:0,width:310,height:53},
{x:0,y:129,width:310,height:61},
{x:0,y:53,width:113,height:76},
{x:229,y:53,width:81,height:76},
];
const result=layoutChartAnnotations([label],plot,{measureText,obstacles});
assert.equal(result.length,1);
assert.equal(result[0].lines.join(" "),label.label);
assert.equal(result[0].lines.length,2);
assert.ok(result[0].width<=100 && result[0].height<=60);
assert.ok(result[0].x>=121-1e-7 && result[0].x+result[0].width<=221+1e-7);
assert.ok(result[0].y>=61-1e-7 && result[0].y+result[0].height<=121+1e-7);
assertSafe(result,[label],plot);
});
test("5000 painted obstacles do not turn full-text fallback into a quadratic search", () => {
const plot={x:50,y:10,width:720,height:260};
const obstacles=Array.from({length:5000},(_,index)=>({
x:50+(index/5000)*716,y:12+((index*37)%250),width:6,height:6,
}));
for (const kind of ["point","range"]) {
const labels=Array.from({length:8},(_,index)=>({...item(String(index),80+index*80,220,
"A supported story about the business",kind),
...(kind==="range"?{range:{axis:"x",start:280,end:420}}:{})}));
const start=performance.now();
const cpuStart=process.cpuUsage();
const result=layoutChartAnnotations(labels,plot,{measureText:(text)=>text.length*7,obstacles});
const elapsed=performance.now()-start;
const cpu=process.cpuUsage(cpuStart);
const cpuMs=(cpu.user+cpu.system)/1000;
assert.deepEqual(result,[],"Dense marks leave no full-text pocket; callers keep the complete figure notes");
// CPU time excludes scheduler pauses when this file runs beside the full
// template suite, while still rejecting the former 20-second quadratic sweep.
assert.ok(cpuMs<1000,`Eight ${kind} labels against 5000 marks took ${cpuMs.toFixed(1)}ms CPU (${elapsed.toFixed(1)}ms wall); expected under 1 second CPU`);
}
});
test("filled actual plot uses notes instead of placing text in an imaginary outer band", () => {
const plot={x:40,y:20,width:300,height:200};
const labels=[item("full",190,100,"Documented context")];
assert.deepEqual(layoutChartAnnotations(labels,plot,{measureText,obstacles:[{...plot}]}),[]);
});
test("near labels need no arrow and displaced arrows point from text to the exact anchor", () => {
const box={id:"a",x:50,y:50,width:100,height:30,anchor:{x:162,y:65}};
assert.equal(chartAnnotationArrow(box),null);
const far={...box,anchor:{x:220,y:110}};
const arrow=chartAnnotationArrow(far);
assert.deepEqual(arrow.end,far.anchor);
assert.deepEqual(arrow.start,{x:150,y:80});
assert.notDeepEqual(arrow.left,arrow.right);
assert.ok(arrow.left.x<arrow.end.x && arrow.right.x<arrow.end.x);
assert.equal((arrow.path.match(/L /gu)||[]).length,3,"Shaft and two head strokes form an actual arrow");
assert.equal(chartAnnotationArrow(far,[{x:175,y:80,width:35,height:40}]),null,
"An optional arrow must not cross another label");
});
test("displaced non-bar connectors cannot cross unrelated painted evidence", () => {
const box={id:"point",x:0,y:0,width:40,height:20,anchor:{x:200,y:120}};
const blocker={x:100,y:50,width:20,height:30};
assert.equal(chartAnnotationArrow(box,[],[blocker]),null,
"A label-only collision check would draw the shaft through this unrelated curve or filled mark");
const targetDot={x:196,y:116,width:8,height:8};
const targetReference={x:198,y:0,width:4,height:220};
for(const target of [targetDot,targetReference]){
const arrow=chartAnnotationArrow(box,[],[target]);
assert.ok(arrow,"The intended point or reference may meet its connector at the exact anchor");
assert.deepEqual(arrow.end,box.anchor);
assert.equal(chartAnnotationArrow(box,[],[target,blocker]),null,
"Contact with the target never permits crossing a second painted mark");
}
assert.equal(chartAnnotationArrow(box,[],[{x:90,y:40,width:115,height:90}]),null,
"An area containing the anchor cannot be ignored along the entire connector");
const clear=chartAnnotationArrow(box);
const headOnly={x:clear.right.x-1,y:clear.right.y-1,width:2,height:2};
assert.equal(chartAnnotationArrow(box,[],[headOnly]),null,"Arrowheads also avoid unrelated evidence");
});
SHA-256: 9071895cbb46d55ec303fec0084c6374a65e359d5a0de29c03710eba03a25bfb