@tangle-network/charts 0.10.0 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -0
- package/dist/index.d.ts +44 -1
- package/dist/index.js +136 -1
- package/dist/react/index.d.ts +47 -1
- package/dist/react/index.js +433 -2
- package/dist/react/live.css +15 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -302,6 +302,12 @@ Selection is controlled by bucket ID. Hover, focus, click, Enter, Space and touc
|
|
|
302
302
|
|
|
303
303
|
**Provenance and extraction boundary.** Sparkline comes from `tangle-network/agent-app`, `src/web-react/sparkline.tsx` and its tests at `a8947683a050394d13848bb7e4cbbe7cf08d208e`. Stacked SVG rendering and select-never-toggle regressions come from merged ADC [PR #8746](https://github.com/tangle-network/agent-dev-container/pull/8746), `products/platform/web/src/client/components/UsageChartSvg.tsx` and `UsageChart.test.tsx` at `a1c3958fd73a3894d4515f9dc476dae9ace30ce0`. Platform's aggregation, currency/product/category functions, legend composition and portal-tooltip positioning are **not** copied. The persistent readout and native pointer popover avoid a new React DOM peer. This is not a new universal usage-chart API, a zero-baseline replacement for SpendCard's sparkline, or a consumer migration; Platform and Agent App are not edited.
|
|
304
304
|
|
|
305
|
+
### Thousands of dots: `DotMap` and `unitDotLayout`
|
|
306
|
+
|
|
307
|
+
`unitDotLayout(groups, width, height)` places one dot per item in a rectangle per group, the rectangles a squarified treemap sized by count, so a fleet of 6,000 runs reads as its groups first and its items second with no item hidden in an aggregate. It returns the rectangles, every item's `x, y` (interleaved, groups in input order) and a radius per item from its group's cell.
|
|
308
|
+
|
|
309
|
+
`DotMap` draws those positions, or a semantic map's `x, y`, on one canvas: wheel or pinch zooms about the pointer, drag pans, a click selects, double-click or `0` fits, and `+ - ←→↑↓` work from the keyboard. Colours are palette indexes (`colors: Uint8Array` into `palette`, CSS colours or custom properties), so a lens change repaints without a new layout; `dimmed` greys filtered points, `radii` may vary per point (a dot sized by its run's hours inside its cell), and `labels` name clusters without overlapping. Hit tests use a uniform grid, one path is filled per colour, and the canvas records its last frame's draw time in `data-frame-ms`. The caller owns hover text (`describe`), selection, the legend and the details.
|
|
310
|
+
|
|
305
311
|
### React and package-boundary checks
|
|
306
312
|
|
|
307
313
|
```sh
|
package/dist/index.d.ts
CHANGED
|
@@ -301,4 +301,47 @@ interface SmallMultiplesOptions {
|
|
|
301
301
|
}
|
|
302
302
|
export declare function smallMultiples(panels: MultiplePanel[], o: SmallMultiplesOptions): Figure | Refusal;
|
|
303
303
|
//#endregion
|
|
304
|
-
|
|
304
|
+
//#region src/unit-dots.d.ts
|
|
305
|
+
/**
|
|
306
|
+
* Unit dots: one dot per item, packed in a rectangle per group, the rectangles laid out as a squarified treemap sized
|
|
307
|
+
* by count. A fleet of 6,000 runs reads as its groups first and its items second, with no item hidden in an
|
|
308
|
+
* aggregate. Positions are in layout units (the `width` and `height` given); a canvas scales them to its pixels.
|
|
309
|
+
*/
|
|
310
|
+
interface DotGroup {
|
|
311
|
+
id: string;
|
|
312
|
+
label: string;
|
|
313
|
+
/** Items in this group; their positions come out in group order, then item order within the group. */
|
|
314
|
+
count: number;
|
|
315
|
+
}
|
|
316
|
+
interface DotGroupRect {
|
|
317
|
+
id: string;
|
|
318
|
+
label: string;
|
|
319
|
+
count: number;
|
|
320
|
+
x0: number;
|
|
321
|
+
y0: number;
|
|
322
|
+
x1: number;
|
|
323
|
+
y1: number;
|
|
324
|
+
}
|
|
325
|
+
interface UnitDotLayout {
|
|
326
|
+
rects: DotGroupRect[];
|
|
327
|
+
/** x, y of every item, interleaved, groups in input order. */
|
|
328
|
+
positions: Float32Array;
|
|
329
|
+
/** Each item's dot radius in layout units: one size per group, from its cell. */
|
|
330
|
+
radii: Float32Array;
|
|
331
|
+
}
|
|
332
|
+
interface UnitDotOptions {
|
|
333
|
+
/** Space between rectangles. Default 6. */
|
|
334
|
+
gap?: number;
|
|
335
|
+
/** Space above each rectangle's dots for its label. Default 16. */
|
|
336
|
+
header?: number;
|
|
337
|
+
/** The largest dot radius, so a group of three is not three giant discs. Default 6. */
|
|
338
|
+
maxRadius?: number;
|
|
339
|
+
}
|
|
340
|
+
/**
|
|
341
|
+
* Lays out `groups` in a `width` × `height` box: a treemap rectangle per group with a header for its label, and its
|
|
342
|
+
* items on a grid of square cells inside it, filled row by row. Groups keep their input order in `positions`; the
|
|
343
|
+
* treemap places larger groups first.
|
|
344
|
+
*/
|
|
345
|
+
export declare function unitDotLayout(groups: DotGroup[], width: number, height: number, o?: UnitDotOptions): UnitDotLayout;
|
|
346
|
+
//#endregion
|
|
347
|
+
export { type BandKind, type BreakdownInput, type BreakdownOptions, CHECKER_HUES, type ChartBand, type ChartMark, type ComparisonIntervalsOptions, type ComparisonRow, type CostBasis, type CostFrontierOptions, type DotGroup, type DotGroupRect, type Estimate, type Exclusion, type Figure, type HeatmapCell, type HeatmapColumn, type HeatmapOptions, type HeatmapRow, type HeatmapStatus, type Interval, type IntervalMethod, type LatencyRangeOptions, type LatencyRangeRow, type MarkKind, type MarkVerdict, type MatrixCell, type MetricBarsOptions, type MetricRow, type MultiplePanel, OUTCOME_COLORS, OUTCOME_ORDER, type Observations, type PhaseWaterfallOptions, type PointFields, type PointGlyph, type RankedRatesOptions, type RateIdentity, type RateRow, type Refusal, type RenderFigureOptions, type Setup, type SmallMultiplesOptions, type TaskMatrixOptions, type TickOutcome, type Tier, type TierCounts, type TimePoint, type TimeSeriesOptions, type TimeSeriesRow, type TimelineLane, type TimelineOptions, type TimelineSpan, type TimelineTick, type UnitDotLayout, type UnitDotOptions, type WaterfallPhase, type XCategory, type YScale, type YTarget, type YTick, checkerColor, isRefusal, timeline };
|
package/dist/index.js
CHANGED
|
@@ -3183,4 +3183,139 @@ function draw(panels, o, width, xLabel, yLabelOf, marksOf) {
|
|
|
3183
3183
|
return svgRoot(width, height + key.height, body + key.svg);
|
|
3184
3184
|
}
|
|
3185
3185
|
//#endregion
|
|
3186
|
-
|
|
3186
|
+
//#region src/unit-dots.ts
|
|
3187
|
+
/** Squarified treemap (Bruls, Huizing and van Wijk) of positive values, largest first, into the rectangle. */
|
|
3188
|
+
function squarify(values, x, y, w, h) {
|
|
3189
|
+
const out = new Array(values.length);
|
|
3190
|
+
const total = values.reduce((sum, value) => sum + value, 0);
|
|
3191
|
+
if (!total) return values.map(() => [
|
|
3192
|
+
x,
|
|
3193
|
+
y,
|
|
3194
|
+
0,
|
|
3195
|
+
0
|
|
3196
|
+
]);
|
|
3197
|
+
const scale = w * h / total;
|
|
3198
|
+
let i = 0;
|
|
3199
|
+
while (i < values.length) {
|
|
3200
|
+
const side = Math.min(w, h);
|
|
3201
|
+
let row = [i];
|
|
3202
|
+
let rowSum = values[i] * scale;
|
|
3203
|
+
const worst = (sum, items) => {
|
|
3204
|
+
const areas = items.map((k) => values[k] * scale);
|
|
3205
|
+
const max = Math.max(...areas), min = Math.min(...areas);
|
|
3206
|
+
return Math.max(side * side * max / (sum * sum), sum * sum / (side * side * min));
|
|
3207
|
+
};
|
|
3208
|
+
let j = i + 1;
|
|
3209
|
+
while (j < values.length) {
|
|
3210
|
+
const next = rowSum + values[j] * scale;
|
|
3211
|
+
if (worst(next, [...row, j]) > worst(rowSum, row)) break;
|
|
3212
|
+
row = [...row, j];
|
|
3213
|
+
rowSum = next;
|
|
3214
|
+
j++;
|
|
3215
|
+
}
|
|
3216
|
+
const thickness = rowSum / side;
|
|
3217
|
+
let offset = 0;
|
|
3218
|
+
for (const k of row) {
|
|
3219
|
+
const length = values[k] * scale / thickness;
|
|
3220
|
+
out[k] = w >= h ? [
|
|
3221
|
+
x,
|
|
3222
|
+
y + offset,
|
|
3223
|
+
thickness,
|
|
3224
|
+
length
|
|
3225
|
+
] : [
|
|
3226
|
+
x + offset,
|
|
3227
|
+
y,
|
|
3228
|
+
length,
|
|
3229
|
+
thickness
|
|
3230
|
+
];
|
|
3231
|
+
offset += length;
|
|
3232
|
+
}
|
|
3233
|
+
if (w >= h) {
|
|
3234
|
+
x += thickness;
|
|
3235
|
+
w -= thickness;
|
|
3236
|
+
} else {
|
|
3237
|
+
y += thickness;
|
|
3238
|
+
h -= thickness;
|
|
3239
|
+
}
|
|
3240
|
+
i = j;
|
|
3241
|
+
}
|
|
3242
|
+
return out;
|
|
3243
|
+
}
|
|
3244
|
+
/**
|
|
3245
|
+
* Lays out `groups` in a `width` × `height` box: a treemap rectangle per group with a header for its label, and its
|
|
3246
|
+
* items on a grid of square cells inside it, filled row by row. Groups keep their input order in `positions`; the
|
|
3247
|
+
* treemap places larger groups first.
|
|
3248
|
+
*/
|
|
3249
|
+
function unitDotLayout(groups, width, height, o = {}) {
|
|
3250
|
+
const gap = o.gap ?? 6, header = o.header ?? 16, maxRadius = o.maxRadius ?? 6;
|
|
3251
|
+
const order = groups.map((group, index) => ({
|
|
3252
|
+
group,
|
|
3253
|
+
index
|
|
3254
|
+
})).filter(({ group }) => group.count > 0).sort((a, b) => b.group.count - a.group.count);
|
|
3255
|
+
const totalItems = order.reduce((sum, item) => sum + item.group.count, 0) || 1;
|
|
3256
|
+
const headerShare = header * width * .25 / (width * height) * totalItems;
|
|
3257
|
+
const boxes = squarify(order.map(({ group }) => group.count + headerShare), 0, 0, width, height);
|
|
3258
|
+
const rects = new Array(groups.length);
|
|
3259
|
+
const total = groups.reduce((sum, group) => sum + Math.max(0, group.count), 0);
|
|
3260
|
+
const positions = new Float32Array(total * 2);
|
|
3261
|
+
const radii = new Float32Array(total);
|
|
3262
|
+
const startOf = [];
|
|
3263
|
+
let start = 0;
|
|
3264
|
+
for (const group of groups) {
|
|
3265
|
+
startOf.push(start);
|
|
3266
|
+
start += Math.max(0, group.count);
|
|
3267
|
+
}
|
|
3268
|
+
order.forEach(({ group, index }, k) => {
|
|
3269
|
+
const [bx, by, bw, bh] = boxes[k];
|
|
3270
|
+
const x0 = bx + gap / 2, y0 = by + gap / 2, x1 = bx + bw - gap / 2, y1 = by + bh - gap / 2;
|
|
3271
|
+
rects[index] = {
|
|
3272
|
+
id: group.id,
|
|
3273
|
+
label: group.label,
|
|
3274
|
+
count: group.count,
|
|
3275
|
+
x0,
|
|
3276
|
+
y0,
|
|
3277
|
+
x1,
|
|
3278
|
+
y1
|
|
3279
|
+
};
|
|
3280
|
+
const innerW = Math.max(1, x1 - x0 - 4), innerH = Math.max(1, y1 - y0 - header - 2);
|
|
3281
|
+
let best = {
|
|
3282
|
+
cols: 1,
|
|
3283
|
+
cell: 0
|
|
3284
|
+
};
|
|
3285
|
+
const ideal = Math.max(1, Math.round(Math.sqrt(group.count * innerW / innerH)));
|
|
3286
|
+
for (let cols = Math.max(1, ideal - 2); cols <= ideal + 2; cols++) {
|
|
3287
|
+
const rows = Math.ceil(group.count / cols);
|
|
3288
|
+
const cell = Math.min(innerW / cols, innerH / rows);
|
|
3289
|
+
if (cell > best.cell) best = {
|
|
3290
|
+
cols,
|
|
3291
|
+
cell
|
|
3292
|
+
};
|
|
3293
|
+
}
|
|
3294
|
+
const r = Math.min(maxRadius, best.cell * .4);
|
|
3295
|
+
for (let item = 0; item < group.count; item++) {
|
|
3296
|
+
const col = item % best.cols, row = Math.floor(item / best.cols);
|
|
3297
|
+
const p = startOf[index] + item;
|
|
3298
|
+
positions[p * 2] = x0 + 2 + (col + .5) * best.cell;
|
|
3299
|
+
positions[p * 2 + 1] = y0 + header + (row + .5) * best.cell;
|
|
3300
|
+
radii[p] = r;
|
|
3301
|
+
}
|
|
3302
|
+
});
|
|
3303
|
+
groups.forEach((group, index) => {
|
|
3304
|
+
if (!rects[index]) rects[index] = {
|
|
3305
|
+
id: group.id,
|
|
3306
|
+
label: group.label,
|
|
3307
|
+
count: 0,
|
|
3308
|
+
x0: 0,
|
|
3309
|
+
y0: 0,
|
|
3310
|
+
x1: 0,
|
|
3311
|
+
y1: 0
|
|
3312
|
+
};
|
|
3313
|
+
});
|
|
3314
|
+
return {
|
|
3315
|
+
rects,
|
|
3316
|
+
positions,
|
|
3317
|
+
radii
|
|
3318
|
+
};
|
|
3319
|
+
}
|
|
3320
|
+
//#endregion
|
|
3321
|
+
export { CHECKER_HUES, OUTCOME_COLORS, OUTCOME_ORDER, breakdown, chartTokensCss, checkerColor, comparisonIntervals, costFrontier, figureCss, heatmap, isRefusal, latencyRange, metricBars, phaseWaterfall, rankedRates, rateOrder, renderFigure, renderNotes, renderRefusals, smallMultiples, taskMatrix, timeSeries, timeline, unitDotLayout };
|
package/dist/react/index.d.ts
CHANGED
|
@@ -272,4 +272,50 @@ interface TimelineViewProps {
|
|
|
272
272
|
}
|
|
273
273
|
export declare function TimelineView({ lanes, options, defaultCollapsed, presentation }: TimelineViewProps): import("react").JSX.Element;
|
|
274
274
|
//#endregion
|
|
275
|
-
|
|
275
|
+
//#region src/react/dot-map.d.ts
|
|
276
|
+
interface DotMapRect {
|
|
277
|
+
x0: number;
|
|
278
|
+
y0: number;
|
|
279
|
+
x1: number;
|
|
280
|
+
y1: number;
|
|
281
|
+
label: string;
|
|
282
|
+
/** Shown after the label, such as the group's count. */
|
|
283
|
+
note?: string;
|
|
284
|
+
}
|
|
285
|
+
interface DotMapLabel {
|
|
286
|
+
x: number;
|
|
287
|
+
y: number;
|
|
288
|
+
text: string;
|
|
289
|
+
}
|
|
290
|
+
interface DotMapProps {
|
|
291
|
+
/** x and y of every point in layout units, interleaved. */
|
|
292
|
+
positions: Float32Array;
|
|
293
|
+
/** Each point's radius in layout units, or one radius for all. */
|
|
294
|
+
radii: Float32Array | number;
|
|
295
|
+
/** Each point's palette index. */
|
|
296
|
+
colors: Uint8Array;
|
|
297
|
+
/** CSS colours (custom properties allowed) by palette index. */
|
|
298
|
+
palette: readonly string[];
|
|
299
|
+
/** The box the positions are laid out in: [x0, y0, x1, y1]. The map fits it on first draw and on reset. */
|
|
300
|
+
extent: readonly [number, number, number, number];
|
|
301
|
+
/** Group rectangles drawn under the points, labelled at their top left. */
|
|
302
|
+
rects?: readonly DotMapRect[];
|
|
303
|
+
/** Labels drawn over the points, such as cluster names at their centroids. */
|
|
304
|
+
labels?: readonly DotMapLabel[];
|
|
305
|
+
/** 1 dims a point (filtered out); dimmed points draw first and faint. */
|
|
306
|
+
dimmed?: Uint8Array | null;
|
|
307
|
+
selected?: number | null;
|
|
308
|
+
/** Radius floor in screen pixels, so a dense semantic map stays visible zoomed out. Default 1.2. */
|
|
309
|
+
minPixelRadius?: number;
|
|
310
|
+
onHover?: (index: number | null) => void;
|
|
311
|
+
onSelect?: (index: number | null) => void;
|
|
312
|
+
/** Text for a point's hover card. */
|
|
313
|
+
describe?: (index: number) => string;
|
|
314
|
+
height?: number;
|
|
315
|
+
ariaLabel: string;
|
|
316
|
+
}
|
|
317
|
+
/** The categorical lens colours, in slot order, then "other": the palette a DotMap lens indexes. */
|
|
318
|
+
export declare const DOT_MAP_CATEGORIES: readonly string[];
|
|
319
|
+
export declare function DotMap({ positions, radii, colors, palette, extent, rects, labels, dimmed, selected, minPixelRadius, onHover, onSelect, describe, height, ariaLabel }: DotMapProps): import("react").JSX.Element;
|
|
320
|
+
//#endregion
|
|
321
|
+
export type { BandKind, ChartBand, ChartMark, DotMapLabel, DotMapProps, DotMapRect, Figure, FigureAtWidth, FigureViewProps, LineSeries, MarkKind, MarkVerdict, MarkerShape, MeterSegment, PointFields, PointGlyph, Refusal, SparklineDirection, SparklineGeometry, SparklineGeometryOptions, SparklineLabelOptions, SparklinePoint, SparklineProps, SparklineValues, StackedBarBucket, StackedBarChartProps, StackedBarSegment, StackedBarSeries, TickOutcome, TimelineLane, TimelineOptions, TimelineSpan, TimelineTick, TimelineViewProps, WaterfallBarProps, WaterfallHeaderProps, WaterfallRowProps, WaterfallSpanGeometry, WaterfallTimeAxisProps, WaterfallWindow, XCategory, YScale, YTarget, YTick };
|
package/dist/react/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { K as markerPath, U as hatchPath, _ as markName, c as MARK_HEADS, p as eventsGap, t as timeline, v as isRefusal } from "../timeline-ApeJgFac.js";
|
|
2
2
|
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
-
import { Fragment as Fragment$1, useEffect, useId, useMemo, useRef, useState } from "react";
|
|
3
|
+
import { Fragment as Fragment$1, useCallback, useEffect, useId, useMemo, useRef, useState } from "react";
|
|
4
4
|
//#region src/react/sparkline.tsx
|
|
5
5
|
const DEFAULT_SPARKLINE_WIDTH = 96;
|
|
6
6
|
const DEFAULT_SPARKLINE_HEIGHT = 24;
|
|
@@ -1736,4 +1736,435 @@ function TimelineView({ lanes, options, defaultCollapsed = [], presentation }) {
|
|
|
1736
1736
|
});
|
|
1737
1737
|
}
|
|
1738
1738
|
//#endregion
|
|
1739
|
-
|
|
1739
|
+
//#region src/react/dot-map.tsx
|
|
1740
|
+
/**
|
|
1741
|
+
* A canvas of many dots (thousands of runs or agents) that pans and zooms at 60 fps: unit dots in group rectangles
|
|
1742
|
+
* (`unitDotLayout`) or a semantic map's x/y. Colours are palette indexes, so a lens change repaints without a new
|
|
1743
|
+
* layout; the caller owns hover and selection and draws its own legend and details.
|
|
1744
|
+
*/
|
|
1745
|
+
/** The categorical lens colours, in slot order, then "other": the palette a DotMap lens indexes. */
|
|
1746
|
+
const DOT_MAP_CATEGORIES = Object.freeze([
|
|
1747
|
+
"var(--chart-category-1)",
|
|
1748
|
+
"var(--chart-category-2)",
|
|
1749
|
+
"var(--chart-category-3)",
|
|
1750
|
+
"var(--chart-category-other)"
|
|
1751
|
+
]);
|
|
1752
|
+
/** Resolves CSS colours (including custom properties and light-dark()) to what the page computes, for the canvas. */
|
|
1753
|
+
function resolveColors(host, palette) {
|
|
1754
|
+
const probe = document.createElement("span");
|
|
1755
|
+
probe.style.display = "none";
|
|
1756
|
+
host.appendChild(probe);
|
|
1757
|
+
const out = palette.map((color) => {
|
|
1758
|
+
probe.style.color = "";
|
|
1759
|
+
probe.style.color = color;
|
|
1760
|
+
return getComputedStyle(probe).color || color;
|
|
1761
|
+
});
|
|
1762
|
+
probe.remove();
|
|
1763
|
+
return out;
|
|
1764
|
+
}
|
|
1765
|
+
/** A uniform grid over the layout, for hit tests in constant time per pointer move. */
|
|
1766
|
+
function gridIndex(positions, extent, cell) {
|
|
1767
|
+
const [x0, y0, x1, y1] = extent;
|
|
1768
|
+
const cols = Math.max(1, Math.ceil((x1 - x0) / cell));
|
|
1769
|
+
const rows = Math.max(1, Math.ceil((y1 - y0) / cell));
|
|
1770
|
+
const cells = /* @__PURE__ */ new Map();
|
|
1771
|
+
for (let i = 0; i < positions.length / 2; i++) {
|
|
1772
|
+
const c = Math.min(cols - 1, Math.max(0, Math.floor((positions[i * 2] - x0) / cell)));
|
|
1773
|
+
const key = Math.min(rows - 1, Math.max(0, Math.floor((positions[i * 2 + 1] - y0) / cell))) * cols + c;
|
|
1774
|
+
const list = cells.get(key);
|
|
1775
|
+
if (list) list.push(i);
|
|
1776
|
+
else cells.set(key, [i]);
|
|
1777
|
+
}
|
|
1778
|
+
return {
|
|
1779
|
+
cols,
|
|
1780
|
+
rows,
|
|
1781
|
+
cell,
|
|
1782
|
+
cells,
|
|
1783
|
+
x0,
|
|
1784
|
+
y0
|
|
1785
|
+
};
|
|
1786
|
+
}
|
|
1787
|
+
function DotMap({ positions, radii, colors, palette, extent, rects, labels, dimmed, selected, minPixelRadius = 1.2, onHover, onSelect, describe, height = 560, ariaLabel }) {
|
|
1788
|
+
const box = useRef(null);
|
|
1789
|
+
const canvas = useRef(null);
|
|
1790
|
+
const [size, setSize] = useState({
|
|
1791
|
+
width: 0,
|
|
1792
|
+
height
|
|
1793
|
+
});
|
|
1794
|
+
const view = useRef({
|
|
1795
|
+
scale: 1,
|
|
1796
|
+
tx: 0,
|
|
1797
|
+
ty: 0
|
|
1798
|
+
});
|
|
1799
|
+
const fitted = useRef("");
|
|
1800
|
+
const frame = useRef(0);
|
|
1801
|
+
const [hover, setHover] = useState(null);
|
|
1802
|
+
const colorsResolved = useRef([]);
|
|
1803
|
+
const count = positions.length / 2;
|
|
1804
|
+
const maxRadius = useMemo(() => typeof radii === "number" ? radii : radii.reduce((max, r) => r > max ? r : max, 0), [radii]);
|
|
1805
|
+
const index = useMemo(() => gridIndex(positions, extent, Math.max(maxRadius * 2, (extent[2] - extent[0]) / 200)), [
|
|
1806
|
+
positions,
|
|
1807
|
+
extent,
|
|
1808
|
+
maxRadius
|
|
1809
|
+
]);
|
|
1810
|
+
useEffect(() => {
|
|
1811
|
+
if (!box.current) return;
|
|
1812
|
+
const observer = new ResizeObserver(([entry]) => setSize({
|
|
1813
|
+
width: Math.floor(entry.contentRect.width),
|
|
1814
|
+
height
|
|
1815
|
+
}));
|
|
1816
|
+
observer.observe(box.current);
|
|
1817
|
+
return () => observer.disconnect();
|
|
1818
|
+
}, [height]);
|
|
1819
|
+
const fit = useCallback(() => {
|
|
1820
|
+
const [x0, y0, x1, y1] = extent;
|
|
1821
|
+
const pad = 8;
|
|
1822
|
+
const scale = Math.min((size.width - 16) / Math.max(1e-9, x1 - x0), (size.height - 16) / Math.max(1e-9, y1 - y0));
|
|
1823
|
+
view.current = {
|
|
1824
|
+
scale,
|
|
1825
|
+
tx: pad - x0 * scale + (size.width - 16 - (x1 - x0) * scale) / 2,
|
|
1826
|
+
ty: pad - y0 * scale + (size.height - 16 - (y1 - y0) * scale) / 2
|
|
1827
|
+
};
|
|
1828
|
+
}, [extent, size]);
|
|
1829
|
+
const draw = useCallback(() => {
|
|
1830
|
+
frame.current = 0;
|
|
1831
|
+
const element = canvas.current;
|
|
1832
|
+
if (!element || !size.width) return;
|
|
1833
|
+
const started = performance.now();
|
|
1834
|
+
const dpr = window.devicePixelRatio || 1;
|
|
1835
|
+
if (element.width !== Math.round(size.width * dpr) || element.height !== Math.round(size.height * dpr)) {
|
|
1836
|
+
element.width = Math.round(size.width * dpr);
|
|
1837
|
+
element.height = Math.round(size.height * dpr);
|
|
1838
|
+
}
|
|
1839
|
+
const ctx = element.getContext("2d");
|
|
1840
|
+
if (!ctx) return;
|
|
1841
|
+
if (!colorsResolved.current.length && box.current) colorsResolved.current = resolveColors(box.current, palette);
|
|
1842
|
+
const { scale, tx, ty } = view.current;
|
|
1843
|
+
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
|
1844
|
+
ctx.clearRect(0, 0, size.width, size.height);
|
|
1845
|
+
const style = box.current ? getComputedStyle(box.current) : null;
|
|
1846
|
+
const ink = style?.color || "#888";
|
|
1847
|
+
if (rects?.length) {
|
|
1848
|
+
ctx.lineWidth = 1;
|
|
1849
|
+
ctx.font = "11px ui-sans-serif, system-ui, sans-serif";
|
|
1850
|
+
for (const rect of rects) {
|
|
1851
|
+
if (rect.x1 <= rect.x0) continue;
|
|
1852
|
+
const x = rect.x0 * scale + tx, y = rect.y0 * scale + ty, w = (rect.x1 - rect.x0) * scale, h = (rect.y1 - rect.y0) * scale;
|
|
1853
|
+
if (x > size.width || y > size.height || x + w < 0 || y + h < 0) continue;
|
|
1854
|
+
ctx.globalAlpha = .35;
|
|
1855
|
+
ctx.strokeStyle = ink;
|
|
1856
|
+
ctx.strokeRect(x + .5, y + .5, w, h);
|
|
1857
|
+
if (w > 36) {
|
|
1858
|
+
ctx.globalAlpha = .8;
|
|
1859
|
+
ctx.fillStyle = ink;
|
|
1860
|
+
const text = rect.note ? `${rect.label} · ${rect.note}` : rect.label;
|
|
1861
|
+
ctx.save();
|
|
1862
|
+
ctx.beginPath();
|
|
1863
|
+
ctx.rect(x, y, w - 4, 16);
|
|
1864
|
+
ctx.clip();
|
|
1865
|
+
ctx.fillText(text, x + 4, y + 12);
|
|
1866
|
+
ctx.restore();
|
|
1867
|
+
}
|
|
1868
|
+
}
|
|
1869
|
+
ctx.globalAlpha = 1;
|
|
1870
|
+
}
|
|
1871
|
+
const paths = /* @__PURE__ */ new Map();
|
|
1872
|
+
const faint = new Path2D();
|
|
1873
|
+
const r0 = typeof radii === "number" ? radii : 0;
|
|
1874
|
+
for (let i = 0; i < count; i++) {
|
|
1875
|
+
const x = positions[i * 2] * scale + tx;
|
|
1876
|
+
const y = positions[i * 2 + 1] * scale + ty;
|
|
1877
|
+
if (x < -8 || y < -8 || x > size.width + 8 || y > size.height + 8) continue;
|
|
1878
|
+
const r = Math.max(minPixelRadius, (typeof radii === "number" ? r0 : radii[i]) * scale);
|
|
1879
|
+
const path = dimmed?.[i] ? faint : paths.get(colors[i]) ?? paths.set(colors[i], new Path2D()).get(colors[i]);
|
|
1880
|
+
path.moveTo(x + r, y);
|
|
1881
|
+
path.arc(x, y, r, 0, Math.PI * 2);
|
|
1882
|
+
}
|
|
1883
|
+
ctx.globalAlpha = .18;
|
|
1884
|
+
ctx.fillStyle = ink;
|
|
1885
|
+
ctx.fill(faint);
|
|
1886
|
+
ctx.globalAlpha = .92;
|
|
1887
|
+
for (const [color, path] of paths) {
|
|
1888
|
+
ctx.fillStyle = colorsResolved.current[color] ?? ink;
|
|
1889
|
+
ctx.fill(path);
|
|
1890
|
+
}
|
|
1891
|
+
ctx.globalAlpha = 1;
|
|
1892
|
+
if (selected !== null && selected !== void 0 && selected < count) {
|
|
1893
|
+
const x = positions[selected * 2] * scale + tx, y = positions[selected * 2 + 1] * scale + ty;
|
|
1894
|
+
const r = Math.max(4, (typeof radii === "number" ? r0 : radii[selected]) * scale + 3);
|
|
1895
|
+
ctx.lineWidth = 2;
|
|
1896
|
+
ctx.strokeStyle = ink;
|
|
1897
|
+
ctx.beginPath();
|
|
1898
|
+
ctx.arc(x, y, r, 0, Math.PI * 2);
|
|
1899
|
+
ctx.stroke();
|
|
1900
|
+
}
|
|
1901
|
+
if (labels?.length) {
|
|
1902
|
+
ctx.font = "600 12px ui-sans-serif, system-ui, sans-serif";
|
|
1903
|
+
ctx.textAlign = "center";
|
|
1904
|
+
ctx.lineWidth = 3;
|
|
1905
|
+
const surface = style?.getPropertyValue("--dot-map-surface").trim() || style?.backgroundColor || "#161616";
|
|
1906
|
+
const taken = [];
|
|
1907
|
+
for (const label of labels) {
|
|
1908
|
+
const x = label.x * scale + tx, y = label.y * scale + ty;
|
|
1909
|
+
if (x < 0 || y < 0 || x > size.width || y > size.height) continue;
|
|
1910
|
+
const w = ctx.measureText(label.text).width;
|
|
1911
|
+
const box4 = [
|
|
1912
|
+
x - w / 2 - 2,
|
|
1913
|
+
y - 10,
|
|
1914
|
+
x + w / 2 + 2,
|
|
1915
|
+
y + 4
|
|
1916
|
+
];
|
|
1917
|
+
if (taken.some(([a, b, c, d]) => box4[0] < c && box4[2] > a && box4[1] < d && box4[3] > b)) continue;
|
|
1918
|
+
taken.push(box4);
|
|
1919
|
+
ctx.strokeStyle = surface;
|
|
1920
|
+
ctx.strokeText(label.text, x, y);
|
|
1921
|
+
ctx.fillStyle = ink;
|
|
1922
|
+
ctx.fillText(label.text, x, y);
|
|
1923
|
+
}
|
|
1924
|
+
ctx.textAlign = "start";
|
|
1925
|
+
}
|
|
1926
|
+
element.dataset.frameMs = (performance.now() - started).toFixed(2);
|
|
1927
|
+
}, [
|
|
1928
|
+
size,
|
|
1929
|
+
positions,
|
|
1930
|
+
radii,
|
|
1931
|
+
colors,
|
|
1932
|
+
palette,
|
|
1933
|
+
rects,
|
|
1934
|
+
labels,
|
|
1935
|
+
dimmed,
|
|
1936
|
+
selected,
|
|
1937
|
+
count,
|
|
1938
|
+
minPixelRadius
|
|
1939
|
+
]);
|
|
1940
|
+
const schedule = useCallback(() => {
|
|
1941
|
+
if (!frame.current) frame.current = requestAnimationFrame(draw);
|
|
1942
|
+
}, [draw]);
|
|
1943
|
+
useEffect(() => {
|
|
1944
|
+
colorsResolved.current = [];
|
|
1945
|
+
}, [palette]);
|
|
1946
|
+
useEffect(() => {
|
|
1947
|
+
if (!size.width) return;
|
|
1948
|
+
const key = `${extent.join(",")}:${size.width}x${size.height}`;
|
|
1949
|
+
if (fitted.current !== key) {
|
|
1950
|
+
fit();
|
|
1951
|
+
fitted.current = key;
|
|
1952
|
+
}
|
|
1953
|
+
schedule();
|
|
1954
|
+
}, [
|
|
1955
|
+
size,
|
|
1956
|
+
extent,
|
|
1957
|
+
fit,
|
|
1958
|
+
schedule
|
|
1959
|
+
]);
|
|
1960
|
+
useEffect(() => schedule(), [schedule]);
|
|
1961
|
+
useEffect(() => () => cancelAnimationFrame(frame.current), []);
|
|
1962
|
+
const pick = useCallback((clientX, clientY) => {
|
|
1963
|
+
const element = canvas.current;
|
|
1964
|
+
if (!element) return null;
|
|
1965
|
+
const bounds = element.getBoundingClientRect();
|
|
1966
|
+
const { scale, tx, ty } = view.current;
|
|
1967
|
+
const lx = (clientX - bounds.left - tx) / scale;
|
|
1968
|
+
const ly = (clientY - bounds.top - ty) / scale;
|
|
1969
|
+
const c = Math.floor((lx - index.x0) / index.cell);
|
|
1970
|
+
const r = Math.floor((ly - index.y0) / index.cell);
|
|
1971
|
+
let best = null;
|
|
1972
|
+
let bestD = Infinity;
|
|
1973
|
+
const reach = Math.max(1, Math.ceil(6 / (scale * index.cell)));
|
|
1974
|
+
for (let dr = -reach; dr <= reach; dr++) for (let dc = -reach; dc <= reach; dc++) {
|
|
1975
|
+
if (c + dc < 0 || r + dr < 0 || c + dc >= index.cols || r + dr >= index.rows) continue;
|
|
1976
|
+
for (const i of index.cells.get((r + dr) * index.cols + (c + dc)) ?? []) {
|
|
1977
|
+
const dx = positions[i * 2] - lx, dy = positions[i * 2 + 1] - ly;
|
|
1978
|
+
const d = dx * dx + dy * dy;
|
|
1979
|
+
const hit = Math.max(6 / scale, typeof radii === "number" ? radii : radii[i]);
|
|
1980
|
+
if (d < bestD && d <= hit * hit) {
|
|
1981
|
+
bestD = d;
|
|
1982
|
+
best = i;
|
|
1983
|
+
}
|
|
1984
|
+
}
|
|
1985
|
+
}
|
|
1986
|
+
return best;
|
|
1987
|
+
}, [
|
|
1988
|
+
index,
|
|
1989
|
+
positions,
|
|
1990
|
+
radii
|
|
1991
|
+
]);
|
|
1992
|
+
const drag = useRef(null);
|
|
1993
|
+
const touches = useRef(/* @__PURE__ */ new Map());
|
|
1994
|
+
const pinch = useRef(null);
|
|
1995
|
+
useEffect(() => {
|
|
1996
|
+
const element = canvas.current;
|
|
1997
|
+
if (!element) return;
|
|
1998
|
+
const wheel = (event) => {
|
|
1999
|
+
event.preventDefault();
|
|
2000
|
+
const bounds = element.getBoundingClientRect();
|
|
2001
|
+
const px = event.clientX - bounds.left, py = event.clientY - bounds.top;
|
|
2002
|
+
const factor = Math.exp(-event.deltaY * .0015);
|
|
2003
|
+
const v = view.current;
|
|
2004
|
+
const scale = Math.min(v.scale * 400, Math.max(v.scale * .0025, v.scale * factor));
|
|
2005
|
+
view.current = {
|
|
2006
|
+
scale,
|
|
2007
|
+
tx: px - (px - v.tx) / v.scale * scale,
|
|
2008
|
+
ty: py - (py - v.ty) / v.scale * scale
|
|
2009
|
+
};
|
|
2010
|
+
schedule();
|
|
2011
|
+
};
|
|
2012
|
+
element.addEventListener("wheel", wheel, { passive: false });
|
|
2013
|
+
return () => element.removeEventListener("wheel", wheel);
|
|
2014
|
+
}, [schedule]);
|
|
2015
|
+
const hovered = hover ? describe?.(hover.index) ?? null : null;
|
|
2016
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
2017
|
+
ref: box,
|
|
2018
|
+
className: "tgc-dot-map",
|
|
2019
|
+
style: {
|
|
2020
|
+
position: "relative",
|
|
2021
|
+
height
|
|
2022
|
+
},
|
|
2023
|
+
"data-points": count,
|
|
2024
|
+
children: [/* @__PURE__ */ jsx("canvas", {
|
|
2025
|
+
ref: canvas,
|
|
2026
|
+
role: "img",
|
|
2027
|
+
"aria-label": ariaLabel,
|
|
2028
|
+
tabIndex: 0,
|
|
2029
|
+
style: {
|
|
2030
|
+
width: "100%",
|
|
2031
|
+
height,
|
|
2032
|
+
display: "block",
|
|
2033
|
+
touchAction: "none",
|
|
2034
|
+
cursor: hover ? "pointer" : "grab"
|
|
2035
|
+
},
|
|
2036
|
+
onPointerDown: (event) => {
|
|
2037
|
+
touches.current.set(event.pointerId, {
|
|
2038
|
+
x: event.clientX,
|
|
2039
|
+
y: event.clientY
|
|
2040
|
+
});
|
|
2041
|
+
if (touches.current.size === 2) {
|
|
2042
|
+
const [a, b] = [...touches.current.values()];
|
|
2043
|
+
pinch.current = {
|
|
2044
|
+
distance: Math.hypot(a.x - b.x, a.y - b.y) || 1,
|
|
2045
|
+
view: { ...view.current }
|
|
2046
|
+
};
|
|
2047
|
+
drag.current = null;
|
|
2048
|
+
} else drag.current = {
|
|
2049
|
+
x: event.clientX,
|
|
2050
|
+
y: event.clientY,
|
|
2051
|
+
moved: false
|
|
2052
|
+
};
|
|
2053
|
+
event.currentTarget.setPointerCapture(event.pointerId);
|
|
2054
|
+
},
|
|
2055
|
+
onPointerMove: (event) => {
|
|
2056
|
+
if (touches.current.has(event.pointerId)) touches.current.set(event.pointerId, {
|
|
2057
|
+
x: event.clientX,
|
|
2058
|
+
y: event.clientY
|
|
2059
|
+
});
|
|
2060
|
+
const p = pinch.current;
|
|
2061
|
+
if (p && touches.current.size === 2) {
|
|
2062
|
+
const [a, b] = [...touches.current.values()];
|
|
2063
|
+
const bounds = event.currentTarget.getBoundingClientRect();
|
|
2064
|
+
const mx = (a.x + b.x) / 2 - bounds.left, my = (a.y + b.y) / 2 - bounds.top;
|
|
2065
|
+
const scale = p.view.scale * ((Math.hypot(a.x - b.x, a.y - b.y) || 1) / p.distance);
|
|
2066
|
+
view.current = {
|
|
2067
|
+
scale,
|
|
2068
|
+
tx: mx - (mx - p.view.tx) / p.view.scale * scale,
|
|
2069
|
+
ty: my - (my - p.view.ty) / p.view.scale * scale
|
|
2070
|
+
};
|
|
2071
|
+
schedule();
|
|
2072
|
+
return;
|
|
2073
|
+
}
|
|
2074
|
+
const d = drag.current;
|
|
2075
|
+
if (d) {
|
|
2076
|
+
const dx = event.clientX - d.x, dy = event.clientY - d.y;
|
|
2077
|
+
if (d.moved || Math.abs(dx) + Math.abs(dy) > 3) {
|
|
2078
|
+
d.moved = true;
|
|
2079
|
+
view.current = {
|
|
2080
|
+
...view.current,
|
|
2081
|
+
tx: view.current.tx + dx,
|
|
2082
|
+
ty: view.current.ty + dy
|
|
2083
|
+
};
|
|
2084
|
+
d.x = event.clientX;
|
|
2085
|
+
d.y = event.clientY;
|
|
2086
|
+
schedule();
|
|
2087
|
+
return;
|
|
2088
|
+
}
|
|
2089
|
+
}
|
|
2090
|
+
const i = pick(event.clientX, event.clientY);
|
|
2091
|
+
const bounds = event.currentTarget.getBoundingClientRect();
|
|
2092
|
+
setHover(i === null ? null : {
|
|
2093
|
+
index: i,
|
|
2094
|
+
x: event.clientX - bounds.left,
|
|
2095
|
+
y: event.clientY - bounds.top
|
|
2096
|
+
});
|
|
2097
|
+
onHover?.(i);
|
|
2098
|
+
},
|
|
2099
|
+
onPointerUp: (event) => {
|
|
2100
|
+
touches.current.delete(event.pointerId);
|
|
2101
|
+
if (pinch.current) {
|
|
2102
|
+
if (touches.current.size < 2) pinch.current = null;
|
|
2103
|
+
drag.current = null;
|
|
2104
|
+
return;
|
|
2105
|
+
}
|
|
2106
|
+
const d = drag.current;
|
|
2107
|
+
drag.current = null;
|
|
2108
|
+
if (d && !d.moved) onSelect?.(pick(event.clientX, event.clientY));
|
|
2109
|
+
},
|
|
2110
|
+
onPointerCancel: (event) => {
|
|
2111
|
+
touches.current.delete(event.pointerId);
|
|
2112
|
+
if (touches.current.size < 2) pinch.current = null;
|
|
2113
|
+
drag.current = null;
|
|
2114
|
+
},
|
|
2115
|
+
onPointerLeave: () => {
|
|
2116
|
+
setHover(null);
|
|
2117
|
+
onHover?.(null);
|
|
2118
|
+
},
|
|
2119
|
+
onDoubleClick: () => {
|
|
2120
|
+
fit();
|
|
2121
|
+
schedule();
|
|
2122
|
+
},
|
|
2123
|
+
onKeyDown: (event) => {
|
|
2124
|
+
const v = view.current;
|
|
2125
|
+
const zoom = (factor) => {
|
|
2126
|
+
const cx = size.width / 2, cy = size.height / 2;
|
|
2127
|
+
view.current = {
|
|
2128
|
+
scale: v.scale * factor,
|
|
2129
|
+
tx: cx - (cx - v.tx) / v.scale * v.scale * factor,
|
|
2130
|
+
ty: cy - (cy - v.ty) / v.scale * v.scale * factor
|
|
2131
|
+
};
|
|
2132
|
+
};
|
|
2133
|
+
if (event.key === "+" || event.key === "=") zoom(1.4);
|
|
2134
|
+
else if (event.key === "-") zoom(1 / 1.4);
|
|
2135
|
+
else if (event.key === "0") fit();
|
|
2136
|
+
else if (event.key === "ArrowLeft") view.current = {
|
|
2137
|
+
...v,
|
|
2138
|
+
tx: v.tx + 60
|
|
2139
|
+
};
|
|
2140
|
+
else if (event.key === "ArrowRight") view.current = {
|
|
2141
|
+
...v,
|
|
2142
|
+
tx: v.tx - 60
|
|
2143
|
+
};
|
|
2144
|
+
else if (event.key === "ArrowUp") view.current = {
|
|
2145
|
+
...v,
|
|
2146
|
+
ty: v.ty + 60
|
|
2147
|
+
};
|
|
2148
|
+
else if (event.key === "ArrowDown") view.current = {
|
|
2149
|
+
...v,
|
|
2150
|
+
ty: v.ty - 60
|
|
2151
|
+
};
|
|
2152
|
+
else return;
|
|
2153
|
+
event.preventDefault();
|
|
2154
|
+
schedule();
|
|
2155
|
+
}
|
|
2156
|
+
}), hover && hovered && /* @__PURE__ */ jsx("div", {
|
|
2157
|
+
className: "tgc-dot-map-tip",
|
|
2158
|
+
role: "tooltip",
|
|
2159
|
+
style: {
|
|
2160
|
+
position: "absolute",
|
|
2161
|
+
left: Math.min(hover.x + 12, Math.max(0, size.width - 300)),
|
|
2162
|
+
top: hover.y + 12,
|
|
2163
|
+
pointerEvents: "none"
|
|
2164
|
+
},
|
|
2165
|
+
children: hovered
|
|
2166
|
+
})]
|
|
2167
|
+
});
|
|
2168
|
+
}
|
|
2169
|
+
//#endregion
|
|
2170
|
+
export { DEFAULT_SPARKLINE_EMPTY_LABEL, DEFAULT_SPARKLINE_HEIGHT, DEFAULT_SPARKLINE_LABEL, DEFAULT_SPARKLINE_UNAVAILABLE_LABEL, DEFAULT_SPARKLINE_WIDTH, DOT_MAP_CATEGORIES, DayBars, DotMap, FigureView, Gantt, HBars, Histogram, LIVE_SERIES, Lines, Meter, Spark, Sparkline, StackedBarChart, TimelineView, WaterfallBar, WaterfallHeader, WaterfallRow, WaterfallTimeAxis, formatCompact, formatSparklineValue, formatWaterfallDuration, sparklineGeometry, sparklineLabel, sparklinePointsAttribute, sparklineReadings, waterfallSpanGeometry, waterfallTicks };
|
package/dist/react/live.css
CHANGED
|
@@ -73,3 +73,18 @@ line.ov-gridline { stroke: color-mix(in srgb, var(--chart-live-faint, #777) 22%,
|
|
|
73
73
|
.ov-event-idle { background: var(--chart-live-hover, var(--chart-shade)); }
|
|
74
74
|
.ov-event-missing { background: repeating-linear-gradient(135deg, var(--chart-live-faint, var(--chart-muted)) 0 1px, transparent 1px 4px); }
|
|
75
75
|
.ov-refusal { margin: 0; color: var(--chart-live-muted, var(--chart-ink-muted)); font-size: .9em; }
|
|
76
|
+
|
|
77
|
+
/*
|
|
78
|
+
* DotMap: thousands of dots on one canvas. A categorical lens names at most three categories in colour, the first
|
|
79
|
+
* three slots of the dataviz reference palette, which pass its all-pairs colour-vision checks in both themes (a
|
|
80
|
+
* scatter puts every pair side by side); every other category is the neutral "other". :where() keeps them overridable.
|
|
81
|
+
*/
|
|
82
|
+
:where(:root) {
|
|
83
|
+
--chart-category-1: light-dark(#2a78d6, #3987e5);
|
|
84
|
+
--chart-category-2: light-dark(#eb6834, #d95926);
|
|
85
|
+
--chart-category-3: light-dark(#1baf7a, #199e70);
|
|
86
|
+
--chart-category-other: #898781;
|
|
87
|
+
}
|
|
88
|
+
.tgc-dot-map { color: var(--chart-live-muted, #8a8a8a); }
|
|
89
|
+
.tgc-dot-map canvas:focus-visible { outline: 2px solid var(--chart-accent, #7760bd); outline-offset: 2px; }
|
|
90
|
+
.tgc-dot-map-tip { z-index: 2; max-width: 300px; padding: 6px 9px; border-radius: 6px; background: var(--chart-tip-bg, #262626); color: var(--chart-tip-ink, #eee); font: 12px/1.4 ui-sans-serif, system-ui, sans-serif; white-space: pre-line; box-shadow: 0 4px 14px rgb(0 0 0 / .3); }
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tangle-network/charts",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.0",
|
|
4
4
|
"description": "Static SVG and HTML figures for benchmarks and operational timing. Draws from producer data and refuses what it cannot draw honestly.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|