@zakkster/lite-chartforge
v1.0.0
Published
Data-viz palette designer. Perceptually linear, CVD-safe, APCA-annotated sequential, diverging and categorical ramps.
Maintainers
Readme
@zakkster/lite-chartforge
A data-viz palette designer built bottom-up in OKLCh. Reactive ramp construction, perceptual audits (CVD, APCA, gamut coverage, monotonicity), repair kernels (linearize L, equalize deltaE, chroma-only gamut compression), export adapters for d3 / Plotly / ECharts / Highcharts / matplotlib, and reactive value-to-color scales — all with zero-GC hot paths.
Single-file ESM per subpath, tree-shakeable, peers on @zakkster/lite-signal
and @zakkster/lite-ease.
npm install @zakkster/lite-chartforge @zakkster/lite-signal @zakkster/lite-easeThe pipeline
Chartforge is designed around a four-stage pipeline. Each stage lives at its own subpath and can be used independently.
design -> audit -> repair -> export
------ ----- ------ ------
core core ./repair ./vendor/{d3,plotly,echarts,...}
./scaleDesign a ramp reactively:
import { createSequentialRamp } from '@zakkster/lite-chartforge';
import { easeInOutQuad } from '@zakkster/lite-ease';
const ramp = createSequentialRamp({
from: { l: 0.20, c: 0.10, h: 280 },
to: { l: 0.90, c: 0.18, h: 100 },
curve: easeInOutQuad,
huePath: 'short',
steps: 256
});Audit for perceptual issues:
import { auditRamp } from '@zakkster/lite-chartforge';
const report = auditRamp(ramp);
// {
// monotonic: { passed: true, deltaE_cv: 0.046, ... },
// gamut: { srgb: 0.47, p3: 0.63, rec2020: 1.0, violations: [...] },
// cvd: { protanopia: true, deuteranopia: true, tritanopia: true, ... },
// apca: { adjacent: [...], violations: [] }
// }Repair issues perceptually (never by naive RGB clipping):
import { compressGamut, equalizeDeltaE } from '@zakkster/lite-chartforge/repair';
// Reduce chroma at constant L and h until every stop fits sRGB.
const compressed = compressGamut(ramp, 'srgb');
// Then rebalance for uniform adjacent deltaEok.
const final = equalizeDeltaE(compressed);Export to your chart library of choice:
import { toPlotlyColorscale } from '@zakkster/lite-chartforge/vendor/plotly';
import { toD3ColorArray } from '@zakkster/lite-chartforge/vendor/d3';
import { toMatplotlibPython } from '@zakkster/lite-chartforge/vendor/matplotlib';
Plotly.newPlot(el, [{
z: data, type: 'heatmap',
colorscale: toPlotlyColorscale(final)
}]);Or scale values to colors reactively:
import { createSequentialScale } from '@zakkster/lite-chartforge/scale';
import { effect } from '@zakkster/lite-signal';
const { scale, setDomain } = createSequentialScale(ramp, { domain: [0, 100] });
effect(() => {
const fn = scale(); // subscribes to ramp + domain changes
for (const point of data) {
point.color = fn(point.value); // fast, no signal reads in the loop
}
renderChart(data);
});Subpaths
| Import path | What it gives you |
| --------------------------------------------------- | ---------------------------------------------------- |
| @zakkster/lite-chartforge | Ramps, palettes, audits — the core. |
| @zakkster/lite-chartforge/repair | linearizeLightness, equalizeDeltaE, compressGamut, and per-arm diverging variants. |
| @zakkster/lite-chartforge/scale | Reactive value-to-color scale primitives. |
| @zakkster/lite-chartforge/vendor | Generic shape utilities (hex array, stops, RGB tuples, oklch strings). |
| @zakkster/lite-chartforge/vendor/d3 | d3-shape adapters. |
| @zakkster/lite-chartforge/vendor/plotly | Plotly colorscale + colorway. |
| @zakkster/lite-chartforge/vendor/echarts | ECharts visualMap. |
| @zakkster/lite-chartforge/vendor/highcharts | Highcharts colorAxis. |
| @zakkster/lite-chartforge/vendor/matplotlib | JSON payload + Python source for ListedColormap. |
The zero-GC contract
The core sample, at, and swatches paths write into caller-owned storage
without allocation. Reactive ramps own a stable-identity Float64Array(3 * steps)
LUT slab that is rebuilt in place when inputs change.
For reactive repair callers, every kernel accepts caller-owned scratch buffers:
const outSlab = new Float64Array(3 * 256);
const cumBuffer = new Float64Array(256);
const scratchLabA = new Float64Array(3);
const scratchLabB = new Float64Array(3);
effect(() => {
equalizeDeltaE(ramp, { outSlab, cumBuffer, scratchLabA, scratchLabB });
renderPalette(outSlab);
});With all opts provided, the entire equalize path allocates zero bytes on subsequent reactive ticks — verified over 5000 calls in the test suite.
Live references warning
lut(), colors(), and swatches() return LIVE references into internal
storage. Do not store their results in React/Vue/Solid state directly:
shallow equality will silently miss updates, and cached snapshots will
mutate out from under you. Instead:
- Snapshot for framework state:
swatches().map(c => ({ ...c })) - Or read reactively: subscribe via
swatches.subscribe(fn), or read inside aneffect(...)from@zakkster/lite-signal.
Peer dependencies
@zakkster/lite-signal^1.3.0— reactive core@zakkster/lite-ease^1.1.0— curve functions
Ecosystem-compatible curve sources also work as curve params:
@zakkster/lite-ease-params (tunable Back/Elastic/Bounce factories) and
@zakkster/lite-cubic-bezier (CSS-compatible bezier runtime).
Related
Chartforge is one of the @zakkster/lite-* micro-libraries built for
allocation-sensitive JavaScript: reactive signals, DOM bindings, color
tooling, UI headless primitives, charts, netcode.
License
MIT (c) Zahary Shinikchiev
