diff --git a/.changeset/issue-9-chart-authoring.md b/.changeset/issue-9-chart-authoring.md new file mode 100644 index 0000000..6aa1eae --- /dev/null +++ b/.changeset/issue-9-chart-authoring.md @@ -0,0 +1,7 @@ +--- +'@tanstack/charts': minor +--- + +Replace the flat axis guide options with composable axis, grid, tick, and +responsive label configuration. Add shared focus-layer marks, coordinate-based +tooltip anchoring, and inferred stacked or grouped mark authoring. diff --git a/API-FRICTION.md b/API-FRICTION.md index 1fe0fe6..23b4d10 100644 --- a/API-FRICTION.md +++ b/API-FRICTION.md @@ -5,7 +5,7 @@ observed difficulty from examples, production migrations, tests, and agent evaluations so later API, documentation, and TanStack Intent skill work is based on evidence. -Last updated: 2026-07-30 +Last updated: 2026-07-31 ## Triage rule @@ -189,6 +189,10 @@ Each entry records: | F-151 | Artifact actions targeted deprecated Node 20 | Tooling | resolved | | F-152 | Version bumps invalidated workspace bundle evidence | Tooling/Release | resolved | | F-153 | Changesets left release-facing version claims behind | Tooling/Release | resolved | +| F-154 | Focus presentation was fixed to one renderer marker | API | resolved | +| F-155 | Axis scale and presentation controls were interleaved | API | resolved | +| F-156 | Responsive tick labels had no collision policy | API | resolved | +| F-157 | Tooltip anchors could not fix coordinates independently | API | resolved | ## Findings @@ -375,11 +379,11 @@ Each entry records: scale with an authored string range/interpolator; bare D3 defaults are numeric or empty and fail instead of becoming invalid CSS paint. - Group inference: when `z` is omitted, connected line and area marks use - `color` to partition paths because one row-level color channel necessarily - identifies those paths. A grouped bar does the same only when `groupScale` - requests subgroup geometry. Explicit `z` remains authoritative and can - differ from `color`; point marks never change geometry merely because they - have a color channel. + discrete `color` to partition paths because one row-level color channel + necessarily identifies those paths. Bars use it as series identity after + stacked or explicit grouped geometry is known. Continuous color cannot infer + a series. Explicit `z` remains authoritative and can differ from `color`; + color never selects stacked versus grouped geometry. - Legend behavior: `colorLegend` uses swatches for categorical scales, a gradient for continuous and sequential scales, and exact stepped bins and boundaries for quantize, quantile, and threshold scales. @@ -454,14 +458,15 @@ Each entry records: thick and shifted each away from its categorical tick. - Expected: the supplied D3 band scale completely owns bar position and thickness. Series or color identity must not silently change geometry. -- Decision: bars fill the primary scale bandwidth by default, with no implicit - inset. `z` remains series identity and a color fallback. True side-by-side - bars inject a secondary D3 band scale through `groupScale`; TanStack copies - it and supplies the primary bandwidth as its responsive range. -- Verification: focused tests cover exact band starts, widths, centers, - source-scale immutability, non-positional `z`, and injected grouped scales. - Stats grouped and stacked snapshots now match Plot rectangle geometry in - both orientations. +- Decision: a single quantitative bar channel is a length and stacks + implicitly at repeated positions. Explicit endpoints opt out. Side-by-side + geometry requires `layout: group()`, optionally with a copied D3 band scale. + `z` supplies series identity; discrete `color` may infer identity only after + geometry is selected. `stackY` exposes order, reversal, diverging, + normalization, centering, and wiggle offsets for bars and vertical areas. +- Verification: focused tests cover implicit diverging bars, normalized and + ordered stacks, area stacks, explicit endpoints, explicit grouping, + source-scale immutability, and continuous-color rejection. ### F-014 — Responsive nicing duplicates layout calculations @@ -662,10 +667,11 @@ Each entry records: entire 320px surface. - Decision: make omitted margin sides automatic. Solve the minimum guide bounds from formatted text, anchors, and rotations; treat numeric sides as hard - overrides; expose resolved bounds for aligned application UI. Keep label - containment separate from tick collision. Content-dependent examples omit - margin locks, and axis titles use the same compact 10px typography as ticks - below 360px. + overrides; expose resolved bounds for aligned application UI. Candidate + generation now precedes rotated-label collision thinning, while containment + remains a separate constraint. The final thinned bounds feed the iterative + margin solver. Content-dependent examples omit margin locks, and axis titles + use the same compact 10px typography as ticks below 360px. - Verification: six guide-bound tests cover deterministic measurement, anchors, baselines, rotation, translated groups, and all four sides. Five scene-layout tests cover long labels and titles, rotated endpoints, narrow-to-wide @@ -1849,10 +1855,9 @@ Each entry records: four bars without temporal context. - Expected: compact and nested charts can independently show or hide each axis guide without custom rendering. -- Decision: add per-axis guide visibility at the narrow axis configuration - layer while retaining `guides: false` as the positionless-chart shorthand. - The option must not add a universal dependency or affect charts that keep - both guides. +- Decision: use `axis: false` at the narrow scale configuration layer while + retaining `guides: false` as the positionless-chart shorthand. Grid + visibility is independent from axis visibility. - Verification: scene-layout tests cover x-only, y-only, both, and neither, including automatic margins and grid suppression. The nested-tooltip case retains period labels while hiding its y guide. The full 79-case matrix @@ -3654,3 +3659,76 @@ Each entry records: - Verification: focused tests cover version-heading discovery, complete replacement, idempotency, and missing-version rejection. Generated package docs remain outputs of `pnpm docs:sync`, never hand-edited sources. + +### F-154 — Focus presentation was fixed to one renderer marker + +- Status: resolved +- Severity: high +- Owner: API +- Observed in: implementing the background-highlight feedback in issue #9 +- Friction: the host could paint only one hardcoded point marker. A focused + category band, rule, active bar, or custom effect required renderer-specific + DOM mutation or a second interaction loop. +- Decision: `whenFocused` filters an ordinary mark from the centralized + `ChartFocusState`. Matching supports primary, group, key, x, y, and series. + Mark order owns under/over placement; filtered marks infer scales but do not + add hit targets. Custom surfaces receive primary, group, source, and pinned + state. +- Verification: SVG tests filter band geometry by semantic x, Canvas tests + preserve the cached base layer while painting underlays and overlays, and + renderer tests verify pointer/keyboard source and pinned state. + +### F-155 — Axis scale and presentation controls were interleaved + +- Status: resolved +- Severity: high +- Owner: API +- Observed in: grid-without-tick-stubs feedback in issue #9 +- Friction: flat scale, axis, tick, title, and grid options coupled visibility. + Hiding the guide also removed grid lines; hiding stubs retained their + geometry in layout. +- Decision: keep `scale`, `nice`, `reverse`, and `grid` at the scale layer and + nest baseline, ticks, tick labels, and title under `axis`. `axis: false` + retains the scale, and `axis.ticks.size: 0` omits stub nodes and their space. +- Verification: scene-layout tests cover independent grid and axis visibility, + zero-size stub omission, formatter placement, title offsets, and mutually + exclusive candidate policies. The combined issue #9 foundation adds 1,340 + gzip bytes to the locked line scene and 2,883 gzip bytes to the + representative-mark entry; the reviewed universal baseline and isolated + ceilings record that cost. + +### F-156 — Responsive tick labels had no collision policy + +- Status: resolved +- Severity: high +- Owner: API +- Observed in: responsive-axis-label feedback in issue #9 +- Friction: authors could choose a tick count or rotation, but neither + guaranteed readable labels as length changed. There was no way to hard-keep + important interior labels. +- Decision: generate semantic candidates from exactly one of count, pixel + spacing, or explicit values; then rotate and collision-thin labels. Thinning + defaults on, categorical x softly prioritizes ends, and `keep` hard-retains + exact labels without adding grid lines or stubs. +- Verification: tests cover width-dependent candidate counts, rotated thinning, + the `thin: false` matrix, soft ends, hard interior retention, and kept + label-only values through iterative automatic margins. Shared facet axes + remeasure after their final cell width changes the thinning result; the + 320-pixel Anscombe case keeps its right edge label contained. That convergence + adds 0.25 kB gzip to the isolated facet bundle (18.65 kB total), covered by + its reviewed 18.8 kB ceiling without changing any exact universal baseline. + +### F-157 — Tooltip anchors could not fix coordinates independently + +- Status: resolved +- Severity: medium +- Owner: API +- Observed in: fixed-tooltip-placement feedback in issue #9 +- Friction: point, pointer, and group-center presets moved both coordinates. + Fixing a grouped tooltip to the plot top while following a value on x + required duplicated plot-bound calculations in a callback. +- Decision: accept `{ x, y }` anchors with point, pointer, value, group-center, + and plot-edge choices per coordinate. Callback context exposes complete + focus, pointer, plot, surface, and resolved-scale state. +- Verification: renderer tests cover mixed plot-center/plot-top anchoring, + keyboard pointer fallback, and the complete typed callback context. diff --git a/ISSUE-9-DESIGN-NOTES.md b/ISSUE-9-DESIGN-NOTES.md new file mode 100644 index 0000000..5ed9ae7 --- /dev/null +++ b/ISSUE-9-DESIGN-NOTES.md @@ -0,0 +1,448 @@ +# Issue #9 design decisions + +Source: https://github.com/TanStack/charts/issues/9 + +This is the working decision log for reviewing the issue sequentially. Update +it after each feedback item is settled. It records API direction, not an +implementation plan or compatibility promise. + +## Review status + +1. Background highlight and focused-mark presentation — implemented +2. Gridlines without tick stubs — implemented +3. Responsive axis labels — implemented +4. Fixed tooltip placement — implemented +5. Stacked and grouped bar authoring — implemented + +## 1. Focus presentation + +### Decision + +Model interaction presentation as ordinary marks filtered by one centralized +chart focus state. Do not create a separate renderer primitive for every +effect such as a point, band, rule, or active bar. + +Observable Plot's pointer render transform is the useful precedent: an +interactive transform filters an ordinary mark to the active datum. TanStack +should retain its stronger centralized focus resolution rather than letting +each interactive mark resolve the pointer independently. + +A representative API is: + +```ts +defineChart({ + focus: 'group-x', + marks: [ + whenFocused( + bandX(rows, { + x: 'date', + fill: '#94a3b8', + fillOpacity: 0.16, + inset: -6, + }), + { + match: 'x', + }, + ), + barY(rows, { + x: 'date', + y: 'value', + color: 'category', + }), + ], +}) +``` + +The implementation follows this contract: + +- one `ChartFocusState` drives tooltips and every focus-filtered mark; +- the state distinguishes the primary point, focused group, input source, and + pinned state; +- filters can match the primary point, focused group, stable point key, shared + x value, shared y value, or group; +- ordinary mark order controls whether an effect is before or after other + marks while axes retain their intended foreground placement; +- full mark data is available for channel and scale inference while the focus + filter controls the rows rendered in the transient state; +- the hardcoded focus circle becomes an implicit focus-filtered mark; +- custom renderers receive complete interaction state rather than + `paintFocus(point, points)`; +- SVG and Canvas must share the behavior without DOM mutation as the public + extension mechanism; +- pointer updates must not rebuild the complete chart scene or repaint + unrelated base geometry. + +### Related work to preserve in the design + +- Controlled or programmatic focus for linked charts, legends, and tables. +- Honest separation between semantic points, hit geometry, and presentation + geometry. +- Keyboard, pointer, pinned, restored, and programmatic focus parity. +- Deterministic composition, non-interactive effect nodes, and transient export + behavior. +- Facets and non-Cartesian coordinate systems must be supported by the general + transform contract; Cartesian helpers can remain conveniences. + +Specialized helpers such as `focusBandX` can be built later as compositions of +an ordinary mark and the focus filter. They should not define the core model. + +## 2. Axis and grid configuration + +### Decision + +Use the freedom to make breaking changes. Replace the current flat mixture of +scale behavior and guide presentation with a nested axis model. Keep grid +presentation independent from the axis. + +```ts +y: { + scale: scaleLinear, + nice: true, + grid: true, + axis: { + line: true, + ticks: { count: 5, size: 0, padding: 4, format: formatCurrency }, + tickLabels: { rotate: 0 }, + label: { + text: 'Revenue', + offset: 'auto', + }, + }, +} +``` + +The automatic-to-explicit range is: + +```ts +// Inferred axis. +y: { scale: scaleLinear } + +// Inferred axis plus grid. +y: { scale: scaleLinear, grid: true } + +// Labels and grid without tick stubs. +y: { + scale: scaleLinear, + grid: true, + axis: { ticks: { size: 0 } }, +} + +// Labels and grid without an axis baseline or tick stubs. +y: { + scale: scaleLinear, + grid: true, + axis: { + line: false, + ticks: { size: 0 }, + }, +} + +// Materialized scale without a visible axis. +y: { + scale: scaleLinear, + axis: false, +} +``` + +This replaces: + +- `guide` with `axis`; +- `ticks` with `axis.ticks.count`; +- `format` with `axis.ticks.format`; +- `tickRotate` with `axis.tickLabels.rotate`; +- `label` and `labelOffset` with `axis.label`; +- the current coupling between `guide: false` and `grid`. + +`axis.ticks.size: 0` should omit tick-stub nodes. Tick padding and automatic +guide margins must use the resolved tick size rather than preserving hidden +four-pixel geometry. + +Do not add explicit `axisX()` or `gridY()` guide marks based only on this +feedback. They introduce additional layout, duplication, positioning, and +facet semantics without current task evidence. + +## 3. Responsive tick labels + +### Decision + +Separate semantic tick generation from label layout. Length-aware scales and +axes choose candidate values first. Rotation and thinning then operate as +orthogonal label policies, with thinning enabled by default as the final +readability guarantee. + +```ts +x: { + scale: scaleBand, + axis: { + ticks: { + spacing: 80, + size: 0, + }, + tickLabels: { + rotate: -35, + thin: { + minGap: 8, + priority: 'ends', + keep: [launchDate], + }, + }, + }, +} +``` + +The resolution pipeline is: + +```text +available axis length +→ requested tick count +→ scale-generated candidate values +→ formatted and rotated label bounds +→ collision thinning +→ automatic guide margins +``` + +Candidate tick policies are mutually exclusive: + +```ts +ticks: { + spacing: 80 +} // length-aware count +ticks: { + count: 5 +} // explicit count hint +ticks: { + values: importantDates +} // exact candidates +``` + +The scale owns semantic candidates. A D3 scale may return a different number +than requested to preserve meaningful numeric or calendar intervals. Band +scales normally use their complete domain as the candidate set. + +Label policies are independent: + +```ts +tickLabels: {} // horizontal and automatically thinned +tickLabels: { rotate: -35 } // rotated and automatically thinned +tickLabels: { thin: false } // every horizontal label +tickLabels: { rotate: -35, thin: false } // every rotated label +tickLabels: false // no labels +``` + +Automatic rotation is not a default. Rotation changes reading direction and +chart height; authors opt into it. Thinning only prevents unreadable overlap +and remains enabled unless explicitly disabled. + +Thinning supports both soft priority and hard retention: + +```ts +tickLabels: { + thin: { + priority: 'ends', + keep: [launchDate, migrationDate], + }, +} +``` + +- `priority` influences the best collision-free subset. +- `keep` guarantees that exact labels render. +- Hard-kept labels are placed first and ordinary colliding labels are removed. +- If hard-kept labels collide with each other, both remain because the author + explicitly required them. +- Exact kept values are label-only by default; they do not implicitly add a + tick stub or gridline. + +For categorical x axes, first and last candidates receive soft priority by +default. They are not hard-kept when the available length cannot fit both. + +Gridlines and tick stubs use the scale-generated candidates before label +thinning. Hiding a label does not remove its stub or gridline. A shorter axis +may still produce fewer gridlines when its length-aware scale generates fewer +candidate ticks. + +Responsive scene layout iterates candidate resolution, label measurement, +thinning, and margins until stable. It uses a conservative result if a +threshold oscillates. Font loading and resize relayout may revise the visible +subset without restarting mark animation. + +## 4. Tooltip anchoring along an axis + +### Decision + +Allow each tooltip anchor coordinate to select its source independently. +Avoid combinatorial presets such as `group-top`, `pointer-top`, and +`value-bottom`. + +```ts +tooltip: { + anchor: { + x: 'value', + y: 'plot-top', + }, + placement: 'bottom', + offset: 12, +} +``` + +`x: 'value'` maps the primary focus point's semantic `xValue` through the +resolved x scale. For dodged bars, this locates the outer category center +rather than a subgroup center or an average that changes when a series is +missing. + +Coordinate sources are axis-specific: + +```ts +anchor: { + x: + | 'point' + | 'pointer' + | 'value' + | 'group-center' + | 'plot-left' + | 'plot-center' + | 'plot-right', + y: + | 'point' + | 'pointer' + | 'value' + | 'group-center' + | 'plot-top' + | 'plot-center' + | 'plot-bottom', +} +``` + +Existing whole-anchor shorthands such as `point`, `pointer`, and +`group-center` can expand to the corresponding x/y pair. + +The full callback remains available and receives complete interaction and +geometry context: + +```ts +anchor: (_points, { focus, pointer, plot, surface, scales }) => ({ + x: scales.x.map(focus.primary.xValue), + y: plot.y, +}) +``` + +Use `plot` for inner plotting bounds rather than the current misleading +`chart` context property. `surface` describes the complete rendered size. + +Fallbacks resolve per coordinate: + +- an unavailable pointer coordinate falls back to the primary point; +- a non-finite semantic value mapping falls back to the primary point; +- an empty group falls back to the primary point; +- an invalid custom coordinate falls back to the primary point. + +## 5. Stacked and grouped mark authoring + +### Decision + +Treat stacking and grouping as different geometric capabilities. Series +identity may be inferred from appearance after the geometry is known, but an +appearance channel must not select the geometry. + +For stackable interval marks, a single value channel represents a length and +is converted to endpoints by an implicit stack transform: + +```ts +barY(rows, { + x: 'date', + y: 'value', + color: 'category', +}) +``` + +Explicit endpoints opt out: + +```ts +barY(rows, { + x: 'date', + y1: 'start', + y2: 'end', + color: 'category', +}) +``` + +Stack behavior can be configured through a reusable transform: + +```ts +barY( + rows, + stackY( + { + order: 'input', + offset: 'normalize', + }, + { + x: 'date', + y: 'value', + color: 'category', + }, + ), +) +``` + +The semantics follow Observable Plot's coherent distinction: `y` is a length, +while `y1` and `y2` are already resolved extents. `stackY` is shared by +vertical bars and areas. + +Grouping remains an explicit geometric choice: + +```ts +barY(rows, { + x: 'date', + y: 'value', + color: 'category', + layout: group(), +}) +``` + +The fully explicit form supplies series identity independently: + +```ts +barY(rows, { + x: 'date', + y: 'value', + z: 'category', + color: 'category', + layout: group(), +}) +``` + +Series resolution obeys these rules: + +- explicit `z` wins; +- otherwise, a discrete color channel may infer series identity once the + geometry is known; +- a continuous color channel cannot infer series identity; +- color alone never switches a mark between stacked and grouped geometry; +- grouping without explicit or inferable series identity is a configuration + error. + +Therefore, supplying only `color` to a stackable interval mark produces +colored stacked intervals. Authors expecting side-by-side intervals must +request grouping explicitly. + +Do not expose one generic `layout` option on every mark. Capabilities follow +mark semantics: + +- bars support implicit length-to-extent stacking and explicit grouping; +- areas share the reusable stack transform; +- lines use positional values and do not stack by default, although a + discrete stroke or color may infer the separate paths required to render + series; +- dots, text, and rules overlap at repeated positions unless an explicit + displacement transform such as dodge or jitter is used; +- cells and heatmaps use aggregation or binning rather than stacking; +- arcs use a dedicated angular or pie transform. + +Computed geometry is part of the transform contract: + +- scale domains use computed extents; +- labels and custom renderers can access endpoints and midpoints; +- tooltips receive the original value and computed endpoints; +- focus, legends, and interaction use the resolved series identity; +- transformed points preserve their original datum; +- types expose only the capabilities supported by each mark. diff --git a/README.md b/README.md index cffe0a2..6da1345 100644 --- a/README.md +++ b/README.md @@ -91,8 +91,8 @@ const revenueChart = defineChart({ y: { scale: scaleLinear, nice: true, - label: 'Revenue', grid: true, + axis: { label: 'Revenue' }, }, tooltip: true, }) diff --git a/benchmarks/bundle-size/universal-baseline.json b/benchmarks/bundle-size/universal-baseline.json index d45f226..c29314a 100644 --- a/benchmarks/bundle-size/universal-baseline.json +++ b/benchmarks/bundle-size/universal-baseline.json @@ -3,36 +3,36 @@ "policy": "Exact minified and gzip output for entries that optional features must not affect. Review every change before updating.", "bundles": { "D3-scale line scene": { - "bytes": 36320, - "gzip": 14119 + "bytes": 40051, + "gzip": 15459 }, "D3-scale line + static SVG": { - "bytes": 39372, - "gzip": 15238 + "bytes": 42992, + "gzip": 16531 }, "Representative marks": { - "bytes": 49134, - "gzip": 17884 + "bytes": 57148, + "gzip": 20767 }, "TanStack DOM host": { - "bytes": 42903, - "gzip": 15701 + "bytes": 48497, + "gzip": 17658 }, "React adapter": { - "bytes": 46343, - "gzip": 16906 + "bytes": 51940, + "gzip": 18833 }, "React line consumer": { - "bytes": 68664, - "gzip": 25712 + "bytes": 74296, + "gzip": 27655 }, "Custom-scale line scene": { - "bytes": 17096, - "gzip": 6432 + "bytes": 20832, + "gzip": 7767 }, "D3 linear-scale line scene": { - "bytes": 36252, - "gzip": 14084 + "bytes": 39983, + "gzip": 15424 } } } diff --git a/benchmarks/comparison/bundle-baseline.json b/benchmarks/comparison/bundle-baseline.json index d5ba33b..78a40d3 100644 --- a/benchmarks/comparison/bundle-baseline.json +++ b/benchmarks/comparison/bundle-baseline.json @@ -1,8 +1,8 @@ { "schemaVersion": 3, - "generatedAt": "2026-07-31T05:39:15.459Z", + "generatedAt": "2026-07-31T07:24:01.788Z", "packageVersions": { - "tanstack": "0.0.1", + "tanstack": "0.0.2", "chartjs": "4.5.1", "echarts": "6.1.0", "recharts": "3.10.1", @@ -11,7 +11,7 @@ "sources": { "tanstack": { "kind": "workspace", - "revision": "5c36a3866d71ad94cbcc934272e2cd7a868065f3" + "revision": "093f81c8ad32ce7cc269ebe65f13028707621a4a" }, "chartjs": { "kind": "package", @@ -44,88 +44,88 @@ }, "bundles": { "tanstack-line-basic": { - "minifiedBytes": 65886, - "gzipBytes": 24861, - "brotliBytes": 22188, - "incrementalGzipBytes": 24861, - "incrementalBrotliBytes": 22188 + "minifiedBytes": 71553, + "gzipBytes": 26826, + "brotliBytes": 23930, + "incrementalGzipBytes": 26826, + "incrementalBrotliBytes": 23930 }, "tanstack-line-interactive": { - "minifiedBytes": 69927, - "gzipBytes": 26070, - "brotliBytes": 23217, - "incrementalGzipBytes": 26070, - "incrementalBrotliBytes": 23217 + "minifiedBytes": 75590, + "gzipBytes": 28043, + "brotliBytes": 24866, + "incrementalGzipBytes": 28043, + "incrementalBrotliBytes": 24866 }, "tanstack-line-advanced": { - "minifiedBytes": 77107, - "gzipBytes": 28419, - "brotliBytes": 25167, - "incrementalGzipBytes": 28419, - "incrementalBrotliBytes": 25167 + "minifiedBytes": 82771, + "gzipBytes": 30321, + "brotliBytes": 26977, + "incrementalGzipBytes": 30321, + "incrementalBrotliBytes": 26977 }, "tanstack-bar-basic": { - "minifiedBytes": 67480, - "gzipBytes": 25415, - "brotliBytes": 22635, - "incrementalGzipBytes": 25415, - "incrementalBrotliBytes": 22635 + "minifiedBytes": 76963, + "gzipBytes": 28916, + "brotliBytes": 25738, + "incrementalGzipBytes": 28916, + "incrementalBrotliBytes": 25738 }, "tanstack-bar-interactive": { - "minifiedBytes": 71517, - "gzipBytes": 26628, - "brotliBytes": 23703, - "incrementalGzipBytes": 26628, - "incrementalBrotliBytes": 23703 + "minifiedBytes": 81001, + "gzipBytes": 30134, + "brotliBytes": 26746, + "incrementalGzipBytes": 30134, + "incrementalBrotliBytes": 26746 }, "tanstack-bar-advanced": { - "minifiedBytes": 72907, - "gzipBytes": 27190, - "brotliBytes": 24112, - "incrementalGzipBytes": 27190, - "incrementalBrotliBytes": 24112 + "minifiedBytes": 81340, + "gzipBytes": 30284, + "brotliBytes": 26868, + "incrementalGzipBytes": 30284, + "incrementalBrotliBytes": 26868 }, "tanstack-area-basic": { - "minifiedBytes": 65765, - "gzipBytes": 24849, - "brotliBytes": 22138, - "incrementalGzipBytes": 24849, - "incrementalBrotliBytes": 22138 + "minifiedBytes": 75033, + "gzipBytes": 28178, + "brotliBytes": 25197, + "incrementalGzipBytes": 28178, + "incrementalBrotliBytes": 25197 }, "tanstack-area-interactive": { - "minifiedBytes": 69806, - "gzipBytes": 26069, - "brotliBytes": 23238, - "incrementalGzipBytes": 26069, - "incrementalBrotliBytes": 23238 + "minifiedBytes": 79074, + "gzipBytes": 29407, + "brotliBytes": 26187, + "incrementalGzipBytes": 29407, + "incrementalBrotliBytes": 26187 }, "tanstack-area-advanced": { - "minifiedBytes": 78219, - "gzipBytes": 28894, - "brotliBytes": 25616, - "incrementalGzipBytes": 28894, - "incrementalBrotliBytes": 25616 + "minifiedBytes": 86437, + "gzipBytes": 31782, + "brotliBytes": 28205, + "incrementalGzipBytes": 31782, + "incrementalBrotliBytes": 28205 }, "tanstack-scatter-basic": { - "minifiedBytes": 65638, - "gzipBytes": 24781, - "brotliBytes": 22078, - "incrementalGzipBytes": 24781, - "incrementalBrotliBytes": 22078 + "minifiedBytes": 71258, + "gzipBytes": 26744, + "brotliBytes": 23802, + "incrementalGzipBytes": 26744, + "incrementalBrotliBytes": 23802 }, "tanstack-scatter-interactive": { - "minifiedBytes": 69679, - "gzipBytes": 26007, - "brotliBytes": 23120, - "incrementalGzipBytes": 26007, - "incrementalBrotliBytes": 23120 + "minifiedBytes": 75295, + "gzipBytes": 27964, + "brotliBytes": 24824, + "incrementalGzipBytes": 27964, + "incrementalBrotliBytes": 24824 }, "tanstack-scatter-advanced": { - "minifiedBytes": 69695, - "gzipBytes": 26012, - "brotliBytes": 23107, - "incrementalGzipBytes": 26012, - "incrementalBrotliBytes": 23107 + "minifiedBytes": 75311, + "gzipBytes": 27969, + "brotliBytes": 24881, + "incrementalGzipBytes": 27969, + "incrementalBrotliBytes": 24881 }, "chartjs-line-basic": { "minifiedBytes": 137909, diff --git a/benchmarks/comparison/libraries/tanstack/area.ts b/benchmarks/comparison/libraries/tanstack/area.ts index e95ef14..5c324a6 100644 --- a/benchmarks/comparison/libraries/tanstack/area.ts +++ b/benchmarks/comparison/libraries/tanstack/area.ts @@ -43,13 +43,13 @@ const definition = (input: BenchmarkInput) => ? [xMinimum(input), xMaximum(input)] : [0, Math.max(1, input.rows.length - 1)], ), - ticks: 6, grid: true, + axis: { ticks: { count: 6 } }, }, y: { scale: scaleLinear().domain([0, BENCHMARK_ADVANCED ? 200 : 100]), - ticks: 5, grid: true, + axis: { ticks: { count: 5 } }, }, color: BENCHMARK_INTERACTIVE ? { diff --git a/benchmarks/comparison/libraries/tanstack/bar.ts b/benchmarks/comparison/libraries/tanstack/bar.ts index 770fb23..7e0d3a5 100644 --- a/benchmarks/comparison/libraries/tanstack/bar.ts +++ b/benchmarks/comparison/libraries/tanstack/bar.ts @@ -38,8 +38,8 @@ const definition = (input: BenchmarkInput) => }, y: { scale: scaleLinear().domain([0, BENCHMARK_ADVANCED ? 200 : 100]), - ticks: 5, grid: true, + axis: { ticks: { count: 5 } }, }, color: BENCHMARK_INTERACTIVE ? { diff --git a/benchmarks/comparison/libraries/tanstack/base.ts b/benchmarks/comparison/libraries/tanstack/base.ts index ced40db..1528937 100644 --- a/benchmarks/comparison/libraries/tanstack/base.ts +++ b/benchmarks/comparison/libraries/tanstack/base.ts @@ -163,9 +163,11 @@ export function mountDefinition( typeof point.xValue === 'number' ? point.xValue : undefined, } } - const points = container.querySelectorAll( - 'svg circle[data-ts-key]:not([data-ts-chart-focus])', - ) + const points = [ + ...container.querySelectorAll( + 'svg circle[data-ts-key]', + ), + ].filter((element) => !element.closest('[data-ts-focus-layer]')) const point = points[ Math.round( diff --git a/benchmarks/comparison/libraries/tanstack/line.ts b/benchmarks/comparison/libraries/tanstack/line.ts index 1893722..b36459c 100644 --- a/benchmarks/comparison/libraries/tanstack/line.ts +++ b/benchmarks/comparison/libraries/tanstack/line.ts @@ -38,13 +38,13 @@ const definition = (input: BenchmarkInput) => ? [xMinimum(input), xMaximum(input)] : [0, Math.max(1, input.rows.length - 1)], ), - ticks: 6, grid: true, + axis: { ticks: { count: 6 } }, }, y: { scale: scaleLinear().domain([0, 100]), - ticks: 5, grid: true, + axis: { ticks: { count: 5 } }, }, color: BENCHMARK_INTERACTIVE ? { diff --git a/benchmarks/comparison/libraries/tanstack/scatter.ts b/benchmarks/comparison/libraries/tanstack/scatter.ts index 6a281fb..8296114 100644 --- a/benchmarks/comparison/libraries/tanstack/scatter.ts +++ b/benchmarks/comparison/libraries/tanstack/scatter.ts @@ -30,13 +30,13 @@ const definition = (input: BenchmarkInput) => ? [xMinimum(input), xMaximum(input)] : [0, Math.max(1, input.rows.length - 1)], ), - ticks: 6, grid: true, + axis: { ticks: { count: 6 } }, }, y: { scale: scaleLinear().domain([0, 100]), - ticks: 5, grid: true, + axis: { ticks: { count: 5 } }, }, color: BENCHMARK_INTERACTIVE ? { diff --git a/benchmarks/conformance/cases/01-line-gaps/tanstack.ts b/benchmarks/conformance/cases/01-line-gaps/tanstack.ts index aa7b5df..1580a8a 100644 --- a/benchmarks/conformance/cases/01-line-gaps/tanstack.ts +++ b/benchmarks/conformance/cases/01-line-gaps/tanstack.ts @@ -16,15 +16,8 @@ const definition = (input: ConformanceInput) => { strokeWidth: 2.25, }), ], - x: { - scale: scaleUtc, - label: 'Week', - }, - y: { - scale: scaleLinear, - label: 'Close (USD)', - grid: true, - }, + x: { scale: scaleUtc, axis: { label: 'Week' } }, + y: { scale: scaleLinear, grid: true, axis: { label: 'Close (USD)' } }, }) } diff --git a/benchmarks/conformance/cases/02-multi-line-end-labels/tanstack.ts b/benchmarks/conformance/cases/02-multi-line-end-labels/tanstack.ts index b4ce39b..c66ce8d 100644 --- a/benchmarks/conformance/cases/02-multi-line-end-labels/tanstack.ts +++ b/benchmarks/conformance/cases/02-multi-line-end-labels/tanstack.ts @@ -31,14 +31,11 @@ const definition = (input: ConformanceInput) => { fontWeight: 600, }), ], - x: { - scale: scaleUtc, - label: 'Week', - }, + x: { scale: scaleUtc, axis: { label: 'Week' } }, y: { scale: scaleLinear, - label: 'Unemployed (thousands)', grid: true, + axis: { label: 'Unemployed (thousands)' }, }, color: { range: colors, diff --git a/benchmarks/conformance/cases/03-temperature-range-band/tanstack.ts b/benchmarks/conformance/cases/03-temperature-range-band/tanstack.ts index b77bf27..00ff616 100644 --- a/benchmarks/conformance/cases/03-temperature-range-band/tanstack.ts +++ b/benchmarks/conformance/cases/03-temperature-range-band/tanstack.ts @@ -27,15 +27,8 @@ const definition = (input: ConformanceInput) => { strokeWidth: 1.75, }), ], - x: { - scale: scaleUtc, - label: 'Week', - }, - y: { - scale: scaleLinear, - label: 'Temperature (°F)', - grid: true, - }, + x: { scale: scaleUtc, axis: { label: 'Week' } }, + y: { scale: scaleLinear, grid: true, axis: { label: 'Temperature (°F)' } }, }) } diff --git a/benchmarks/conformance/cases/04-stacked-time-area/tanstack.ts b/benchmarks/conformance/cases/04-stacked-time-area/tanstack.ts index 528f409..ce3d160 100644 --- a/benchmarks/conformance/cases/04-stacked-time-area/tanstack.ts +++ b/benchmarks/conformance/cases/04-stacked-time-area/tanstack.ts @@ -44,14 +44,11 @@ const definition = (_input: ConformanceInput) => { }), ruleY([0]), ], - x: { - scale: scaleUtc, - label: 'Month', - }, + x: { scale: scaleUtc, axis: { label: 'Month' } }, y: { scale: scaleLinear, - label: 'Unemployed (thousands)', grid: true, + axis: { label: 'Unemployed (thousands)' }, }, color: { range: colors, diff --git a/benchmarks/conformance/cases/13-interval-timeline/tanstack.ts b/benchmarks/conformance/cases/13-interval-timeline/tanstack.ts index 765ee6f..28d425b 100644 --- a/benchmarks/conformance/cases/13-interval-timeline/tanstack.ts +++ b/benchmarks/conformance/cases/13-interval-timeline/tanstack.ts @@ -25,14 +25,10 @@ const definition = (input: ConformanceInput) => { radius: 3, }), ], - x: { - scale: scaleLinear, - grid: true, - label: 'Share price ($)', - }, + x: { scale: scaleLinear, grid: true, axis: { label: 'Share price ($)' } }, y: { scale: () => scaleBand().paddingInner(0.16), - format: (value) => date.format(value), + axis: { ticks: { format: (value) => date.format(value) } }, }, color: { range: colors, diff --git a/benchmarks/conformance/cases/14-error-bars/tanstack.ts b/benchmarks/conformance/cases/14-error-bars/tanstack.ts index a3347ac..1aac84c 100644 --- a/benchmarks/conformance/cases/14-error-bars/tanstack.ts +++ b/benchmarks/conformance/cases/14-error-bars/tanstack.ts @@ -43,11 +43,7 @@ const definition = (input: ConformanceInput) => { x: { scale: () => scaleBand().padding(0.22), }, - y: { - scale: scaleLinear, - grid: true, - label: 'Body mass (g)', - }, + y: { scale: scaleLinear, grid: true, axis: { label: 'Body mass (g)' } }, }) } diff --git a/benchmarks/conformance/cases/15-boxplot/tanstack.ts b/benchmarks/conformance/cases/15-boxplot/tanstack.ts index 5b171f5..d41ebe8 100644 --- a/benchmarks/conformance/cases/15-boxplot/tanstack.ts +++ b/benchmarks/conformance/cases/15-boxplot/tanstack.ts @@ -54,7 +54,7 @@ const definition = () => y: { scale: scaleLinear, grid: true, - label: 'Speed of light (km/s minus 299,000)', + axis: { label: 'Speed of light (km/s minus 299,000)' }, }, }) diff --git a/benchmarks/conformance/cases/16-lollipop/tanstack.ts b/benchmarks/conformance/cases/16-lollipop/tanstack.ts index 6fa8a00..e3ca1db 100644 --- a/benchmarks/conformance/cases/16-lollipop/tanstack.ts +++ b/benchmarks/conformance/cases/16-lollipop/tanstack.ts @@ -33,8 +33,10 @@ const definition = (_input: ConformanceInput) => { y: { scale: scaleLinear, grid: true, - label: 'Frequency', - format: (value) => percent.format(value), + axis: { + ticks: { format: (value) => percent.format(value) }, + label: 'Frequency', + }, }, }) } diff --git a/benchmarks/conformance/cases/17-dumbbell/tanstack.ts b/benchmarks/conformance/cases/17-dumbbell/tanstack.ts index 04f7f88..41575d4 100644 --- a/benchmarks/conformance/cases/17-dumbbell/tanstack.ts +++ b/benchmarks/conformance/cases/17-dumbbell/tanstack.ts @@ -32,7 +32,7 @@ const definition = (input: ConformanceInput) => { x: { scale: scaleLinear, grid: true, - label: '90th/10th percentile wage ratio', + axis: { label: '90th/10th percentile wage ratio' }, }, y: { scale: () => scaleBand().padding(0.22), diff --git a/benchmarks/conformance/cases/18-cumulative-histogram/tanstack.ts b/benchmarks/conformance/cases/18-cumulative-histogram/tanstack.ts index ba392cd..e3784f2 100644 --- a/benchmarks/conformance/cases/18-cumulative-histogram/tanstack.ts +++ b/benchmarks/conformance/cases/18-cumulative-histogram/tanstack.ts @@ -51,16 +51,8 @@ const definition = (input: ConformanceInput) => { inset: 1, }), ], - x: { - scale: scaleLinear, - grid: true, - label: 'Weight (kg)', - }, - y: { - scale: scaleLinear, - grid: true, - label: 'Cumulative count', - }, + x: { scale: scaleLinear, grid: true, axis: { label: 'Weight (kg)' } }, + y: { scale: scaleLinear, grid: true, axis: { label: 'Cumulative count' } }, }) } diff --git a/benchmarks/conformance/cases/19-moving-average-line/tanstack.ts b/benchmarks/conformance/cases/19-moving-average-line/tanstack.ts index 6486751..b8fb3da 100644 --- a/benchmarks/conformance/cases/19-moving-average-line/tanstack.ts +++ b/benchmarks/conformance/cases/19-moving-average-line/tanstack.ts @@ -30,14 +30,11 @@ const definition = (_input: ConformanceInput) => { strokeDasharray: '4 4', }), ], - x: { - scale: scaleUtc, - label: 'Date', - }, + x: { scale: scaleUtc, axis: { label: 'Date' } }, y: { scale: scaleLinear, grid: true, - label: 'Fourteen-day average temperature (°F)', + axis: { label: 'Fourteen-day average temperature (°F)' }, }, }) } diff --git a/benchmarks/conformance/cases/20-normalized-stacked-area/tanstack.ts b/benchmarks/conformance/cases/20-normalized-stacked-area/tanstack.ts index 4590ed1..2cfb520 100644 --- a/benchmarks/conformance/cases/20-normalized-stacked-area/tanstack.ts +++ b/benchmarks/conformance/cases/20-normalized-stacked-area/tanstack.ts @@ -46,15 +46,11 @@ const definition = (_input: ConformanceInput) => { }), ruleY([0]), ], - x: { - scale: scaleUtc, - label: 'Month', - }, + x: { scale: scaleUtc, axis: { label: 'Month' } }, y: { scale: scaleLinear().domain([0, 1]), grid: true, - label: 'Share of unemployment', - format: percent, + axis: { ticks: { format: percent }, label: 'Share of unemployment' }, }, color: { range: colors, diff --git a/benchmarks/conformance/cases/21-streamgraph/tanstack.ts b/benchmarks/conformance/cases/21-streamgraph/tanstack.ts index 2084280..b7bf7a1 100644 --- a/benchmarks/conformance/cases/21-streamgraph/tanstack.ts +++ b/benchmarks/conformance/cases/21-streamgraph/tanstack.ts @@ -43,14 +43,11 @@ const definition = (_input: ConformanceInput) => { fillOpacity: 0.85, }), ], - x: { - scale: scaleUtc, - label: 'Month', - }, + x: { scale: scaleUtc, axis: { label: 'Month' } }, y: { scale: scaleLinear, grid: true, - label: 'Unemployed (thousands)', + axis: { label: 'Unemployed (thousands)' }, }, color: { range: colors, diff --git a/benchmarks/conformance/cases/22-bollinger-band/tanstack.ts b/benchmarks/conformance/cases/22-bollinger-band/tanstack.ts index ee7567b..ccd1e0b 100644 --- a/benchmarks/conformance/cases/22-bollinger-band/tanstack.ts +++ b/benchmarks/conformance/cases/22-bollinger-band/tanstack.ts @@ -35,15 +35,8 @@ const definition = (input: ConformanceInput) => { strokeWidth: 2.25, }), ], - x: { - scale: scaleUtc, - label: 'Date', - }, - y: { - scale: scaleLinear, - grid: true, - label: 'Apple close (USD)', - }, + x: { scale: scaleUtc, axis: { label: 'Date' } }, + y: { scale: scaleLinear, grid: true, axis: { label: 'Apple close (USD)' } }, }) } diff --git a/benchmarks/conformance/cases/24-quantitative-binned-heatmap/tanstack.ts b/benchmarks/conformance/cases/24-quantitative-binned-heatmap/tanstack.ts index 7984abf..e6bf287 100644 --- a/benchmarks/conformance/cases/24-quantitative-binned-heatmap/tanstack.ts +++ b/benchmarks/conformance/cases/24-quantitative-binned-heatmap/tanstack.ts @@ -73,12 +73,12 @@ const definition = (input: ConformanceInput) => { x: { scale: scaleLinear().domain([30, 62]), grid: true, - label: 'Bill length (mm)', + axis: { label: 'Bill length (mm)' }, }, y: { scale: scaleLinear().domain([12, 23]), grid: true, - label: 'Bill depth (mm)', + axis: { label: 'Bill depth (mm)' }, }, color: { scale: scaleSequential, diff --git a/benchmarks/conformance/cases/25-calendar-heatmap/tanstack.ts b/benchmarks/conformance/cases/25-calendar-heatmap/tanstack.ts index 3609996..37c0fb5 100644 --- a/benchmarks/conformance/cases/25-calendar-heatmap/tanstack.ts +++ b/benchmarks/conformance/cases/25-calendar-heatmap/tanstack.ts @@ -27,8 +27,7 @@ const definition = (input: ConformanceInput) => { ], x: { scale: () => scaleBand().paddingInner(0.06).paddingOuter(0.03), - label: 'Week', - format: (value) => `W${value + 1}`, + axis: { ticks: { format: (value) => `W${value + 1}` }, label: 'Week' }, }, y: { scale: scaleBand() diff --git a/benchmarks/conformance/cases/26-diverging-likert/tanstack.ts b/benchmarks/conformance/cases/26-diverging-likert/tanstack.ts index 7bc23bc..31b31f3 100644 --- a/benchmarks/conformance/cases/26-diverging-likert/tanstack.ts +++ b/benchmarks/conformance/cases/26-diverging-likert/tanstack.ts @@ -26,8 +26,10 @@ const definition = () => x: { scale: scaleLinear, grid: true, - label: '← more disagree · Number of responses · more agree →', - format: (value) => `${Math.abs(value)}`, + axis: { + ticks: { format: (value) => `${Math.abs(value)}` }, + label: '← more disagree · Number of responses · more agree →', + }, }, y: { scale: () => scaleBand().paddingInner(0.14).paddingOuter(0.08), diff --git a/benchmarks/conformance/cases/27-parallel-coordinates/tanstack.ts b/benchmarks/conformance/cases/27-parallel-coordinates/tanstack.ts index 89d5343..2bfa89e 100644 --- a/benchmarks/conformance/cases/27-parallel-coordinates/tanstack.ts +++ b/benchmarks/conformance/cases/27-parallel-coordinates/tanstack.ts @@ -40,7 +40,7 @@ const definition = () => y: { scale: scaleLinear().domain([0, 100]), grid: true, - label: 'Relative performance within sample', + axis: { label: 'Relative performance within sample' }, }, color: { range: colors, diff --git a/benchmarks/conformance/cases/28-candlestick/tanstack.ts b/benchmarks/conformance/cases/28-candlestick/tanstack.ts index df1418c..337a118 100644 --- a/benchmarks/conformance/cases/28-candlestick/tanstack.ts +++ b/benchmarks/conformance/cases/28-candlestick/tanstack.ts @@ -48,11 +48,7 @@ const definition = (input: ConformanceInput) => { }), ], x: { scale: scaleUtc }, - y: { - scale: scaleLinear, - grid: true, - label: 'Price', - }, + y: { scale: scaleLinear, grid: true, axis: { label: 'Price' } }, }) } diff --git a/benchmarks/conformance/cases/29-waterfall/tanstack.ts b/benchmarks/conformance/cases/29-waterfall/tanstack.ts index 7cc0941..328316f 100644 --- a/benchmarks/conformance/cases/29-waterfall/tanstack.ts +++ b/benchmarks/conformance/cases/29-waterfall/tanstack.ts @@ -45,12 +45,12 @@ const definition = () => ], x: { scale: () => scaleBand().padding(0.14), - tickRotate: width < 560 ? -32 : 0, + axis: { tickLabels: { rotate: width < 560 ? -32 : 0 } }, }, y: { scale: scaleLinear, grid: true, - label: 'Change in gasoline price (USD per gallon)', + axis: { label: 'Change in gasoline price (USD per gallon)' }, }, color: { domain: kinds, diff --git a/benchmarks/conformance/cases/30-slopegraph/tanstack.ts b/benchmarks/conformance/cases/30-slopegraph/tanstack.ts index e802776..3c55d45 100644 --- a/benchmarks/conformance/cases/30-slopegraph/tanstack.ts +++ b/benchmarks/conformance/cases/30-slopegraph/tanstack.ts @@ -49,7 +49,7 @@ const definition = (input: ConformanceInput) => { y: { scale: scaleLinear, grid: true, - label: '90th/10th percentile wage ratio', + axis: { label: '90th/10th percentile wage ratio' }, }, color: { range: colors, diff --git a/benchmarks/conformance/cases/31-linear-regression/tanstack.ts b/benchmarks/conformance/cases/31-linear-regression/tanstack.ts index 232a92f..5e456ce 100644 --- a/benchmarks/conformance/cases/31-linear-regression/tanstack.ts +++ b/benchmarks/conformance/cases/31-linear-regression/tanstack.ts @@ -67,15 +67,11 @@ const definition = (input: ConformanceInput) => { strokeWidth: 2, }), ], - x: { - scale: scaleLinear, - grid: true, - label: 'Power (hp)', - }, + x: { scale: scaleLinear, grid: true, axis: { label: 'Power (hp)' } }, y: { scale: scaleLinear, grid: true, - label: 'Fuel economy (mpg)', + axis: { label: 'Fuel economy (mpg)' }, }, }) } diff --git a/benchmarks/conformance/cases/32-change-arrows/tanstack.ts b/benchmarks/conformance/cases/32-change-arrows/tanstack.ts index 5504412..63be8c2 100644 --- a/benchmarks/conformance/cases/32-change-arrows/tanstack.ts +++ b/benchmarks/conformance/cases/32-change-arrows/tanstack.ts @@ -36,15 +36,11 @@ const definition = () => { headLength: 8, }), ], - x: { - scale: scaleLinear, - grid: true, - label: 'Log₁₀ population', - }, + x: { scale: scaleLinear, grid: true, axis: { label: 'Log₁₀ population' } }, y: { scale: scaleLinear, grid: true, - label: '90th-to-10th-percentile wage ratio', + axis: { label: '90th-to-10th-percentile wage ratio' }, }, }) } diff --git a/benchmarks/conformance/cases/33-difference-chart/tanstack.ts b/benchmarks/conformance/cases/33-difference-chart/tanstack.ts index 8b2d721..577596b 100644 --- a/benchmarks/conformance/cases/33-difference-chart/tanstack.ts +++ b/benchmarks/conformance/cases/33-difference-chart/tanstack.ts @@ -45,14 +45,9 @@ const definition = (input: ConformanceInput) => { ], x: { scale: scaleUtc, - ticks: 9, - format: formatDifferenceMonth, - }, - y: { - scale: scaleLinear, - ticks: 6, - grid: true, + axis: { ticks: { count: 9, format: formatDifferenceMonth } }, }, + y: { scale: scaleLinear, grid: true, axis: { ticks: { count: 6 } } }, color: { domain: ['positive', 'negative'], range: ['#16a34a', '#dc2626'], diff --git a/benchmarks/conformance/cases/34-pointer-tooltip/tanstack.ts b/benchmarks/conformance/cases/34-pointer-tooltip/tanstack.ts index 765c3af..d109ba6 100644 --- a/benchmarks/conformance/cases/34-pointer-tooltip/tanstack.ts +++ b/benchmarks/conformance/cases/34-pointer-tooltip/tanstack.ts @@ -27,11 +27,7 @@ const definition = (input: ConformanceInput) => { }), ], x: { scale: scaleUtc }, - y: { - scale: scaleLinear, - grid: true, - label: 'Apple close (USD)', - }, + y: { scale: scaleLinear, grid: true, axis: { label: 'Apple close (USD)' } }, }) } diff --git a/benchmarks/conformance/cases/35-grouped-tooltip/tanstack.ts b/benchmarks/conformance/cases/35-grouped-tooltip/tanstack.ts index 51216e4..629c90f 100644 --- a/benchmarks/conformance/cases/35-grouped-tooltip/tanstack.ts +++ b/benchmarks/conformance/cases/35-grouped-tooltip/tanstack.ts @@ -32,7 +32,7 @@ const definition = (input: ConformanceInput) => { y: { scale: scaleLinear, grid: true, - label: 'Unemployed (thousands)', + axis: { label: 'Unemployed (thousands)' }, }, color: { domain: industryNames, diff --git a/benchmarks/conformance/cases/37-delaunay-network/tanstack.ts b/benchmarks/conformance/cases/37-delaunay-network/tanstack.ts index 7155a51..b5176de 100644 --- a/benchmarks/conformance/cases/37-delaunay-network/tanstack.ts +++ b/benchmarks/conformance/cases/37-delaunay-network/tanstack.ts @@ -85,15 +85,11 @@ const definition = (input: ConformanceInput) => { r: 4, }), ], - x: { - scale: scaleLinear, - grid: true, - label: 'Weight (lb)', - }, + x: { scale: scaleLinear, grid: true, axis: { label: 'Weight (lb)' } }, y: { scale: scaleLinear, grid: true, - label: 'Fuel economy (mpg)', + axis: { label: 'Fuel economy (mpg)' }, }, }) } diff --git a/benchmarks/conformance/cases/42-vector-field/tanstack.ts b/benchmarks/conformance/cases/42-vector-field/tanstack.ts index 7be52f5..e05eb2b 100644 --- a/benchmarks/conformance/cases/42-vector-field/tanstack.ts +++ b/benchmarks/conformance/cases/42-vector-field/tanstack.ts @@ -18,16 +18,8 @@ const definition = () => stroke: '#2563eb', }), ], - x: { - scale: scaleLinear, - grid: true, - label: 'Longitude', - }, - y: { - scale: scaleLinear, - grid: true, - label: 'Latitude', - }, + x: { scale: scaleLinear, grid: true, axis: { label: 'Longitude' } }, + y: { scale: scaleLinear, grid: true, axis: { label: 'Latitude' } }, }) export const mount = tanstackMount(definition, 'Two-dimensional vector field') diff --git a/benchmarks/conformance/cases/43-hexbin-density/tanstack.ts b/benchmarks/conformance/cases/43-hexbin-density/tanstack.ts index 2b1ab29..5868265 100644 --- a/benchmarks/conformance/cases/43-hexbin-density/tanstack.ts +++ b/benchmarks/conformance/cases/43-hexbin-density/tanstack.ts @@ -64,12 +64,12 @@ const definition = (input: ConformanceInput) => { x: { scale: scaleLinear().domain([1500, 5500]), grid: true, - label: 'Weight (lb)', + axis: { label: 'Weight (lb)' }, }, y: { scale: scaleLinear().domain([5, 50]), grid: true, - label: 'Fuel economy (mpg)', + axis: { label: 'Fuel economy (mpg)' }, }, color: { scale: scaleThreshold, diff --git a/benchmarks/conformance/cases/50-empirical-cdf/tanstack.ts b/benchmarks/conformance/cases/50-empirical-cdf/tanstack.ts index 062b953..0c4ee89 100644 --- a/benchmarks/conformance/cases/50-empirical-cdf/tanstack.ts +++ b/benchmarks/conformance/cases/50-empirical-cdf/tanstack.ts @@ -45,13 +45,15 @@ const definition = (input: ConformanceInput) => { x: { scale: scaleLinear, grid: true, - label: 'Fuel economy (mpg)', + axis: { label: 'Fuel economy (mpg)' }, }, y: { scale: scaleLinear().domain([0, 1]), grid: true, - label: 'Cumulative proportion', - format: (value) => percent.format(value), + axis: { + ticks: { format: (value) => percent.format(value) }, + label: 'Cumulative proportion', + }, }, }) } diff --git a/benchmarks/conformance/cases/51-faceted-distributions/tanstack.ts b/benchmarks/conformance/cases/51-faceted-distributions/tanstack.ts index c3129e1..f9a517e 100644 --- a/benchmarks/conformance/cases/51-faceted-distributions/tanstack.ts +++ b/benchmarks/conformance/cases/51-faceted-distributions/tanstack.ts @@ -88,14 +88,15 @@ const definition = (input: ConformanceInput) => { x: { scale: scaleLinear().domain([2500, 6500]), grid: true, - label: 'Body mass (g)', + axis: { label: 'Body mass (g)' }, }, y: { scale: scaleLinear().domain([0, 0.4]), grid: true, - ticks: 3, - label: 'Proportion', - format: (value) => percent.format(value), + axis: { + ticks: { count: 3, format: (value) => percent.format(value) }, + label: 'Proportion', + }, }, }), }), diff --git a/benchmarks/conformance/cases/53-log-scale-scatter/tanstack.ts b/benchmarks/conformance/cases/53-log-scale-scatter/tanstack.ts index 91cd420..d632e6f 100644 --- a/benchmarks/conformance/cases/53-log-scale-scatter/tanstack.ts +++ b/benchmarks/conformance/cases/53-log-scale-scatter/tanstack.ts @@ -32,13 +32,9 @@ const definition = (input: ConformanceInput) => { x: { scale: scaleLog().domain([200, 30_000]), grid: true, - label: 'Class size', - }, - y: { - scale: scaleLinear, - grid: true, - label: 'Hierarchy depth', + axis: { label: 'Class size' }, }, + y: { scale: scaleLinear, grid: true, axis: { label: 'Hierarchy depth' } }, }) } diff --git a/benchmarks/conformance/cases/54-bump-ranking/tanstack.ts b/benchmarks/conformance/cases/54-bump-ranking/tanstack.ts index c497da4..50ab8df 100644 --- a/benchmarks/conformance/cases/54-bump-ranking/tanstack.ts +++ b/benchmarks/conformance/cases/54-bump-ranking/tanstack.ts @@ -56,16 +56,18 @@ const definition = () => { ], x: { scale: scaleUtc, - ticks: 7, - format: (date) => `${date.getUTCFullYear()}`, - label: 'Year', + axis: { + ticks: { count: 7, format: (date) => `${date.getUTCFullYear()}` }, + label: 'Year', + }, }, y: { scale: scaleLinear().domain([5.2, 0.8]), - ticks: 5, - format: (value) => `#${value}`, grid: true, - label: 'Rank', + axis: { + ticks: { count: 5, format: (value) => `#${value}` }, + label: 'Rank', + }, }, color: { domain: includedIndustries, diff --git a/benchmarks/conformance/cases/55-indexed-multi-line/tanstack.ts b/benchmarks/conformance/cases/55-indexed-multi-line/tanstack.ts index 7fc8c5b..2d635e4 100644 --- a/benchmarks/conformance/cases/55-indexed-multi-line/tanstack.ts +++ b/benchmarks/conformance/cases/55-indexed-multi-line/tanstack.ts @@ -45,15 +45,14 @@ const definition = () => { dx: 5, }), ], - x: { - scale: scaleUtc, - label: 'Month', - }, + x: { scale: scaleUtc, axis: { label: 'Month' } }, y: { scale: scaleLinear, grid: true, - format: formatIndex, - label: 'Change from January 2008', + axis: { + ticks: { format: formatIndex }, + label: 'Change from January 2008', + }, }, color: { range: colors, diff --git a/benchmarks/conformance/cases/56-connected-scatter/tanstack.ts b/benchmarks/conformance/cases/56-connected-scatter/tanstack.ts index 2890545..27f27d4 100644 --- a/benchmarks/conformance/cases/56-connected-scatter/tanstack.ts +++ b/benchmarks/conformance/cases/56-connected-scatter/tanstack.ts @@ -44,12 +44,12 @@ const definition = () => x: { scale: scaleLinear, grid: true, - label: 'Miles driven per person', + axis: { label: 'Miles driven per person' }, }, y: { scale: scaleLinear, grid: true, - label: 'Cost of gasoline ($ per gallon)', + axis: { label: 'Cost of gasoline ($ per gallon)' }, }, }) diff --git a/benchmarks/conformance/cases/57-scatter-marginal-histograms/tanstack.ts b/benchmarks/conformance/cases/57-scatter-marginal-histograms/tanstack.ts index 3ecdcd1..308cc8c 100644 --- a/benchmarks/conformance/cases/57-scatter-marginal-histograms/tanstack.ts +++ b/benchmarks/conformance/cases/57-scatter-marginal-histograms/tanstack.ts @@ -69,14 +69,15 @@ const definition = (input: ConformanceInput) => { x: { scale: scaleLinear().domain([170, 245]), grid: true, - format: visibleFlipperTick, - label: 'Flipper length (mm)', + axis: { + ticks: { format: visibleFlipperTick }, + label: 'Flipper length (mm)', + }, }, y: { scale: scaleLinear().domain([2500, 7000]), grid: true, - format: visibleMassTick, - label: 'Body mass (g)', + axis: { ticks: { format: visibleMassTick }, label: 'Body mass (g)' }, }, color: { range: colors, diff --git a/benchmarks/conformance/cases/58-select-extrema/tanstack.ts b/benchmarks/conformance/cases/58-select-extrema/tanstack.ts index 1d9f93a..fe56a3f 100644 --- a/benchmarks/conformance/cases/58-select-extrema/tanstack.ts +++ b/benchmarks/conformance/cases/58-select-extrema/tanstack.ts @@ -41,15 +41,8 @@ const definition = () => { dy: (point) => point.dy, }), ], - x: { - scale: scaleUtc, - label: 'Date', - }, - y: { - scale: scaleLinear, - grid: true, - label: 'Apple close (USD)', - }, + x: { scale: scaleUtc, axis: { label: 'Date' } }, + y: { scale: scaleLinear, grid: true, axis: { label: 'Apple close (USD)' } }, }) } diff --git a/benchmarks/conformance/cases/59-grouped-reducer-bars/tanstack.ts b/benchmarks/conformance/cases/59-grouped-reducer-bars/tanstack.ts index 8eb27c2..d847c37 100644 --- a/benchmarks/conformance/cases/59-grouped-reducer-bars/tanstack.ts +++ b/benchmarks/conformance/cases/59-grouped-reducer-bars/tanstack.ts @@ -43,7 +43,7 @@ const definition = () => { y: { scale: scaleLinear, grid: true, - label: 'Mean body mass (g)', + axis: { label: 'Mean body mass (g)' }, }, }) } diff --git a/benchmarks/conformance/cases/60-lag-autocorrelation/tanstack.ts b/benchmarks/conformance/cases/60-lag-autocorrelation/tanstack.ts index 4fa4be4..2b48755 100644 --- a/benchmarks/conformance/cases/60-lag-autocorrelation/tanstack.ts +++ b/benchmarks/conformance/cases/60-lag-autocorrelation/tanstack.ts @@ -43,12 +43,12 @@ const definition = () => { x: { scale: scaleLinear().domain(closeDomain), grid: true, - label: 'Previous close (USD)', + axis: { label: 'Previous close (USD)' }, }, y: { scale: scaleLinear().domain(closeDomain), grid: true, - label: 'Current close (USD)', + axis: { label: 'Current close (USD)' }, }, }) } diff --git a/benchmarks/conformance/cases/61-quantile-ribbon/tanstack.ts b/benchmarks/conformance/cases/61-quantile-ribbon/tanstack.ts index e593881..99e5420 100644 --- a/benchmarks/conformance/cases/61-quantile-ribbon/tanstack.ts +++ b/benchmarks/conformance/cases/61-quantile-ribbon/tanstack.ts @@ -31,14 +31,11 @@ const definition = () => { strokeWidth: 2.25, }), ], - x: { - scale: scaleUtc, - label: 'Month', - }, + x: { scale: scaleUtc, axis: { label: 'Month' } }, y: { scale: scaleLinear, grid: true, - label: 'Unemployed people by industry (thousands)', + axis: { label: 'Unemployed people by industry (thousands)' }, }, }) } diff --git a/benchmarks/conformance/cases/62-ridgeline-density/tanstack.ts b/benchmarks/conformance/cases/62-ridgeline-density/tanstack.ts index 6a0325a..bb91653 100644 --- a/benchmarks/conformance/cases/62-ridgeline-density/tanstack.ts +++ b/benchmarks/conformance/cases/62-ridgeline-density/tanstack.ts @@ -37,15 +37,19 @@ const definition = (input: ConformanceInput) => { ], x: { scale: scaleLinear().domain([4, 10]), - label: 'IMDb rating', grid: true, + axis: { label: 'IMDb rating' }, }, y: { scale: scaleLinear().domain([-0.08, 2.86]), - ticks: seasons.length, - format: (value) => { - const season = seasons[Math.round(value)] - return season === undefined ? '' : `Season ${season}` + axis: { + ticks: { + count: seasons.length, + format: (value) => { + const season = seasons[Math.round(value)] + return season === undefined ? '' : `Season ${season}` + }, + }, }, }, color: { diff --git a/benchmarks/conformance/cases/63-violin-distributions/tanstack.ts b/benchmarks/conformance/cases/63-violin-distributions/tanstack.ts index a64aba9..cf0e8d2 100644 --- a/benchmarks/conformance/cases/63-violin-distributions/tanstack.ts +++ b/benchmarks/conformance/cases/63-violin-distributions/tanstack.ts @@ -49,14 +49,14 @@ const definition = (input: ConformanceInput) => { ], x: { scale: scaleLinear().domain([0.5, 3.5]), - ticks: violinSpecies.length, - format: (value) => violinSpecies[Math.round(value) - 1] ?? '', - }, - y: { - scale: scaleLinear, - grid: true, - label: 'Body mass (g)', + axis: { + ticks: { + count: violinSpecies.length, + format: (value) => violinSpecies[Math.round(value) - 1] ?? '', + }, + }, }, + y: { scale: scaleLinear, grid: true, axis: { label: 'Body mass (g)' } }, color: { range: colors, }, diff --git a/benchmarks/conformance/cases/64-marimekko-mosaic/tanstack.ts b/benchmarks/conformance/cases/64-marimekko-mosaic/tanstack.ts index f23f115..0f88e4f 100644 --- a/benchmarks/conformance/cases/64-marimekko-mosaic/tanstack.ts +++ b/benchmarks/conformance/cases/64-marimekko-mosaic/tanstack.ts @@ -31,13 +31,11 @@ const definition = () => { ], x: { scale: scaleLinear().domain([0, 1]), - format: percent, - label: 'Share of responses', + axis: { ticks: { format: percent }, label: 'Share of responses' }, }, y: { scale: scaleLinear().domain([0, 1.12]), - format: percent, - label: 'Within-question share', + axis: { ticks: { format: percent }, label: 'Within-question share' }, }, color: { domain: mosaicResponses, diff --git a/benchmarks/conformance/cases/65-voronoi-nearest-tooltip/tanstack.ts b/benchmarks/conformance/cases/65-voronoi-nearest-tooltip/tanstack.ts index a3e47ae..f67dba6 100644 --- a/benchmarks/conformance/cases/65-voronoi-nearest-tooltip/tanstack.ts +++ b/benchmarks/conformance/cases/65-voronoi-nearest-tooltip/tanstack.ts @@ -97,15 +97,11 @@ const definition = (rows: readonly CompleteCar[]) => r: 4, }), ], - x: { - scale: scaleLinear, - grid: true, - label: 'Weight (lb)', - }, + x: { scale: scaleLinear, grid: true, axis: { label: 'Weight (lb)' } }, y: { scale: scaleLinear, grid: true, - label: 'Fuel economy (mpg)', + axis: { label: 'Fuel economy (mpg)' }, }, color: { range: colors, diff --git a/benchmarks/conformance/cases/70-composed-chart/tanstack.ts b/benchmarks/conformance/cases/70-composed-chart/tanstack.ts index 769b521..9786991 100644 --- a/benchmarks/conformance/cases/70-composed-chart/tanstack.ts +++ b/benchmarks/conformance/cases/70-composed-chart/tanstack.ts @@ -52,13 +52,11 @@ const definition = (input: ConformanceInput) => ], x: { scale: () => scaleBand().paddingInner(0.1).paddingOuter(0.05), - format: (value) => dateFormat.format(value), - }, - y: { - scale: scaleLinear, - ticks: 5, - grid: true, + axis: { + ticks: { format: (value: Date) => dateFormat.format(value) }, + }, }, + y: { scale: scaleLinear, grid: true, axis: { ticks: { count: 5 } } }, margin: { top: 20, right: 20, bottom: 50, left: 80 }, } }) diff --git a/benchmarks/conformance/cases/71-recharts-population-pyramid/tanstack.ts b/benchmarks/conformance/cases/71-recharts-population-pyramid/tanstack.ts index 4ff9131..d16dee4 100644 --- a/benchmarks/conformance/cases/71-recharts-population-pyramid/tanstack.ts +++ b/benchmarks/conformance/cases/71-recharts-population-pyramid/tanstack.ts @@ -26,10 +26,14 @@ const definition = (input: ConformanceInput) => { ], x: { scale: scaleLinear().domain([-80, 80]), - ticks: 5, - format: (value) => Math.abs(value).toLocaleString('en-US'), - label: 'Penguins observed', grid: true, + axis: { + ticks: { + count: 5, + format: (value) => Math.abs(value).toLocaleString('en-US'), + }, + label: 'Penguins observed', + }, }, y: { scale: () => scaleBand().paddingInner(0.02).paddingOuter(0.01), diff --git a/benchmarks/conformance/cases/72-recharts-mixed-bars/tanstack.ts b/benchmarks/conformance/cases/72-recharts-mixed-bars/tanstack.ts index adcc3cb..1666b25 100644 --- a/benchmarks/conformance/cases/72-recharts-mixed-bars/tanstack.ts +++ b/benchmarks/conformance/cases/72-recharts-mixed-bars/tanstack.ts @@ -1,5 +1,5 @@ import { weather } from '@charts-poc/demo-data/weather' -import { barY, defineChart } from '@tanstack/charts' +import { barY, defineChart, group } from '@tanstack/charts' import { scaleBand, scaleLinear } from 'd3-scale' import { tanstackMount } from '../../shared/mount' import type { ConformanceInput } from '../../types' @@ -27,7 +27,7 @@ const definition = (input: ConformanceInput) => { y2: 'precipitation', z: () => 'stack', fill: '#8884d8', - groupScale, + layout: group({ scale: groupScale }), inset: 1, }), barY(rows, { @@ -36,7 +36,7 @@ const definition = (input: ConformanceInput) => { y2: (row) => row.precipitation + row.wind, z: () => 'stack', fill: '#82ca9d', - groupScale, + layout: group({ scale: groupScale }), inset: 1, }), barY(rows, { @@ -44,19 +44,15 @@ const definition = (input: ConformanceInput) => { y: 'temp_max', z: () => 'independent', fill: '#ffc658', - groupScale, + layout: group({ scale: groupScale }), inset: 1, }), ], x: { scale: () => scaleBand().paddingInner(0.1).paddingOuter(0.05), - format: (value) => dateFormat.format(value), - }, - y: { - scale: scaleLinear, - ticks: 5, - grid: true, + axis: { ticks: { format: (value) => dateFormat.format(value) } }, }, + y: { scale: scaleLinear, grid: true, axis: { ticks: { count: 5 } } }, margin: { top: 20, right: 20, bottom: 50, left: 80 }, }) } diff --git a/benchmarks/conformance/cases/73-many-point-scatter/tanstack.ts b/benchmarks/conformance/cases/73-many-point-scatter/tanstack.ts index 753c385..e729a4b 100644 --- a/benchmarks/conformance/cases/73-many-point-scatter/tanstack.ts +++ b/benchmarks/conformance/cases/73-many-point-scatter/tanstack.ts @@ -23,16 +23,8 @@ const definition = (input: ConformanceInput) => { fillOpacity: 0.72, }), ], - x: { - scale: scaleLinear, - ticks: 6, - grid: true, - }, - y: { - scale: scaleLinear, - ticks: 6, - grid: true, - }, + x: { scale: scaleLinear, grid: true, axis: { ticks: { count: 6 } } }, + y: { scale: scaleLinear, grid: true, axis: { ticks: { count: 6 } } }, color: { range: colors, }, diff --git a/benchmarks/conformance/cases/80-echarts-axis-pointer/tanstack.ts b/benchmarks/conformance/cases/80-echarts-axis-pointer/tanstack.ts index 0c1c291..0e25153 100644 --- a/benchmarks/conformance/cases/80-echarts-axis-pointer/tanstack.ts +++ b/benchmarks/conformance/cases/80-echarts-axis-pointer/tanstack.ts @@ -49,17 +49,20 @@ const definition = (input: ConformanceInput) => { ], x: { scale: scaleUtc, - format: (value) => - value.toLocaleDateString(undefined, { - month: 'short', - timeZone: 'UTC', - }), + axis: { + ticks: { + format: (value) => + value.toLocaleDateString(undefined, { + month: 'short', + timeZone: 'UTC', + }), + }, + }, }, y: { scale: scaleLinear, - ticks: 5, grid: true, - label: 'Unemployed (thousands)', + axis: { ticks: { count: 5 }, label: 'Unemployed (thousands)' }, }, color: { domain: axisPointerIndustries, diff --git a/benchmarks/conformance/cases/81-recharts-interactive-legend/tanstack.ts b/benchmarks/conformance/cases/81-recharts-interactive-legend/tanstack.ts index c93b119..146d383 100644 --- a/benchmarks/conformance/cases/81-recharts-interactive-legend/tanstack.ts +++ b/benchmarks/conformance/cases/81-recharts-interactive-legend/tanstack.ts @@ -54,17 +54,20 @@ const definition = (input: InteractiveLegendInput) => { ], x: { scale: scaleUtc, - format: (date) => - date.toLocaleDateString('en-US', { - month: 'short', - timeZone: 'UTC', - }), + axis: { + ticks: { + format: (date) => + date.toLocaleDateString('en-US', { + month: 'short', + timeZone: 'UTC', + }), + }, + }, }, y: { scale: scaleLinear().domain(yDomain), - ticks: 5, grid: true, - label: 'Unemployed (thousands)', + axis: { ticks: { count: 5 }, label: 'Unemployed (thousands)' }, }, color: { domain: legendSeries.map((series) => series.id), diff --git a/benchmarks/conformance/cases/82-chart-table-selection/tanstack.ts b/benchmarks/conformance/cases/82-chart-table-selection/tanstack.ts index 0e82065..db6e644 100644 --- a/benchmarks/conformance/cases/82-chart-table-selection/tanstack.ts +++ b/benchmarks/conformance/cases/82-chart-table-selection/tanstack.ts @@ -48,15 +48,11 @@ const definition = (input: ChartTableInput) => { ] : []), ], - x: { - scale: scaleLinear, - label: 'Flipper length (mm)', - }, + x: { scale: scaleLinear, axis: { label: 'Flipper length (mm)' } }, y: { scale: scaleLinear, - ticks: 5, grid: true, - label: 'Body mass (g)', + axis: { ticks: { count: 5 }, label: 'Body mass (g)' }, }, margin: { top: 16, right: 24, bottom: 42, left: 62 }, }) diff --git a/benchmarks/conformance/cases/83-focus-context-window/tanstack.ts b/benchmarks/conformance/cases/83-focus-context-window/tanstack.ts index dbd1e96..086a42a 100644 --- a/benchmarks/conformance/cases/83-focus-context-window/tanstack.ts +++ b/benchmarks/conformance/cases/83-focus-context-window/tanstack.ts @@ -63,13 +63,9 @@ const detailDefinition = (input: DetailInput) => { ], x: { scale: scaleUtc().domain([input.window.start, input.window.end]), - label: 'Selected time window', - }, - y: { - scale: scaleLinear, - grid: true, - label: 'Close ($)', + axis: { label: 'Selected time window' }, }, + y: { scale: scaleLinear, grid: true, axis: { label: 'Close ($)' } }, margin: detailMargin, }) } @@ -87,17 +83,18 @@ const overviewDefinition = (input: ConformanceInput) => { ], x: { scale: scaleUtc().domain(fullDomain), - ticks: 4, - format: (value) => - value.toLocaleDateString(undefined, { - month: 'short', - timeZone: 'UTC', - }), - }, - y: { - scale: scaleLinear, - guide: false, + axis: { + ticks: { + count: 4, + format: (value) => + value.toLocaleDateString(undefined, { + month: 'short', + timeZone: 'UTC', + }), + }, + }, }, + y: { scale: scaleLinear, axis: false }, margin: overviewMargin, }) } diff --git a/benchmarks/conformance/cases/84-pinned-nested-chart-tooltip/tanstack.ts b/benchmarks/conformance/cases/84-pinned-nested-chart-tooltip/tanstack.ts index 1b32169..fc070d7 100644 --- a/benchmarks/conformance/cases/84-pinned-nested-chart-tooltip/tanstack.ts +++ b/benchmarks/conformance/cases/84-pinned-nested-chart-tooltip/tanstack.ts @@ -58,13 +58,12 @@ const mainDefinition = (input: MainChartInput) => { ], x: { scale: scaleLinear().domain([170, 235]), - label: 'Flipper length (mm)', + axis: { label: 'Flipper length (mm)' }, }, y: { scale: scaleLinear().domain([3000, 6000]), - ticks: 5, grid: true, - label: 'Body mass (g)', + axis: { ticks: { count: 5 }, label: 'Body mass (g)' }, }, margin: { top: 18, right: 24, bottom: 42, left: 68 }, }) @@ -83,10 +82,7 @@ const miniDefinition = (input: MiniChartInput) => x: { scale: () => scaleBand().paddingInner(0.18).paddingOuter(0.08), }, - y: { - scale: scaleLinear, - guide: false, - }, + y: { scale: scaleLinear, axis: false }, margin: { top: 6, right: 6, bottom: 24, left: 6 }, }) diff --git a/benchmarks/conformance/cases/85-scrollable-resource-lanes/tanstack.ts b/benchmarks/conformance/cases/85-scrollable-resource-lanes/tanstack.ts index f6e6ccd..73b280d 100644 --- a/benchmarks/conformance/cases/85-scrollable-resource-lanes/tanstack.ts +++ b/benchmarks/conformance/cases/85-scrollable-resource-lanes/tanstack.ts @@ -63,7 +63,7 @@ const definition = (input: ConformanceInput) => { x: { scale: scaleUtc().domain(resourceTimelineDomain), grid: true, - ticks: Math.max(6, Math.floor(width / 84)), + axis: { ticks: { count: Math.max(6, Math.floor(width / 84)) } }, }, y: { scale: scaleBand() @@ -71,7 +71,7 @@ const definition = (input: ConformanceInput) => { .paddingInner(0.08) .paddingOuter(0.04), grid: false, - guide: false, + axis: false, }, color: { domain: timelineStatuses, diff --git a/benchmarks/conformance/cases/86-streaming-window-preservation/tanstack.ts b/benchmarks/conformance/cases/86-streaming-window-preservation/tanstack.ts index 48a1d7f..4a5d1f0 100644 --- a/benchmarks/conformance/cases/86-streaming-window-preservation/tanstack.ts +++ b/benchmarks/conformance/cases/86-streaming-window-preservation/tanstack.ts @@ -60,24 +60,27 @@ const definition = (input: StreamingChartInput) => { ], x: { scale: scaleUtc().domain(input.viewport), - label: - input.viewportMode === 'locked' - ? 'Locked viewport' - : input.viewportMode === 'latest' - ? 'Following latest' - : 'All samples', - format: (value) => - value.toLocaleDateString(undefined, { - month: 'short', - day: 'numeric', - timeZone: 'UTC', - }), + axis: { + ticks: { + format: (value) => + value.toLocaleDateString(undefined, { + month: 'short', + day: 'numeric', + timeZone: 'UTC', + }), + }, + label: + input.viewportMode === 'locked' + ? 'Locked viewport' + : input.viewportMode === 'latest' + ? 'Following latest' + : 'All samples', + }, }, y: { scale: scaleLinear, - ticks: 5, grid: true, - label: 'Downloads', + axis: { ticks: { count: 5 }, label: 'Downloads' }, }, margin: { top: 18, right: 24, bottom: 44, left: 58 }, }) diff --git a/benchmarks/conformance/cases/87-echarts-synchronized-cursors/tanstack.ts b/benchmarks/conformance/cases/87-echarts-synchronized-cursors/tanstack.ts index 1a695e2..c2a0af4 100644 --- a/benchmarks/conformance/cases/87-echarts-synchronized-cursors/tanstack.ts +++ b/benchmarks/conformance/cases/87-echarts-synchronized-cursors/tanstack.ts @@ -74,18 +74,23 @@ const definition = (input: SynchronizedViewInput) => { ], x: { scale: scaleUtc, - format: (value) => - value.toLocaleDateString(undefined, { - month: 'short', - timeZone: 'UTC', - }), + axis: { + ticks: { + format: (value) => + value.toLocaleDateString(undefined, { + month: 'short', + timeZone: 'UTC', + }), + }, + }, }, y: { scale: scaleLinear().domain(synchronizedCursorYDomains[input.view]), - ticks: 4, grid: true, - label: input.view === 'current' ? '2020 travelers' : '2019 travelers', - format: formatTravelers, + axis: { + ticks: { count: 4, format: formatTravelers }, + label: input.view === 'current' ? '2020 travelers' : '2019 travelers', + }, }, margin: { top: 16, diff --git a/benchmarks/conformance/cases/88-echarts-free-cursor/tanstack.ts b/benchmarks/conformance/cases/88-echarts-free-cursor/tanstack.ts index bc75097..d99ecbf 100644 --- a/benchmarks/conformance/cases/88-echarts-free-cursor/tanstack.ts +++ b/benchmarks/conformance/cases/88-echarts-free-cursor/tanstack.ts @@ -66,15 +66,11 @@ const definition = (input: ConformanceInput) => { strokeWidth: 1, }), ], - x: { - scale: configuredXScale, - label: 'Horsepower', - }, + x: { scale: configuredXScale, axis: { label: 'Horsepower' } }, y: { scale: configuredYScale, - ticks: 7, grid: true, - label: 'Fuel economy (mpg)', + axis: { ticks: { count: 7 }, label: 'Fuel economy (mpg)' }, }, margin: { top: 22, diff --git a/benchmarks/conformance/cases/89-brush-range-selection/tanstack.ts b/benchmarks/conformance/cases/89-brush-range-selection/tanstack.ts index 3ec2d45..d5b3e9d 100644 --- a/benchmarks/conformance/cases/89-brush-range-selection/tanstack.ts +++ b/benchmarks/conformance/cases/89-brush-range-selection/tanstack.ts @@ -68,14 +68,15 @@ const definition = (input: ConformanceInput) => { ], x: { scale: brushScale, - format: (value) => brushMonthFormatter.format(value), - label: 'Month', + axis: { + ticks: { format: (value) => brushMonthFormatter.format(value) }, + label: 'Month', + }, }, y: { scale: scaleLinear, - ticks: 4, grid: true, - label: 'AAPL close ($)', + axis: { ticks: { count: 4 }, label: 'AAPL close ($)' }, }, margin: { top: 52, right: 24, bottom: 44, left: 58 }, }) diff --git a/benchmarks/conformance/cases/90-zoomable-time-window/tanstack.ts b/benchmarks/conformance/cases/90-zoomable-time-window/tanstack.ts index 3049ea1..cf4e08b 100644 --- a/benchmarks/conformance/cases/90-zoomable-time-window/tanstack.ts +++ b/benchmarks/conformance/cases/90-zoomable-time-window/tanstack.ts @@ -63,19 +63,22 @@ const definition = (input: ZoomChartInput) => { ], x: { scale: scaleForWindow(input.window), - label: 'Date', - format: (value) => - value.toLocaleDateString(undefined, { - month: 'short', - day: 'numeric', - timeZone: 'UTC', - }), + axis: { + ticks: { + format: (value) => + value.toLocaleDateString(undefined, { + month: 'short', + day: 'numeric', + timeZone: 'UTC', + }), + }, + label: 'Date', + }, }, y: { scale: scaleLinear, - ticks: 4, grid: true, - label: 'AAPL close ($)', + axis: { ticks: { count: 4 }, label: 'AAPL close ($)' }, }, margin: { top: 56, right: 24, bottom: 44, left: 58 }, }) diff --git a/benchmarks/conformance/cases/91-timeline-playback-scrubber/tanstack.ts b/benchmarks/conformance/cases/91-timeline-playback-scrubber/tanstack.ts index 93a24c8..88bbe29 100644 --- a/benchmarks/conformance/cases/91-timeline-playback-scrubber/tanstack.ts +++ b/benchmarks/conformance/cases/91-timeline-playback-scrubber/tanstack.ts @@ -54,18 +54,21 @@ const definition = (input: ConformanceInput) => { ], x: { scale: scaleUtc, - format: (value) => - value.toLocaleDateString(undefined, { - month: 'short', - day: 'numeric', - timeZone: 'UTC', - }), + axis: { + ticks: { + format: (value) => + value.toLocaleDateString(undefined, { + month: 'short', + day: 'numeric', + timeZone: 'UTC', + }), + }, + }, }, y: { scale: scaleLinear, - ticks: 4, grid: true, - label: 'AAPL close ($)', + axis: { ticks: { count: 4 }, label: 'AAPL close ($)' }, }, margin, }) diff --git a/benchmarks/conformance/cases/92-editable-event-range/tanstack.ts b/benchmarks/conformance/cases/92-editable-event-range/tanstack.ts index 824a905..2920a54 100644 --- a/benchmarks/conformance/cases/92-editable-event-range/tanstack.ts +++ b/benchmarks/conformance/cases/92-editable-event-range/tanstack.ts @@ -97,12 +97,16 @@ const definition = (input: EditableChartInput) => { x: { scale: scaleUtc().domain(editableDomain), grid: true, - format: (value) => - value.toLocaleDateString(undefined, { - month: 'short', - day: 'numeric', - timeZone: 'UTC', - }), + axis: { + ticks: { + format: (value: Date) => + value.toLocaleDateString(undefined, { + month: 'short', + day: 'numeric', + timeZone: 'UTC', + }), + }, + }, }, y: { scale: scaleBand() diff --git a/benchmarks/conformance/cases/bar-grouped/tanstack.ts b/benchmarks/conformance/cases/bar-grouped/tanstack.ts index eeca544..e5c8c51 100644 --- a/benchmarks/conformance/cases/bar-grouped/tanstack.ts +++ b/benchmarks/conformance/cases/bar-grouped/tanstack.ts @@ -1,5 +1,5 @@ import { penguins } from '@charts-poc/demo-data/penguins' -import { barY, colorLegend, defineChart } from '@tanstack/charts' +import { barY, colorLegend, defineChart, group } from '@tanstack/charts' import { rollups } from 'd3-array' import { scaleBand, scaleLinear } from 'd3-scale' import { tanstackMount } from '../../shared/mount' @@ -27,19 +27,20 @@ const definition = (input: ConformanceInput) => x: 'species', y: 'count', color: 'sex', - groupScale: scaleBand().domain(sexDomain).paddingInner(0.08), + layout: group({ + scale: scaleBand().domain(sexDomain).paddingInner(0.08), + }), inset: 1, }), ], x: { scale: () => scaleBand().paddingInner(0.14).paddingOuter(0.06), - tickRotate: width < 640 ? -32 : 0, + axis: { tickLabels: { rotate: width < 640 ? -32 : 0 } }, }, y: { scale: scaleLinear, - label: 'Penguins', - ticks: 5, grid: true, + axis: { ticks: { count: 5 }, label: 'Penguins' }, }, color: { range: sexColors, diff --git a/benchmarks/conformance/cases/bar-horizontal-ranking/tanstack.ts b/benchmarks/conformance/cases/bar-horizontal-ranking/tanstack.ts index ce5c454..2506517 100644 --- a/benchmarks/conformance/cases/bar-horizontal-ranking/tanstack.ts +++ b/benchmarks/conformance/cases/bar-horizontal-ranking/tanstack.ts @@ -21,9 +21,8 @@ const definition = (input: ConformanceInput) => { ], x: { scale: scaleLinear, - label: '2015 population', - ticks: 5, grid: true, + axis: { ticks: { count: 5 }, label: '2015 population' }, }, y: { scale: () => scaleBand().paddingInner(0.1).paddingOuter(0.05), diff --git a/benchmarks/conformance/cases/bar-stacked/case.json b/benchmarks/conformance/cases/bar-stacked/case.json index 3621730..1b0178c 100644 --- a/benchmarks/conformance/cases/bar-stacked/case.json +++ b/benchmarks/conformance/cases/bar-stacked/case.json @@ -8,16 +8,17 @@ "support": "composed", "features": [ "stacked bars", - "D3 stack intervals", + "stack transform", "categorical color inference", "monthly time intervals", - "explicit lower and upper bounds" + "implicit lower and upper bounds" ], "geometry": [ { - "role": "rect", + "role": "bar", "count": 69, - "maxCount": 72 + "maxCount": 72, + "rendererRoles": { "observable-plot": "rect", "tanstack": "bar" } }, { "role": "rule", diff --git a/benchmarks/conformance/cases/bar-stacked/tanstack.ts b/benchmarks/conformance/cases/bar-stacked/tanstack.ts index 8eebc81..df2d3ee 100644 --- a/benchmarks/conformance/cases/bar-stacked/tanstack.ts +++ b/benchmarks/conformance/cases/bar-stacked/tanstack.ts @@ -1,48 +1,45 @@ import { crimeanWar } from '@charts-poc/demo-data/crimean-war' -import { defineChart, rect, ruleY } from '@tanstack/charts' +import { barY, defineChart, ruleY, stackY } from '@tanstack/charts' import { scaleLinear, scaleUtc } from 'd3-scale' -import { stack, stackOrderReverse } from 'd3-shape' import { tanstackMount } from '../../shared/mount' -import type { CrimeanWarRow } from '@charts-poc/demo-data/crimean-war' import type { ConformanceInput } from '../../types' const causes = ['disease', 'wounds', 'other'] as const const causeColors = ['#4269d0', '#ff725c', '#efb118'] -type Cause = (typeof causes)[number] - -interface MortalityInterval { - date: Date - nextMonth: Date - cause: Cause - deaths: number - y1: number - y2: number -} const definition = (input: ConformanceInput) => { - const intervals = stackRows(crimeanWar.slice(input.revision)) + const rows = crimeanWar.slice(input.revision).flatMap((row) => + causes.map((cause) => ({ + date: row.date, + cause, + deaths: row[cause], + })), + ) return defineChart({ marks: [ - rect(intervals, { - x1: 'date', - x2: 'nextMonth', - y1: 'y1', - y2: 'y2', - color: 'cause', - }), + barY( + rows, + stackY( + { order: [...causes].reverse() }, + { + x: 'date', + y: 'deaths', + z: 'cause', + color: 'cause', + }, + ), + ), ruleY([0]), ], x: { scale: scaleUtc, - ticks: 6, - format: (value) => month.format(value), + axis: { ticks: { count: 6, format: (value) => month.format(value) } }, }, y: { scale: scaleLinear, - label: 'Deaths', - ticks: 5, grid: true, + axis: { ticks: { count: 5 }, label: 'Deaths' }, }, color: { domain: causes, range: causeColors }, }) @@ -54,26 +51,3 @@ const month = new Intl.DateTimeFormat('en-US', { month: 'short', timeZone: 'UTC', }) - -function stackRows(rows: readonly CrimeanWarRow[]): MortalityInterval[] { - return stack() - .keys(causes) - .order(stackOrderReverse) - .value((row, cause) => row[cause])(rows) - .flatMap((layer) => - layer.map((point) => ({ - date: point.data.date, - nextMonth: new Date( - Date.UTC( - point.data.date.getUTCFullYear(), - point.data.date.getUTCMonth() + 1, - 1, - ), - ), - cause: layer.key, - deaths: point[1] - point[0], - y1: point[0], - y2: point[1], - })), - ) -} diff --git a/benchmarks/conformance/cases/bar-vertical-sorted/tanstack.ts b/benchmarks/conformance/cases/bar-vertical-sorted/tanstack.ts index b230cab..89b5cd3 100644 --- a/benchmarks/conformance/cases/bar-vertical-sorted/tanstack.ts +++ b/benchmarks/conformance/cases/bar-vertical-sorted/tanstack.ts @@ -22,14 +22,18 @@ const definition = (_input: ConformanceInput) => ], x: { scale: () => scaleBand().paddingInner(0.1).paddingOuter(0.05), - tickRotate: width < 560 ? -32 : 0, + axis: { tickLabels: { rotate: width < 560 ? -32 : 0 } }, }, y: { scale: scaleLinear, - label: 'Frequency', - ticks: 5, grid: true, - format: (value) => percent.format(value), + axis: { + ticks: { + count: 5, + format: (value: number) => percent.format(value), + }, + label: 'Frequency', + }, }, } }) diff --git a/benchmarks/conformance/cases/facets-anscombe/tanstack.ts b/benchmarks/conformance/cases/facets-anscombe/tanstack.ts index 053219c..eb7e418 100644 --- a/benchmarks/conformance/cases/facets-anscombe/tanstack.ts +++ b/benchmarks/conformance/cases/facets-anscombe/tanstack.ts @@ -24,12 +24,12 @@ const definition = (_input: ConformanceInput) => x: { scale: scaleLinear().domain([3, 20]), grid: true, - ticks: 5, + axis: { ticks: { count: 5 } }, }, y: { scale: scaleLinear().domain([2, 14]), grid: true, - ticks: 4, + axis: { ticks: { count: 4 } }, }, }), }), diff --git a/benchmarks/conformance/cases/heatmap-labeled/tanstack.ts b/benchmarks/conformance/cases/heatmap-labeled/tanstack.ts index 13c981c..35e28ff 100644 --- a/benchmarks/conformance/cases/heatmap-labeled/tanstack.ts +++ b/benchmarks/conformance/cases/heatmap-labeled/tanstack.ts @@ -48,14 +48,14 @@ const definition = (_input: ConformanceInput) => .domain(episodeDomain) .paddingInner(0.04) .paddingOuter(0.02), - label: 'Episode', + axis: { label: 'Episode' }, }, y: { scale: scaleBand() .domain(seasonDomain) .paddingInner(0.04) .paddingOuter(0.02), - label: 'Season', + axis: { label: 'Season' }, }, color: { scale: () => scaleLinear().range(ratingColors), diff --git a/benchmarks/conformance/cases/histogram/tanstack.ts b/benchmarks/conformance/cases/histogram/tanstack.ts index f419dfd..efa79d2 100644 --- a/benchmarks/conformance/cases/histogram/tanstack.ts +++ b/benchmarks/conformance/cases/histogram/tanstack.ts @@ -54,13 +54,9 @@ const definition = (input: ConformanceInput) => { x: { scale: scaleLinear, grid: true, - label: 'Fuel economy (mpg)', - }, - y: { - scale: scaleLinear, - grid: true, - label: 'Count', + axis: { label: 'Fuel economy (mpg)' }, }, + y: { scale: scaleLinear, grid: true, axis: { label: 'Count' } }, }) } diff --git a/benchmarks/conformance/cases/scatter-bubble/tanstack.ts b/benchmarks/conformance/cases/scatter-bubble/tanstack.ts index 97e37e8..f3373c0 100644 --- a/benchmarks/conformance/cases/scatter-bubble/tanstack.ts +++ b/benchmarks/conformance/cases/scatter-bubble/tanstack.ts @@ -34,16 +34,8 @@ const definition = (input: ConformanceInput) => { strokeWidth: 0.75, }), ], - x: { - scale: scaleLinear, - grid: true, - label: 'Bill length (mm)', - }, - y: { - scale: scaleLinear, - grid: true, - label: 'Bill depth (mm)', - }, + x: { scale: scaleLinear, grid: true, axis: { label: 'Bill length (mm)' } }, + y: { scale: scaleLinear, grid: true, axis: { label: 'Bill depth (mm)' } }, color: { range: groupRange, legend: colorLegend({ label: 'Species' }), diff --git a/benchmarks/rendering.ts b/benchmarks/rendering.ts index e2c4adc..9be4607 100644 --- a/benchmarks/rendering.ts +++ b/benchmarks/rendering.ts @@ -121,13 +121,13 @@ const nativeDownloads = defineChart({ ], x: { scale: scaleUtc().domain(downloadData.map((point) => point.date)), - ticks: 6, + axis: { ticks: { count: 6 } }, }, y: { scale: scaleLinear() .domain([0, max(downloadData, (point) => point.downloads) ?? 1]) .nice(5), - ticks: 5, + axis: { ticks: { count: 5 } }, }, }) const largeData = Array.from({ length: 10_000 }, (_, index) => ({ @@ -296,11 +296,11 @@ const createNativeDynamicDefinition = (input: { ], x: { scale: scaleUtc().domain(input.points.map((point) => point.date)), - ticks: 6, + axis: { ticks: { count: 6 } }, }, y: { scale: scaleLinear().domain([0, maximum]).nice(5), - ticks: 5, + axis: { ticks: { count: 5 } }, }, } }) diff --git a/docs/comparison.md b/docs/comparison.md index 81f1fb1..a928cfc 100644 --- a/docs/comparison.md +++ b/docs/comparison.md @@ -12,14 +12,14 @@ evidence without turning untested behavior into a checkmark. | Library | Package | Measured source | | -------------------------------------------------------------------------------------- | -------------------- | ------------------- | -| [TanStack Charts](./overview.md) | `@tanstack/charts` | workspace `5c36a38` | +| [TanStack Charts](./overview.md) | `@tanstack/charts` | workspace `093f81c` | | [Chart.js](https://www.chartjs.org/docs/latest/) | `chart.js` | npm `4.5.1` | | [Apache ECharts](https://echarts.apache.org/handbook/en/best-practices/canvas-vs-svg/) | `echarts` | npm `6.1.0` | | [Recharts](https://recharts.github.io/en-US/) | `recharts` | npm `3.10.1` | | [Observable Plot](https://observablehq.com/plot/features/plots) | `@observablehq/plot` | npm `0.6.17` | The competitor versions are exact package pins, not latest versions inferred -at page render time. The measured TanStack workspace revision is `5c36a38`. +at page render time. The measured TanStack workspace revision is `093f81c`. ## Capability matrix @@ -59,7 +59,7 @@ that lane externalizes React and React DOM. | Library | Full cold-page gzip | React externalized | | --------------- | ------------------: | -----------------: | -| TanStack Charts | 24.20–28.22 KiB | — | +| TanStack Charts | 26.12–31.04 KiB | — | | Chart.js | 44.70–58.21 KiB | — | | Apache ECharts | 153.10–173.18 KiB | — | | Recharts | 153.08–168.27 KiB | 94.96–109.96 KiB | diff --git a/docs/concepts/chart-definitions.md b/docs/concepts/chart-definitions.md index 462a1a1..f22c872 100644 --- a/docs/concepts/chart-definitions.md +++ b/docs/concepts/chart-definitions.md @@ -38,8 +38,8 @@ const letterFrequencies = defineChart({ y: { scale: scaleLinear, nice: true, - label: 'Frequency', grid: true, + axis: { label: 'Frequency' }, }, }) ``` @@ -57,7 +57,7 @@ const productRanking = defineChart({ x: { scale: scaleLinear, nice: true, - ticks: width < 480 ? 4 : 7, + axis: { ticks: { count: width < 480 ? 4 : 7 } }, }, y: { scale: () => scaleBand().padding(0.1), @@ -106,7 +106,7 @@ function ProductRanking({ rows, metric }: Props) { x: { scale: scaleLinear, nice: true, - ticks: width < 480 ? 4 : 7, + axis: { ticks: { count: width < 480 ? 4 : 7 } }, }, y: { scale: () => scaleBand().padding(0.1), diff --git a/docs/concepts/data-and-channels.md b/docs/concepts/data-and-channels.md index b8370be..f5ca377 100644 --- a/docs/concepts/data-and-channels.md +++ b/docs/concepts/data-and-channels.md @@ -89,9 +89,9 @@ also supplies that path grouping. When both are present, `z` wins for geometry and interaction grouping while `color` remains an independent color-scale value. Omitting `color` reuses `z` for color. -On bars, neither channel implicitly invents grouped-bar geometry. Supply a D3 -`groupScale` when multiple bars must occupy sub-bands within one category. -The scale uses `z` when present, otherwise `color`. See +Bars stack their length channel by default. Use `layout: group()` when multiple +bars must occupy sub-bands within one category. Grouping uses `z` when present, +otherwise a discrete `color` channel may infer series identity. See [Bars and Rankings](../examples/bars-and-rankings.md). ## Color channels and constants @@ -365,13 +365,13 @@ const bubbleChart = defineChart({ ], x: { scale: scaleLinear, - label: 'Bill length (mm)', grid: true, + axis: { label: 'Bill length (mm)' }, }, y: { scale: scaleLinear, - label: 'Bill depth (mm)', grid: true, + axis: { label: 'Bill depth (mm)' }, }, color: { scale: scaleOrdinal(species, ['#2563eb', '#f97316', '#10b981']), diff --git a/docs/concepts/grammar-of-graphics.md b/docs/concepts/grammar-of-graphics.md index badabd4..46ad2f6 100644 --- a/docs/concepts/grammar-of-graphics.md +++ b/docs/concepts/grammar-of-graphics.md @@ -137,13 +137,13 @@ const axes = { x: { scale: scaleUtc, nice: true, - label: 'Month', + axis: { label: 'Month' }, }, y: { scale: scaleLinear, nice: true, - label: 'Revenue', grid: true, + axis: { label: 'Revenue' }, }, } ``` @@ -159,10 +159,14 @@ Axis guide options live next to their scale: ```ts const y = { scale: revenueScale, - label: 'Monthly revenue', - format: (value: number) => `$${Math.round(value / 1_000)}k`, - ticks: 5, grid: true, + axis: { + label: 'Monthly revenue', + ticks: { + count: 5, + format: (value: number) => `$${Math.round(value / 1_000)}k`, + }, + }, } ``` @@ -272,7 +276,7 @@ const composedChart = defineChart({ ], x: { scale: () => scaleBand().padding(0.12), - label: 'Date', + axis: { label: 'Date' }, }, y: { scale: scaleLinear, diff --git a/docs/concepts/layout-axes-and-coordinates.md b/docs/concepts/layout-axes-and-coordinates.md index 018bfa3..f5cd51d 100644 --- a/docs/concepts/layout-axes-and-coordinates.md +++ b/docs/concepts/layout-axes-and-coordinates.md @@ -63,12 +63,14 @@ const chart = defineChart(({ width }) => ({ marks: [lineY(rows, { x: 'date', y: 'value' })], x: { scale: xScale, - ticks: width < 420 ? 4 : 8, - tickRotate: width < 520 ? -30 : undefined, + axis: { + ticks: { count: width < 420 ? 4 : 8 }, + tickLabels: { rotate: width < 520 ? -30 : undefined }, + }, }, y: { scale: yScale, - label: width < 480 ? undefined : 'Weekly downloads', + axis: { label: width < 480 ? undefined : 'Weekly downloads' }, }, })) ``` @@ -124,10 +126,9 @@ inherited font and relayout after web fonts load. Advanced renderers can supply `measureText`. Its metrics include painted x and y offsets relative to the requested anchor and baseline, not only width and height. This is necessary for correct containment of rotated and anchored labels. -Automatic margins contain chart-owned guides and Cartesian `text` marks; they do -not avoid collisions between adjacent labels. Use `ticks` to reduce density, -`tickRotate` when guide labels overlap, and explicit text placement when data -labels overlap one another. +Automatic margins contain chart-owned guides and Cartesian `text` marks. Axis +tick labels are thinned against their measured, optionally rotated bounds. +Explicit text placement remains responsible for data-label collisions. ## Axis guide options @@ -136,34 +137,61 @@ Each axis combines a required scale factory or instance with optional guide cont ```ts const x = { scale: xScale, - label: 'Month', - ticks: 6, - format: (date: Date) => monthFormatter.format(date), - tickRotate: -30, - labelOffset: 12, grid: false, + axis: { + ticks: { + count: 6, + format: (date: Date) => monthFormatter.format(date), + }, + tickLabels: { rotate: -30 }, + label: { text: 'Month', offset: 12 }, + }, } ``` -| Option | Purpose | -| ------------- | -------------------------------------------------- | -| `guide` | Show or hide this axis, its title, ticks, and grid | -| `ticks` | Suggested tick count | -| `format` | Format a typed tick value | -| `grid` | Draw grid lines at ticks | -| `label` | Axis title | -| `reverse` | Reverse the responsive range | -| `tickRotate` | Rotate tick labels in degrees | -| `labelOffset` | Add distance between the axis and title | +| Option | Purpose | +| ----------------- | ---------------------------------------------------- | +| `axis` | Configure the axis or hide it with `false` | +| `axis.line` | Show or hide the baseline | +| `axis.ticks` | Configure candidates, stubs, padding, and formatting | +| `axis.tickLabels` | Configure label rotation and collision thinning | +| `axis.label` | Configure the axis title and offset | +| `grid` | Draw grid lines at semantic candidates | +| `reverse` | Reverse the responsive range | The y grid defaults to visible and the x grid defaults to hidden when `grid` is omitted. +Candidate generation and label layout are separate. Choose at most one of +`axis.ticks.count`, `axis.ticks.spacing`, and `axis.ticks.values`. Grid lines +and tick stubs use the generated candidates; label thinning does not remove +either. `axis.ticks.size: 0` removes stubs while retaining labels and grid +lines. + +Rotation and thinning are independent. Thinning is enabled by default and +uses measured rotated bounds: + +```ts +const x = { + scale: xScale, + axis: { + ticks: { spacing: 80 }, + tickLabels: { + rotate: -35, + thin: { minGap: 8, priority: 'ends', keep: importantDates }, + }, + }, +} +``` + +Hard-kept labels are retained even when they collide. Values absent from the +candidate set add labels only. + Hide one guide without removing its scale: ```ts const x = { scale: xScale, - guide: false, + axis: false, } ``` @@ -247,7 +275,8 @@ barX(rows, { The D3 band scale’s `paddingInner` and `paddingOuter` determine category spacing. `inset` removes additional pixels from both bar edges after layout. -For side-by-side bars, `groupScale` subdivides the primary bandwidth. See [Bars and Rankings](../examples/bars-and-rankings.md). +For side-by-side bars, `layout: group()` subdivides the primary bandwidth. See +[Bars and Rankings](../examples/bars-and-rankings.md). ## Scene and pointer coordinates @@ -327,9 +356,11 @@ const rankingChart = defineChart({ x: { scale: scaleLinear, nice: true, - label: '2015 population', - format: (value) => compact.format(value), grid: true, + axis: { + label: '2015 population', + ticks: { format: (value) => compact.format(value) }, + }, }, y: { scale: () => scaleBand().paddingInner(0.12).paddingOuter(0.06), diff --git a/docs/concepts/marks-and-layering.md b/docs/concepts/marks-and-layering.md index daa3203..15bf86d 100644 --- a/docs/concepts/marks-and-layering.md +++ b/docs/concepts/marks-and-layering.md @@ -92,11 +92,10 @@ rule. When `z` is omitted on a connected line or area, an authored `color` channel supplies the path groups as well as color semantics. Explicit `z` always wins when the two fields differ. -Bars use the primary band by default. Supply a configured D3 `groupScale` for -side-by-side bars; it groups by `z` when present and otherwise by `color`. -When rows should stack, prepare explicit `y1` and `y2` or `x1` and `x2` -intervals before the mark. TanStack Charts does not guess whether bars should -dodge, stack, or overlap. +Bars stack their single quantitative channel by default. Use +`layout: group({ scale })` for side-by-side bars; it groups by `z` when present +and otherwise by discrete `color`. Explicit `y1`/`y2` or `x1`/`x2` channels +opt out of implicit stacking and preserve authored intervals. ## Line and area gaps @@ -239,13 +238,13 @@ const temperatureChart = defineChart({ ], x: { scale: scaleUtc, - label: 'Day', + axis: { label: 'Day' }, }, y: { scale: scaleLinear, nice: true, - label: 'Temperature (°F)', grid: true, + axis: { label: 'Temperature (°F)' }, }, }) ``` diff --git a/docs/concepts/scales-and-d3.md b/docs/concepts/scales-and-d3.md index 9230802..bffd6f9 100644 --- a/docs/concepts/scales-and-d3.md +++ b/docs/concepts/scales-and-d3.md @@ -168,7 +168,9 @@ const categoryScale = () => TanStack Charts applies the plot range, reads the scale bandwidth, and treats the mapped value as the center of the band for mark and interaction coordinates. Bars use the primary bandwidth by default. -For grouped bars, pass a second band scale as the mark’s `groupScale`. Its range is assigned within the primary band. Grouping is explicit because `z` alone cannot decide whether a chart should overlap, stack, dodge, or only color its rows. +For grouped bars, use `layout: group({ scale })`. The supplied band scale is +copied and its range is assigned within the primary band. Grouping is explicit; +the default length-channel geometry is stacked. ## Color scales @@ -348,13 +350,13 @@ const logChart = defineChart({ ], x: { scale: scaleLog().domain([200, 30_000]), - label: 'Class size', grid: true, + axis: { label: 'Class size' }, }, y: { scale: scaleLinear, - label: 'Hierarchy depth', grid: true, + axis: { label: 'Hierarchy depth' }, }, }) ``` diff --git a/docs/examples/interactive-charts.md b/docs/examples/interactive-charts.md index 5852347..64aca10 100644 --- a/docs/examples/interactive-charts.md +++ b/docs/examples/interactive-charts.md @@ -16,6 +16,7 @@ selection, or product record. | Reader task | Start with | | ------------------------------------------------------------ | ---------------------------------------------- | | Inspect one point or a same-x group | Native chart focus and tooltip | +| Paint a band, rule, or mark only for the active datum/group | `whenFocused` around an ordinary mark | | Keep rich framework detail open, including another chart | Pinned composed tooltip body | | Navigate a wide schedule without changing its semantic scale | Native horizontal scrolling | | Crop and pan a continuous domain | Controlled zoom and viewport state | diff --git a/docs/examples/stacked-and-composition.md b/docs/examples/stacked-and-composition.md index a287d10..b803bc2 100644 --- a/docs/examples/stacked-and-composition.md +++ b/docs/examples/stacked-and-composition.md @@ -24,16 +24,25 @@ values remain available elsewhere. | Which subgroup values must be compared precisely across groups? | Grouped bars or aligned small multiples | | Do contributions extend in positive and negative directions? | Diverging stack around an explicit zero | -Prepare explicit interval endpoints for every stacked row. The application -owns series order, offset, normalization, and the denominator behind every -proportion. [Scales and D3](../concepts/scales-and-d3.md) defines that -preparation boundary. +Single-value bar and area channels stack implicitly. Use `stackY` when the +order or offset must be explicit; supply interval endpoints when the +application has already computed them. ## Preserve totals with a stacked area -A stacked area combines a shared ordered x domain with explicit lower and upper -boundaries for every series. The top boundary carries the total; the thickness -of each layer carries its contribution. +A stacked area combines a shared ordered x domain with one length per series. +The top boundary carries the total; the thickness of each layer carries its +contribution. + +```ts +areaY( + rows, + stackY( + { order: ['Core', 'Services'] }, + { x: 'date', y: 'value', color: 'series' }, + ), +) +``` -Treat offset and layer order as analytical parameters. Keep them deterministic -across revisions, preserve stable series colors, and provide exact values -through [Tooltips and Focus](../guides/tooltips-and-focus.md). +Use `offset: 'center'` or `offset: 'wiggle'` and an explicit order when the +composition depends on them. Keep those values stable across revisions, +preserve series colors, and provide exact values through +[Tooltips and Focus](../guides/tooltips-and-focus.md). Use an ordinary stacked area when totals or baselines are part of the question. diff --git a/docs/framework/octane/adapter.md b/docs/framework/octane/adapter.md index c8d1eac..e43766a 100644 --- a/docs/framework/octane/adapter.md +++ b/docs/framework/octane/adapter.md @@ -58,7 +58,7 @@ The default Node target renders the complete `.ts-chart-host`, `.ts-chart-surface`, and accessible SVG at `initialWidth`. The browser target hydrates the same structure before mounting the host. -The Canvas entry renders a deterministic named root and two `aria-hidden` +The Canvas entry renders a deterministic named root and three `aria-hidden` canvases on the server. It paints no server pixels. The browser adopts the elements, paints after mount, and attaches the same focus, keyboard, tooltip, and selection host. diff --git a/docs/framework/octane/quick-start.md b/docs/framework/octane/quick-start.md index 410df5e..f649710 100644 --- a/docs/framework/octane/quick-start.md +++ b/docs/framework/octane/quick-start.md @@ -56,9 +56,11 @@ const letterFrequencyChart = defineChart({ y: { scale: scaleLinear, nice: true, - label: 'Frequency', - format: (value) => percent.format(value), grid: true, + axis: { + label: 'Frequency', + ticks: { format: (value) => percent.format(value) }, + }, }, tooltip: true, }) diff --git a/docs/framework/react/adapter.md b/docs/framework/react/adapter.md index 7e932a5..826014d 100644 --- a/docs/framework/react/adapter.md +++ b/docs/framework/react/adapter.md @@ -68,7 +68,7 @@ The client renders the same initial structure, then the layout effect adopts and reconciles that SVG. There is no placeholder-only server mode. The Canvas entry emits the same outer structure with a named Canvas root and -two `aria-hidden` canvases. It does not paint pixels on the server. The client +three `aria-hidden` canvases. It does not paint pixels on the server. The client adopts those elements, paints after mount, and attaches the same focus, keyboard, tooltip, and selection host. diff --git a/docs/framework/react/quick-start.md b/docs/framework/react/quick-start.md index 0d88e12..76b1f04 100644 --- a/docs/framework/react/quick-start.md +++ b/docs/framework/react/quick-start.md @@ -56,9 +56,11 @@ const letterFrequencyChart = defineChart({ y: { scale: scaleLinear, nice: true, - label: 'Frequency', - format: (value) => percent.format(value), grid: true, + axis: { + label: 'Frequency', + ticks: { format: (value) => percent.format(value) }, + }, }, tooltip: true, }) diff --git a/docs/guides/accessibility.md b/docs/guides/accessibility.md index c3ed331..1ff0fb4 100644 --- a/docs/guides/accessibility.md +++ b/docs/guides/accessibility.md @@ -33,7 +33,7 @@ visible nearby: The SVG renderer emits an image role, a chart roledescription, and a `` when a description is supplied. The Canvas renderer places the same image role, name, roledescription, description, and tab index on its root while keeping its -two paint canvases `aria-hidden`. Do not put instructions, conclusions, and all +three paint canvases `aria-hidden`. Do not put instructions, conclusions, and all underlying data into one enormous accessible name. ## Preserve semantic context outside the surface diff --git a/docs/guides/dynamic-data-and-animation.md b/docs/guides/dynamic-data-and-animation.md index a755232..9500a93 100644 --- a/docs/guides/dynamic-data-and-animation.md +++ b/docs/guides/dynamic-data-and-animation.md @@ -27,7 +27,7 @@ function RankingChart({ rows, metric, accent }: Props) { x: { scale: scaleLinear, nice: true, - ticks: width < 420 ? 4 : 7, + axis: { ticks: { count: width < 420 ? 4 : 7 } }, }, y: { scale: () => scaleBand().padding(0.1), diff --git a/docs/guides/exporting.md b/docs/guides/exporting.md index 08f12f2..fb2fbd0 100644 --- a/docs/guides/exporting.md +++ b/docs/guides/exporting.md @@ -91,12 +91,12 @@ coordinate system. Pass the Canvas root or an ancestor containing it to the same `renderChartImage` or `downloadChartImage` functions. The exporter draws the base scene layer at the requested dimensions and scale. Set -`includeFocus: true` to composite the focus overlay; it is excluded by +`includeFocus: true` to composite the focus layers; they are excluded by default. -Canvas focus is painted on a separate overlay so pointer movement does not -repaint the base scene. Applications that need only the raw base bitmap may -also call `toBlob()` or `toDataURL()` on +Canvas focus is painted on underlay and overlay canvases so pointer movement +does not repaint the base scene. Applications that need only the raw base +bitmap may also call `toBlob()` or `toDataURL()` on `CanvasChartSurface.canvas`. Unlike SVG serialization, Canvas export does not retain vector geometry, accessible markup, or independently styleable nodes. @@ -129,7 +129,7 @@ needs embedded or inlined assets. - Fonts and external resources are portable. - Focus decoration is included only when meaningful. - Raster scale is chosen for the target medium. -- A Canvas export intentionally includes or excludes the focus overlay. +- A Canvas export intentionally includes or excludes focus layers. See [Rendering and Export](../reference/rendering-and-export.md) for every function and option. diff --git a/docs/guides/migrating.md b/docs/guides/migrating.md index ce3f47e..6954720 100644 --- a/docs/guides/migrating.md +++ b/docs/guides/migrating.md @@ -49,6 +49,16 @@ Map each visible layer independently: Then assign explicit scales and guides. Complex charts are usually several ordinary marks sharing a coordinate system, not one specialized chart type. +For the current breaking API: + +- move axis presentation under `axis`; +- use `axis: false` to hide one axis while retaining its scale; +- move candidate count and formatting under `axis.ticks`; +- move rotation and thinning under `axis.tickLabels`; +- use a single bar/area value as stack length, explicit endpoints to opt out, + and `layout: group()` for side-by-side bars; +- replace renderer-specific focus decoration with `whenFocused`. + See [Marks and Layering](../concepts/marks-and-layering.md) and the [Example Gallery](../examples/index.md). diff --git a/docs/guides/responsive-charts.md b/docs/guides/responsive-charts.md index 516c703..7933e5d 100644 --- a/docs/guides/responsive-charts.md +++ b/docs/guides/responsive-charts.md @@ -70,7 +70,7 @@ const definition = defineChart({ x: { scale: scaleLinear, nice: true, - label: 'Weekly downloads', + axis: { label: 'Weekly downloads' }, }, y: { scale: () => scaleBand().padding(0.1), @@ -87,9 +87,9 @@ margin: { ``` `margin: 0` locks every side and is appropriate for guide-free sparklines. -Automatic margins guarantee containment, not collision-free labels. Use -`ticks`, `tickRotate`, or a different representation when labels compete for -the same axis space. +Axis labels thin automatically after candidate generation and optional +rotation. Use `axis.ticks.spacing`, `axis.tickLabels.rotate`, hard-kept labels, +or a different representation when labels compete for the same axis space. -Treat offset and layer order as analytical parameters. Keep them deterministic -across revisions, preserve stable series colors, and provide exact values -through [Tooltips and Focus](../guides/tooltips-and-focus.md). +Use `offset: 'center'` or `offset: 'wiggle'` and an explicit order when the +composition depends on them. Keep those values stable across revisions, +preserve series colors, and provide exact values through +[Tooltips and Focus](../guides/tooltips-and-focus.md). Use an ordinary stacked area when totals or baselines are part of the question. diff --git a/packages/charts-core/docs/framework/octane/adapter.md b/packages/charts-core/docs/framework/octane/adapter.md index c8d1eac..e43766a 100644 --- a/packages/charts-core/docs/framework/octane/adapter.md +++ b/packages/charts-core/docs/framework/octane/adapter.md @@ -58,7 +58,7 @@ The default Node target renders the complete `.ts-chart-host`, `.ts-chart-surface`, and accessible SVG at `initialWidth`. The browser target hydrates the same structure before mounting the host. -The Canvas entry renders a deterministic named root and two `aria-hidden` +The Canvas entry renders a deterministic named root and three `aria-hidden` canvases on the server. It paints no server pixels. The browser adopts the elements, paints after mount, and attaches the same focus, keyboard, tooltip, and selection host. diff --git a/packages/charts-core/docs/framework/octane/quick-start.md b/packages/charts-core/docs/framework/octane/quick-start.md index 410df5e..f649710 100644 --- a/packages/charts-core/docs/framework/octane/quick-start.md +++ b/packages/charts-core/docs/framework/octane/quick-start.md @@ -56,9 +56,11 @@ const letterFrequencyChart = defineChart({ y: { scale: scaleLinear, nice: true, - label: 'Frequency', - format: (value) => percent.format(value), grid: true, + axis: { + label: 'Frequency', + ticks: { format: (value) => percent.format(value) }, + }, }, tooltip: true, }) diff --git a/packages/charts-core/docs/framework/react/adapter.md b/packages/charts-core/docs/framework/react/adapter.md index 7e932a5..826014d 100644 --- a/packages/charts-core/docs/framework/react/adapter.md +++ b/packages/charts-core/docs/framework/react/adapter.md @@ -68,7 +68,7 @@ The client renders the same initial structure, then the layout effect adopts and reconciles that SVG. There is no placeholder-only server mode. The Canvas entry emits the same outer structure with a named Canvas root and -two `aria-hidden` canvases. It does not paint pixels on the server. The client +three `aria-hidden` canvases. It does not paint pixels on the server. The client adopts those elements, paints after mount, and attaches the same focus, keyboard, tooltip, and selection host. diff --git a/packages/charts-core/docs/framework/react/quick-start.md b/packages/charts-core/docs/framework/react/quick-start.md index 0d88e12..76b1f04 100644 --- a/packages/charts-core/docs/framework/react/quick-start.md +++ b/packages/charts-core/docs/framework/react/quick-start.md @@ -56,9 +56,11 @@ const letterFrequencyChart = defineChart({ y: { scale: scaleLinear, nice: true, - label: 'Frequency', - format: (value) => percent.format(value), grid: true, + axis: { + label: 'Frequency', + ticks: { format: (value) => percent.format(value) }, + }, }, tooltip: true, }) diff --git a/packages/charts-core/docs/guides/accessibility.md b/packages/charts-core/docs/guides/accessibility.md index c3ed331..1ff0fb4 100644 --- a/packages/charts-core/docs/guides/accessibility.md +++ b/packages/charts-core/docs/guides/accessibility.md @@ -33,7 +33,7 @@ visible nearby: The SVG renderer emits an image role, a chart roledescription, and a `` when a description is supplied. The Canvas renderer places the same image role, name, roledescription, description, and tab index on its root while keeping its -two paint canvases `aria-hidden`. Do not put instructions, conclusions, and all +three paint canvases `aria-hidden`. Do not put instructions, conclusions, and all underlying data into one enormous accessible name. ## Preserve semantic context outside the surface diff --git a/packages/charts-core/docs/guides/dynamic-data-and-animation.md b/packages/charts-core/docs/guides/dynamic-data-and-animation.md index a755232..9500a93 100644 --- a/packages/charts-core/docs/guides/dynamic-data-and-animation.md +++ b/packages/charts-core/docs/guides/dynamic-data-and-animation.md @@ -27,7 +27,7 @@ function RankingChart({ rows, metric, accent }: Props) { x: { scale: scaleLinear, nice: true, - ticks: width < 420 ? 4 : 7, + axis: { ticks: { count: width < 420 ? 4 : 7 } }, }, y: { scale: () => scaleBand().padding(0.1), diff --git a/packages/charts-core/docs/guides/exporting.md b/packages/charts-core/docs/guides/exporting.md index 08f12f2..fb2fbd0 100644 --- a/packages/charts-core/docs/guides/exporting.md +++ b/packages/charts-core/docs/guides/exporting.md @@ -91,12 +91,12 @@ coordinate system. Pass the Canvas root or an ancestor containing it to the same `renderChartImage` or `downloadChartImage` functions. The exporter draws the base scene layer at the requested dimensions and scale. Set -`includeFocus: true` to composite the focus overlay; it is excluded by +`includeFocus: true` to composite the focus layers; they are excluded by default. -Canvas focus is painted on a separate overlay so pointer movement does not -repaint the base scene. Applications that need only the raw base bitmap may -also call `toBlob()` or `toDataURL()` on +Canvas focus is painted on underlay and overlay canvases so pointer movement +does not repaint the base scene. Applications that need only the raw base +bitmap may also call `toBlob()` or `toDataURL()` on `CanvasChartSurface.canvas`. Unlike SVG serialization, Canvas export does not retain vector geometry, accessible markup, or independently styleable nodes. @@ -129,7 +129,7 @@ needs embedded or inlined assets. - Fonts and external resources are portable. - Focus decoration is included only when meaningful. - Raster scale is chosen for the target medium. -- A Canvas export intentionally includes or excludes the focus overlay. +- A Canvas export intentionally includes or excludes focus layers. See [Rendering and Export](../reference/rendering-and-export.md) for every function and option. diff --git a/packages/charts-core/docs/guides/migrating.md b/packages/charts-core/docs/guides/migrating.md index ce3f47e..6954720 100644 --- a/packages/charts-core/docs/guides/migrating.md +++ b/packages/charts-core/docs/guides/migrating.md @@ -49,6 +49,16 @@ Map each visible layer independently: Then assign explicit scales and guides. Complex charts are usually several ordinary marks sharing a coordinate system, not one specialized chart type. +For the current breaking API: + +- move axis presentation under `axis`; +- use `axis: false` to hide one axis while retaining its scale; +- move candidate count and formatting under `axis.ticks`; +- move rotation and thinning under `axis.tickLabels`; +- use a single bar/area value as stack length, explicit endpoints to opt out, + and `layout: group()` for side-by-side bars; +- replace renderer-specific focus decoration with `whenFocused`. + See [Marks and Layering](../concepts/marks-and-layering.md) and the [Example Gallery](../examples/index.md). diff --git a/packages/charts-core/docs/guides/responsive-charts.md b/packages/charts-core/docs/guides/responsive-charts.md index 516c703..7933e5d 100644 --- a/packages/charts-core/docs/guides/responsive-charts.md +++ b/packages/charts-core/docs/guides/responsive-charts.md @@ -70,7 +70,7 @@ const definition = defineChart({ x: { scale: scaleLinear, nice: true, - label: 'Weekly downloads', + axis: { label: 'Weekly downloads' }, }, y: { scale: () => scaleBand().padding(0.1), @@ -87,9 +87,9 @@ margin: { ``` `margin: 0` locks every side and is appropriate for guide-free sparklines. -Automatic margins guarantee containment, not collision-free labels. Use -`ticks`, `tickRotate`, or a different representation when labels compete for -the same axis space. +Axis labels thin automatically after candidate generation and optional +rotation. Use `axis.ticks.spacing`, `axis.tickLabels.rotate`, hard-kept labels, +or a different representation when labels compete for the same axis space.