@feugene/granularity-charts
v0.11.02026-08-30
Added
-
Chart components declare the layer tokens their frame reads. The tooltip of
GrChartFrameis positioned by the core’suseFloating: the layer name is passed to it as a parameter and thevar()is assembled at runtime inside the core’soverlayStack.ts. Neithervar(--gr-z-tooltip)norvar(--gr-z-modal)appears anywhere in this package.Measured with
granular prunebefore the change:gr-z-tooltipsurvived only because theGrChartFramegroup ships its shared SFC into a scanned directory, whilegr-z-modalwas removed outright — a chart opened inside a modal would have lost its tooltip stacking. Nine components now declare both.Gated by
src/__tests__/dynamicTokens.test.ts, which scans the component directory together with the shared directory of its group, since it is the frame that calls the composable.
v0.10.12026-08-27
Fixed
- Development warnings never reached the browser: the
__GR_DEV__guard included atypeof processcheck, andprocessis undefined in the browser, so the whole expression collapsed tofalsein development too. The guard now matches the core package. Production bundles are unaffected — the branch is still dropped.
v0.10.02026-08-27
Changed
-
Peer floors on
@feugene/*raised to the current minor. Every peer this package declares on the ecosystem now starts at the version the monorepo actually ships:@feugene/fint-i18n→>=0.7.0 <1.0.0@feugene/granularity→>=0.36.0 <1.0.0@feugene/unocss-preset-granular→>=0.13.0 <1.0.0@feugene/unplugin-granularity→>=0.7.0 <1.0.0
The floors had drifted far behind — some still admitted releases from a year of development ago — and a range that claims support it was never tested against is worse than a narrow one: the install succeeds and the breakage surfaces later, in the consumer’s app.
This is breaking for anyone below a floor. Installing against an older
@feugene/granularitynow produces a peer conflict instead of silence. The fix is to move the core up; nothing in this package’s own API changed.
v0.9.02026-08-20
Changed
-
canvasThresholdnow counts drawn vertices, not total points, and defaults to24_000instead of2000. If you set this prop, re-read it: the old number now means “canvas almost always”.The old axis was simply the wrong one. Decimation caps each series at the resolution of the screen — about two vertices per pixel — so one series of 100 000 points draws as 2400 vertices and costs a millisecond, while twenty series of 2400 (the same 48 000 points) cost sixteen. Total points say nothing about the price;
series × drawn verticesdoes.Until now the prop gated markers rather than any canvas — there was no canvas in the package. That cap still exists, at the same number, as an internal constant: marker behaviour is unchanged.
Added
-
A canvas renderer above the threshold, for
GrChartLineandGrChartArea. Measured at 1200px wide with 2400 vertices per series: SVG grows linearly at ~0.8 ms per series and stops fitting a frame at twenty (16.3 ms); canvas stays near flat (1.7 ms). Below a few series the difference is noise, which is why the threshold is high rather than aggressive.Accessibility is untouched by the switch. The cursor, keyboard, tooltip and hidden table work off one transparent overlay and the full series — never off the marks — so they carry over unchanged. The canvas is
aria-hiddenand does not take pointer events. The accessibility suite runs against both renderers.canvasThreshold: 0disables canvas entirely, for when the drawing has to stay vector: printing, SVG export, custom CSS over the marks.Two consequences worth knowing: the grid moves into the canvas (it has to stay under the series, and the canvas sits under the
<svg>so that axes and the active point stay on top), and a gradient area fill becomes a solid one —url(#…)means nothing to a canvas, and twenty gradient-filled areas read as mush anyway. -
curveCommandsandcommandsToPathin the geometry module. Curve maths is now computed once into numeric draw commands, and thedstring and the canvas each read from it. The two renderers have nowhere to diverge — the equivalence is pinned by a test.
v0.8.12026-08-20
Fixed
- Two dev warnings no longer reach production. Repeated categories in a series and a series carrying
both
dataandx/yare reported throughconsole.warn, and neither call was behind a condition — the message shipped in consumers’ builds. The package now expands__GR_DEV__on build like the core does, and the pairedgr-check-dist-dev-guardruns onyarn buildso a substitution that stops working fails the build instead of reaching a consumer as__GR_DEV__ is not defined.
v0.8.02026-08-20
Added
- The x-axis window is operable from the keyboard, closing the gap
0.7.0shipped with.+/-zoom,Shift+arrows pan by a quarter of the window,0restores the full series. Zooming anchors on the active point rather than the window centre — anchored on the centre, the ends of a series would stay out of reach however long you held the key. The keys are not a mode: thezoomunion ('brush' | 'wheel' | 'both') names pointer gestures, and the keyboard is on whenever zoom is on. Making it switchable would offer a way to build a zoom that cannot be reached from a keyboard — WCAG 2.1 SC 2.1.1 — by simply not writing a line.Shift+arrow belongs to the window even at full range, where it does nothing: one chord must mean one thing rather than pan or move the cursor depending on state.Ctrl,AltandCmdcombinations are left to the browser. The hint naming these keys goes into the surface’s description, not its name: consumers override the name withariaLabelalmost every time, and a hint living there would vanish with it.
Changed
-
The hidden data table has a row cap:
dataTableMaxRows,'auto'by default. One row per point is readable while there are few rows; nobody reads ten thousand of them in sequence, and rebuilding that many costs on the order of a hundred milliseconds per zoom step. Above the cap the table prints the points the chart draws — same LTTB, same budget — and says so in atfootnote.'auto'means “as many rows as anyone can read”, not “whatever is drawn”: it takes the drawing’s budget when there is one — then the table matches the drawing exactly, down to the LTTB-selected points — and falls back to a flat 500-row ceiling with even sampling when there is none, which is the case for category scales and fordecimate: 'never'. Anything else would let the whole point be lost behind one toggle. The prop exists on all nine types that have a table; where the type builds its own table model (pie, radar, funnel, bullet, waterfall, heatmap) the same ceiling applies as a row sample, so no chart type is left without a way to bound it.This reverses the earlier rule that the table always prints every row. That rule assumed trimming would hand a blind reader different data than a sighted one sees; on a long series the assumption is false — a sighted reader does not read ten thousand values either, they read the shape and hover for specifics. A table trimmed to what is drawn gives exactly that shape, and the per-point truth stays with the keyboard, which still walks the full series and announces every point. The contract did not weaken, it got sharper: the table matches the drawing, the keyboard matches the data. What to enable is the application’s call — a number sets its own cap,
Infinityremoves it,dataTable: 'off'drops the table entirely. Measured on 10 000 points, a window change went from 119 ms with the full table to 14 ms under'auto', against 8 ms with no table at all. -
A decimated series now carries its own
byXindex.decimateSeriesspread the original series, so the index described the full row set whilepointsheld the trimmed one — harmless today, because nothing readbyXoff a decimated series, and a trap the moment something did. The table above the cap is that something. -
The hidden data table now follows the settled window rather than every step of it. It holds one row per point, so rebuilding ten thousand of them costs on the order of a hundred milliseconds, while the wheel and key auto-repeat change the window dozens of times a second — a synchronous table turned the gesture into a queue of repaints the chart never caught up with. Measured on a 10 000-point series, a window change went from 132 ms to 44 ms, and a continuous gesture now pays for one rebuild instead of one per step. The delay is 80 ms and applies to window changes only: new series, a hidden series or a different domain reach the table immediately. The contract is unchanged — at rest the table matches the drawing exactly; they differ only mid-gesture, when nobody is reading it, and the window change itself is announced through the live region synchronously.
v0.7.02026-08-19
Added
- Zoom into a stretch of the x axis.
GrChartLineandGrChartAreagainedzoom(false | 'brush' | 'wheel' | 'both', defaultfalse) andv-model:xWindow. The window selects data rather than cropping the drawing: it is applied inside normalisation, right after sorting and before stacking, so positions, cursor, keyboard, hidden data table and the value-axis extent all follow it. That is deliberately the opposite of decimation, and the rule behind both is the same — the accessible representation must match what is on screen, and what is on screen is the user’s choice. Decimation is invisible to the user, so hiding rows from a screen reader would be a lie; the window is visible, so the table follows it. One consequence is worth knowing: zooming resolves fine structure that reads as solid hatching at full range, because the decimation budget is measured against plot width and a narrow window holds fewer points, so each gets more vertices. The axis domain becomes the window itself rather than the extent of the surviving points, so a brushed stretch does not snap to the nearest data;includeXValuesno longer widens it, since a reference past the edge would undo the zoom;activeIndexaddresses the current window. A drag shorter than 4px is a click, not a brush, so picking a point still works;Escapecancels a drag in progress; the tooltip goes quiet while brushing; touch is left alone, because a drag across the canvas is how a page is scrolled. Category scales have no window, for the same reason they are never decimated. chart/chartZoom— the window arithmetic as a pure module:windowFromPixels,zoomWindow,clampWindowandsmallestGap. A consumer restoring a zoom level from the URL needs no chart to compute it.ChartDataalso reportsfullXDomain, the unwindowed extent — measure a gesture against the current window and there would be no way out of a zoom.alignedTicksandscaleForAxisare re-exported from@feugene/granularity-charts/chart. Both were announced in v0.4.0 but never left the barrel.
Changed
- The pointer hot path is indexed, not scanned. Every normalised series now carries
byX: ReadonlyMap<number, NormalizedPoint>, built in the same pass that reads the points. The tooltip’s active point,activeSymbolMarks, the hidden table and the bar chart’s tooltip anchor used to walk the whole series — up toS + 2full passes per change of active point, measured against complete series rather than decimated ones.barHitIndex, the only one that ran on everypointermovefor bars and waterfalls, is now a binary search; a per-pixel sweep test pins it to the old traversal, ragged position sets included.GrChartHeatmapbuilds one cell instead of the whole matrix for its anchor and active outline.
Fixed
- The
canvasThresholddoc block was attached to the wrong prop on bothGrChartLineandGrChartArea, and described the wrong mode: with default settings the threshold only gatesshowPoints: 'always', because'auto'stops drawing markers earlier, at sixty points. The same inaccuracy is corrected in the showcase, whose API tables were also missingdecimateandmaxPointsentirely. The binary-search note inchartScalesat onscaleForAxiswhile describingnearestIndex.
v0.6.02026-08-19
Added
GrChartBarlays bars sideways.orientation="horizontal"turns categories into rows, so a long department or product name reads as a line of text instead of a slanted, clipped tail — the scenario the docs used to redirect away from. Stacking, grouping,'100%', references, legend, tooltip and the hidden data table behave exactly as they do vertically. Axes keep their data names in both layouts:yDomain,yTickFormatandyTickCountalways address the value axis, soshowGrid: 'y'draws vertical lines when the chart is horizontal.dualAxisis not supported sideways — the second value axis would have to sit on top, where the layout reserves no room; the prop is ignored and dev builds warn. Keyboard follows the eye:ArrowDown/ArrowUpwalk the categories,ArrowLeft/ArrowRightswitch the series being read.- Long series are decimated for drawing (LTTB).
GrChartLineandGrChartAreagaineddecimate('auto' | 'always' | 'never', default'auto') andmaxPoints;GrSparklinesizes its budget from its fixed canvas and needs no prop at all. Decimation shortens thedstring and nothing else: the cursor, the keyboard, the tooltip and the hidden data table keep the full series, soEndstill lands on the ten-thousandth point and the table still prints every row. The budget is two vertices per pixel of plot width (at least 64), quantised to 32px so the path does not shimmer while a pane is resized; category scales are never decimated, gaps keep exactly one separator each, and a stack shares one set of abscissas across its group.
Fixed
- Axis labels no longer run off the canvas. SVG has no
text-overflow, so a category label wider than its gutter used to be cut by the canvas edge with no ellipsis — the reader saw the tail of a word with no sign that the start was missing. Labels are now trimmed to the reserved width with an ellipsis and carry the full text in<title>. The horizontal bar chart also reserves room for the outermost value tick, which is centred on its gridline and used to hang half its width past the edge. GrChartWaterfallkeyboard follows its horizontal layout. Sideways, the steps run top to bottom, but the arrows still walked them left to right.
Removed
rendererprop onGrChartLine. It was never read by anything, and the documentation promised a canvas path behind it that does not exist in this package. Long series are handled bydecimateinstead.
Earlier versions
The bodies of these releases are not shown: the full feed weighs more than a page is allowed to. The file in the repository is always complete.
v0.5.22026-08-19v0.5.12026-08-18v0.5.02026-08-18v0.4.02026-08-17v0.3.02026-08-14v0.2.02026-08-13