@ruckc/truss-chart 1.6.0 → 1.8.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.
@@ -0,0 +1,149 @@
1
+ /** Pure helpers for DensityChart: data preparation, single-pass binning, hit testing, view maths and the colour ramp. */
2
+ import { MAX_SERIES } from './util.js';
3
+ /**
4
+ * Normalises either input style into parallel arrays. Typed arrays are used as-is (no copy).
5
+ * Groups beyond the 8 palette slots, and points without a group in a grouped set, take slot 0.
6
+ */
7
+ export function prepare(points, xs, ys, groupIds, groupNames) {
8
+ let px, py, g = null, names = [];
9
+ if (xs && ys) {
10
+ px = xs;
11
+ py = ys;
12
+ if (groupIds && groupNames?.length) {
13
+ g = groupIds;
14
+ names = groupNames.slice(0, MAX_SERIES);
15
+ }
16
+ }
17
+ else {
18
+ const pts = points ?? [];
19
+ const n = pts.length;
20
+ const fx = new Float64Array(n), fy = new Float64Array(n);
21
+ const ids = new Map();
22
+ let fg = null;
23
+ for (let i = 0; i < n; i++) {
24
+ const p = pts[i];
25
+ fx[i] = p.x;
26
+ fy[i] = p.y;
27
+ if (p.group != null) {
28
+ let id = ids.get(p.group);
29
+ if (id === undefined) {
30
+ if (ids.size >= MAX_SERIES)
31
+ id = 0;
32
+ else {
33
+ id = ids.size;
34
+ ids.set(p.group, id);
35
+ }
36
+ }
37
+ if (!fg)
38
+ fg = new Uint8Array(n);
39
+ fg[i] = id;
40
+ }
41
+ }
42
+ px = fx;
43
+ py = fy;
44
+ g = fg;
45
+ names = [...ids.keys()];
46
+ }
47
+ const n = Math.min(px.length, py.length);
48
+ let x0 = Infinity, x1 = -Infinity, y0 = Infinity, y1 = -Infinity;
49
+ for (let i = 0; i < n; i++) {
50
+ const x = px[i], y = py[i];
51
+ if (!Number.isFinite(x) || !Number.isFinite(y))
52
+ continue;
53
+ if (x < x0)
54
+ x0 = x;
55
+ if (x > x1)
56
+ x1 = x;
57
+ if (y < y0)
58
+ y0 = y;
59
+ if (y > y1)
60
+ y1 = y;
61
+ }
62
+ return { xs: px, ys: py, g, names, n, extent: x0 <= x1 ? { x0, x1, y0, y1 } : null };
63
+ }
64
+ /**
65
+ * Counts the points inside `v` into `out` (cols x rows, row 0 at the bottom, y up) in one pass, skipping hidden
66
+ * groups and non-finite values. Returns how many points fell inside the view. No allocation.
67
+ */
68
+ export function bin(p, hid, v, cols, rows, out) {
69
+ out.fill(0, 0, cols * rows);
70
+ const { xs, ys, g, n } = p;
71
+ const sx = cols / (v.x1 - v.x0), sy = rows / (v.y1 - v.y0);
72
+ const lastC = cols - 1, lastR = rows - 1;
73
+ let inside = 0;
74
+ for (let i = 0; i < n; i++) {
75
+ const x = xs[i], y = ys[i];
76
+ // written so NaN fails every comparison
77
+ if (!(x >= v.x0 && x <= v.x1 && y >= v.y0 && y <= v.y1))
78
+ continue;
79
+ if (g !== null && hid[g[i]] === 1)
80
+ continue;
81
+ let c = ((x - v.x0) * sx) | 0, r = ((y - v.y0) * sy) | 0;
82
+ if (c > lastC)
83
+ c = lastC;
84
+ if (r > lastR)
85
+ r = lastR;
86
+ out[r * cols + c]++;
87
+ inside++;
88
+ }
89
+ return inside;
90
+ }
91
+ /** Index of the visible point nearest to the domain position (qx, qy) within `rx`/`ry` domain units, or -1. */
92
+ export function nearest(p, hid, qx, qy, rx, ry) {
93
+ const { xs, ys, g, n } = p;
94
+ let best = -1, bd = 1;
95
+ for (let i = 0; i < n; i++) {
96
+ const dx = (xs[i] - qx) / rx;
97
+ if (!(dx >= -1 && dx <= 1))
98
+ continue;
99
+ const dy = (ys[i] - qy) / ry;
100
+ if (!(dy >= -1 && dy <= 1))
101
+ continue;
102
+ if (g !== null && hid[g[i]] === 1)
103
+ continue;
104
+ const d = dx * dx + dy * dy;
105
+ if (d <= bd) {
106
+ bd = d;
107
+ best = i;
108
+ }
109
+ }
110
+ return best;
111
+ }
112
+ const MIN_SPAN = 1e-7;
113
+ export const isHome = (v, h) => v.x1 - v.x0 >= h.x1 - h.x0 && v.y1 - v.y0 >= h.y1 - h.y0;
114
+ /** Keeps a window of the given span inside [h0, h1]. */
115
+ function fit(lo, span, h0, h1) {
116
+ if (span >= h1 - h0)
117
+ return h0;
118
+ return Math.min(Math.max(lo, h0), h1 - span);
119
+ }
120
+ /** Zooms by factor `k` (>1 zooms in) keeping the point at fractions (fx, fy from the top) of the plot fixed. */
121
+ export function zoomView(v, h, fx, fy, k) {
122
+ const sx = Math.min(h.x1 - h.x0, Math.max((h.x1 - h.x0) * MIN_SPAN, (v.x1 - v.x0) / k));
123
+ const sy = Math.min(h.y1 - h.y0, Math.max((h.y1 - h.y0) * MIN_SPAN, (v.y1 - v.y0) / k));
124
+ const ax = v.x0 + fx * (v.x1 - v.x0), ay = v.y1 - fy * (v.y1 - v.y0);
125
+ const x0 = fit(ax - fx * sx, sx, h.x0, h.x1), y1 = fit(ay - (1 - fy) * sy, sy, h.y0, h.y1);
126
+ return { x0, x1: x0 + sx, y0: y1, y1: y1 + sy };
127
+ }
128
+ /** Shifts the window by fractions of its own span (positive fx moves right, positive fy moves up). */
129
+ export function panView(v, h, fx, fy) {
130
+ const sx = v.x1 - v.x0, sy = v.y1 - v.y0;
131
+ const x0 = fit(v.x0 + fx * sx, sx, h.x0, h.x1), y0 = fit(v.y0 + fy * sy, sy, h.y0, h.y1);
132
+ return { x0, x1: x0 + sx, y0, y1: y0 + sy };
133
+ }
134
+ /** Fallback sequential ramp (colour-blind safe, readable on light and dark surfaces); overridden by `--trc-seq-1..9`. */
135
+ export const DEFAULT_RAMP = ['#3b528b', '#21918c', '#5ec962', '#fde725'];
136
+ /** `#rrggbb` or `rgb[a](r, g, b…)` (the forms a canvas returns for fillStyle) to [r, g, b]. */
137
+ export function parseColor(s) {
138
+ if (s[0] === '#')
139
+ return [parseInt(s.slice(1, 3), 16), parseInt(s.slice(3, 5), 16), parseInt(s.slice(5, 7), 16)];
140
+ const m = s.match(/[\d.]+/g).map(Number);
141
+ return [m[0], m[1], m[2]];
142
+ }
143
+ /** Interpolates the ramp at t in [0, 1]. */
144
+ export function rampColor(stops, t) {
145
+ const pos = Math.min(Math.max(t, 0), 1) * (stops.length - 1);
146
+ const i = Math.min(Math.floor(pos), stops.length - 2), f = pos - i;
147
+ const a = stops[i], b = stops[i + 1];
148
+ return `rgb(${Math.round(a[0] + (b[0] - a[0]) * f)},${Math.round(a[1] + (b[1] - a[1]) * f)},${Math.round(a[2] + (b[2] - a[2]) * f)})`;
149
+ }
package/dist/index.d.ts CHANGED
@@ -4,6 +4,10 @@ export { default as DonutChart } from './DonutChart.svelte';
4
4
  export { default as Histogram } from './Histogram.svelte';
5
5
  export { default as Sparkline } from './Sparkline.svelte';
6
6
  export { default as ScatterChart } from './ScatterChart.svelte';
7
+ export { default as DensityChart } from './DensityChart.svelte';
7
8
  export { default as StatTile } from './StatTile.svelte';
8
- export { seriesColor, niceTicks, linear } from './util.js';
9
- export type { Series, ScatterPoint } from './types.js';
9
+ export { default as ColorScale } from './ColorScale.svelte';
10
+ export { seriesColor, niceTicks, linear, gradientFrom } from './util.js';
11
+ export type { ScaleKind } from './util.js';
12
+ export type { Series, ScatterPoint, Band, ReferenceLine } from './types.js';
13
+ export type { DensityPoint } from './density.js';
package/dist/index.js CHANGED
@@ -4,5 +4,7 @@ export { default as DonutChart } from './DonutChart.svelte';
4
4
  export { default as Histogram } from './Histogram.svelte';
5
5
  export { default as Sparkline } from './Sparkline.svelte';
6
6
  export { default as ScatterChart } from './ScatterChart.svelte';
7
+ export { default as DensityChart } from './DensityChart.svelte';
7
8
  export { default as StatTile } from './StatTile.svelte';
8
- export { seriesColor, niceTicks, linear } from './util.js';
9
+ export { default as ColorScale } from './ColorScale.svelte';
10
+ export { seriesColor, niceTicks, linear, gradientFrom } from './util.js';
package/dist/types.d.ts CHANGED
@@ -20,3 +20,15 @@ export type ScatterPoint = {
20
20
  group?: string;
21
21
  label?: string;
22
22
  };
23
+ /** Shaded envelope for `LineChart`: `lo`/`hi` align with `x` by index, `null` leaves a gap. */
24
+ export type Band = {
25
+ lo: (number | null)[];
26
+ hi: (number | null)[];
27
+ color?: string;
28
+ label?: string;
29
+ };
30
+ export type ReferenceLine = {
31
+ value: number;
32
+ label?: string;
33
+ axis?: 'y';
34
+ };
package/dist/util.d.ts CHANGED
@@ -1,12 +1,27 @@
1
1
  export declare const MAX_SERIES = 8;
2
2
  /** Color follows the entity (its index in the full series list), never its rank among visible ones. */
3
3
  export declare const seriesColor: (i: number) => string;
4
+ export type ScaleKind = 'sequential' | 'diverging';
5
+ /** A CSS `linear-gradient` over the seven palette tokens (`--trc-seq-1..7` or `--trc-div-1..7`), low to high. `direction` is any gradient direction, default `to right`. Retheme-aware: it uses the tokens, not colours. */
6
+ export declare const gradientFrom: (kind: ScaleKind, direction?: string) => string;
4
7
  export declare const linear: (d0: number, d1: number, r0: number, r1: number) => (v: number) => number;
8
+ /** True for a finite value > 0: the only values a log axis can place. */
9
+ export declare const isPos: (v: number | null | undefined) => v is number;
10
+ /** Base-10 log scale; non-positive input clamps to the domain start. */
11
+ export declare const logLinear: (d0: number, d1: number, r0: number, r1: number) => (v: number) => number;
12
+ /** Ticks at powers of 10 covering [min, max]; the domain snaps outward to those powers. */
13
+ export declare function logTicks(min: number, max: number): {
14
+ ticks: number[];
15
+ min: number;
16
+ max: number;
17
+ };
5
18
  export declare function niceTicks(min: number, max: number, count?: number): {
6
19
  ticks: number[];
7
20
  min: number;
8
21
  max: number;
9
22
  };
23
+ /** Text for a value a consumer-supplied formatter returned: nothing (not "undefined") when it returned nothing. */
24
+ export declare const str: (v: unknown) => string;
10
25
  export declare const fmtAxis: (n: number) => string;
11
26
  export declare const fmtValue: (n: number) => string;
12
27
  export declare function fmtX(v: number, span: number, isDate: boolean): string;
package/dist/util.js CHANGED
@@ -1,7 +1,30 @@
1
1
  export const MAX_SERIES = 8;
2
2
  /** Color follows the entity (its index in the full series list), never its rank among visible ones. */
3
3
  export const seriesColor = (i) => `var(--trc-s${(i % MAX_SERIES) + 1})`;
4
+ /** A CSS `linear-gradient` over the seven palette tokens (`--trc-seq-1..7` or `--trc-div-1..7`), low to high. `direction` is any gradient direction, default `to right`. Retheme-aware: it uses the tokens, not colours. */
5
+ export const gradientFrom = (kind, direction = 'to right') => {
6
+ const p = kind === 'diverging' ? 'div' : 'seq';
7
+ return `linear-gradient(${direction}, ${[1, 2, 3, 4, 5, 6, 7].map((i) => `var(--trc-${p}-${i})`).join(', ')})`;
8
+ };
4
9
  export const linear = (d0, d1, r0, r1) => (v) => r0 + ((v - d0) / (d1 - d0 || 1)) * (r1 - r0);
10
+ /** True for a finite value > 0: the only values a log axis can place. */
11
+ export const isPos = (v) => v != null && v > 0 && isFinite(v);
12
+ /** Base-10 log scale; non-positive input clamps to the domain start. */
13
+ export const logLinear = (d0, d1, r0, r1) => {
14
+ const f = linear(Math.log10(d0), Math.log10(d1), r0, r1);
15
+ return (v) => f(Math.log10(v > 0 ? v : d0));
16
+ };
17
+ /** Ticks at powers of 10 covering [min, max]; the domain snaps outward to those powers. */
18
+ export function logTicks(min, max) {
19
+ const lo = Math.floor(Math.log10(min));
20
+ let hi = Math.ceil(Math.log10(max));
21
+ if (hi === lo)
22
+ hi++;
23
+ const ticks = [];
24
+ for (let k = lo; k <= hi; k++)
25
+ ticks.push(10 ** k);
26
+ return { ticks, min: 10 ** lo, max: 10 ** hi };
27
+ }
5
28
  export function niceTicks(min, max, count = 5) {
6
29
  if (min === max) {
7
30
  min -= 1;
@@ -21,6 +44,8 @@ export function niceTicks(min, max, count = 5) {
21
44
  }
22
45
  const compact = new Intl.NumberFormat('en-US', { notation: 'compact', maximumFractionDigits: 1 });
23
46
  const full = new Intl.NumberFormat('en-US', { maximumFractionDigits: 2 });
47
+ /** Text for a value a consumer-supplied formatter returned: nothing (not "undefined") when it returned nothing. */
48
+ export const str = (v) => (v == null ? '' : String(v));
24
49
  export const fmtAxis = (n) => compact.format(n);
25
50
  export const fmtValue = (n) => full.format(n);
26
51
  export function fmtX(v, span, isDate) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ruckc/truss-chart",
3
- "version": "1.6.0",
3
+ "version": "1.8.0",
4
4
  "description": "Zero-dependency SVG charts for Truss UI",
5
5
  "type": "module",
6
6
  "license": "MIT",