@scalewing/react 1.37.0 → 1.39.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +18 -0
- package/README.md +91 -1
- package/dist/accessible-name.d.ts +13 -0
- package/dist/accessible-name.js +1 -0
- package/dist/chart-axis.d.ts +61 -0
- package/dist/chart-axis.js +83 -0
- package/dist/chart-dates.d.ts +6 -0
- package/dist/chart-dates.js +15 -0
- package/dist/chart-metrics.d.ts +40 -0
- package/dist/chart-metrics.js +48 -0
- package/dist/chart-scale.d.ts +48 -0
- package/dist/chart-scale.js +116 -0
- package/dist/chart-table-rows.d.ts +8 -0
- package/dist/chart-table-rows.js +1 -0
- package/dist/column-chart-bars.d.ts +38 -0
- package/dist/column-chart-bars.js +82 -0
- package/dist/column-chart-callouts.d.ts +48 -0
- package/dist/column-chart-callouts.js +54 -0
- package/dist/column-chart-input.d.ts +40 -0
- package/dist/column-chart-input.js +60 -0
- package/dist/column-chart-layout.d.ts +63 -0
- package/dist/column-chart-layout.js +97 -0
- package/dist/components/BarChart.d.ts +1 -1
- package/dist/components/BarChart.js +1 -1
- package/dist/components/ChartLegend.d.ts +38 -0
- package/dist/components/ChartLegend.js +32 -0
- package/dist/components/ColumnChart.d.ts +72 -0
- package/dist/components/ColumnChart.js +71 -0
- package/dist/components/DataBar.d.ts +48 -0
- package/dist/components/DataBar.js +28 -0
- package/dist/components/LineChart.d.ts +83 -0
- package/dist/components/LineChart.js +77 -0
- package/dist/components/Pagination.d.ts +34 -0
- package/dist/components/Pagination.js +27 -0
- package/dist/components/ScrollRegion.d.ts +9 -5
- package/dist/components/ScrollRegion.js +8 -6
- package/dist/components/SectionNav.d.ts +2 -8
- package/dist/components/SegmentedControl.d.ts +2 -8
- package/dist/components/Table.d.ts +48 -4
- package/dist/components/Table.js +9 -3
- package/dist/components/Tabs.d.ts +2 -9
- package/dist/components/chart/ChartTable.d.ts +16 -0
- package/dist/components/chart/ChartTable.js +12 -0
- package/dist/components/chart/ChartValueAxis.d.ts +14 -0
- package/dist/components/chart/ChartValueAxis.js +11 -0
- package/dist/components/chart/use-chart-width.d.ts +8 -0
- package/dist/components/chart/use-chart-width.js +34 -0
- package/dist/components/column-chart/ChartCalloutList.d.ts +16 -0
- package/dist/components/column-chart/ChartCalloutList.js +13 -0
- package/dist/components/column-chart/ColumnChartDrawing.d.ts +17 -0
- package/dist/components/column-chart/ColumnChartDrawing.js +18 -0
- package/dist/components/line-chart/LineChartDrawing.d.ts +17 -0
- package/dist/components/line-chart/LineChartDrawing.js +16 -0
- package/dist/components/table/TableRow.d.ts +8 -2
- package/dist/components/table/TableRow.js +1 -1
- package/dist/components/table/table-max-height.d.ts +20 -0
- package/dist/components/table/table-max-height.js +23 -0
- package/dist/components/use-scroll-overflow.d.ts +10 -8
- package/dist/components/use-scroll-overflow.js +23 -10
- package/dist/css/chart-marks.d.ts +10 -0
- package/dist/css/chart-marks.js +15 -0
- package/dist/css/chart-tones.d.ts +68 -0
- package/dist/css/chart-tones.js +130 -0
- package/dist/css/chevron.d.ts +6 -3
- package/dist/css/chevron.js +9 -4
- package/dist/css/{css-chart.d.ts → css-bar-chart.d.ts} +2 -2
- package/dist/css/{css-chart.js → css-bar-chart.js} +2 -2
- package/dist/css/css-chart-legend.d.ts +5 -0
- package/dist/css/css-chart-legend.js +102 -0
- package/dist/css/css-chart-plot.d.ts +2 -0
- package/dist/css/css-chart-plot.js +170 -0
- package/dist/css/css-components.js +20 -2
- package/dist/css/css-data-bar.d.ts +11 -0
- package/dist/css/css-data-bar.js +91 -0
- package/dist/css/css-line-chart.d.ts +2 -0
- package/dist/css/css-line-chart.js +31 -0
- package/dist/css/css-pagination.d.ts +2 -0
- package/dist/css/css-pagination.js +63 -0
- package/dist/css/css-scroll-region.d.ts +1 -0
- package/dist/css/css-scroll-region.js +10 -5
- package/dist/css/css-table-bounded-edges.d.ts +1 -0
- package/dist/css/css-table-bounded-edges.js +132 -0
- package/dist/css/css-table-bounded.d.ts +2 -0
- package/dist/css/css-table-bounded.js +73 -0
- package/dist/css/css-table-ledger.d.ts +5 -2
- package/dist/css/css-table-ledger.js +5 -2
- package/dist/css/css-table-metrics.d.ts +14 -0
- package/dist/css/css-table-metrics.js +13 -0
- package/dist/css/css-table-sort.js +2 -1
- package/dist/css/css-table-total-row.d.ts +2 -0
- package/dist/css/css-table-total-row.js +29 -0
- package/dist/css/css-table.js +14 -3
- package/dist/css/stylesheet.js +14 -2
- package/dist/data-bar-geometry.d.ts +24 -0
- package/dist/data-bar-geometry.js +41 -0
- package/dist/index.d.ts +6 -1
- package/dist/index.js +5 -0
- package/dist/is-one-of.d.ts +5 -0
- package/dist/is-one-of.js +7 -0
- package/dist/line-chart-input.d.ts +39 -0
- package/dist/line-chart-input.js +70 -0
- package/dist/line-chart-layout.d.ts +68 -0
- package/dist/line-chart-layout.js +99 -0
- package/dist/line-chart-markers.d.ts +29 -0
- package/dist/line-chart-markers.js +57 -0
- package/dist/line-chart-path.d.ts +18 -0
- package/dist/line-chart-path.js +33 -0
- package/dist/line-chart-points.d.ts +6 -0
- package/dist/line-chart-points.js +8 -0
- package/dist/line-chart-table.d.ts +15 -0
- package/dist/line-chart-table.js +29 -0
- package/dist/pagination-pages.d.ts +13 -0
- package/dist/pagination-pages.js +22 -0
- package/dist/scroll-overflow.d.ts +12 -0
- package/dist/scroll-overflow.js +31 -7
- package/dist/styles.css +442 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,23 @@
|
|
|
1
1
|
# @scalewing/react
|
|
2
2
|
|
|
3
|
+
## 1.39.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 9a3234b: `ChartLegend`: a chart's key, a list (`role="list"` kept) of items, each an `aria-hidden` swatch and its words, in a wrapping row in the caption type. `items` (`ChartLegendItem[]`) take a `label`, a `swatch` (`ChartLegendSwatch`: `bar`, `line`, `marker` as a downward triangle, `box` as the boxed current period's dashed outline) and a `tone`: a `bar` a fill tone (`ChartFillTone`: the categorical colours, and `DataBar`'s statement tones), the others a stroke tone (`ChartStrokeTone`: `accent`, `success`, `warning`, `danger`, `neutral`); anything else, or an empty label, throws a `RangeError`. Every chart mark reads one tone table (`sw-chart-tone-<tone>` classes handing `--sw-chart-fill`, `--sw-chart-stroke` and `--sw-chart-ink` to the marks inside), so a swatch is the colour of the mark it names; fills keep 1.5:1, strokes 3:1 and inks 4.5:1 on the surface in every palette and scheme. `DataBar`'s fills now come from that table, unchanged. Forced colours draw every swatch in the system text colour. New exports `ChartLegend`, `ChartLegendProps`, `ChartLegendItem`, `ChartLegendSwatch`, `chartLegendSwatches`, `ChartTone`, `ChartFillTone`, `chartFillTones`, `ChartStrokeTone`, `chartStrokeTones`; new classes `sw-chart-legend`, `sw-chart-legend-item`, `sw-chart-legend-swatch`, `sw-chart-legend-<swatch>` and `sw-chart-tone-<tone>`; no new tokens, no new dependency. Request: `docs/requests/teisoro-chart-legend.md`.
|
|
8
|
+
- 2eb7b03: `ColumnChart`: columns for each of its `categories`, one per series (`ColumnChartSeries`: `id`, `label`, a value per category, a categorical `tone`), `mode` `'stacked'` (positives up from zero, negatives down) or `'grouped'` (side by side), with an optional accent `line` (`ColumnChartLine`, such as a running total) on its own right-hand scale whose zero and grid lines are level with the columns' and whose last value is written beside that axis (`endLabel`), a boxed `highlight` (the current period) and `callouts` (`ColumnChartCallout`) pinned above a column with a dashed leader. `formatValue` writes the figures and `formatTick` the axes. It is an SVG laid out for the width it measures: under 480 px it is shorter, labels fewer categories and lists its callouts under the plot, never scrolling sideways. The drawing is `aria-hidden`; a visually hidden table captioned by `label` carries every figure, the boxed row `aria-current`, and the callouts stay a list. Colours come from the chart tones (ChartLegend's), and forced colours use system colours. Input it cannot draw throws a `RangeError`. New exports `ColumnChart`, `ColumnChartProps`, `ColumnChartSeries`, `ColumnChartLine`, `ColumnChartCallout`, `ColumnChartMode`, `columnChartModes`; new `sw-chart-*` plot classes (`sw-chart`, `sw-chart-svg`, `sw-chart-tick`, `sw-chart-tick-line`, `sw-chart-category`, `sw-chart-grid`, `sw-chart-zero`, `sw-chart-highlight`, `sw-chart-line`, `sw-chart-dot`, `sw-chart-end-label`, `sw-chart-leader`, `sw-chart-callout-text`, `sw-chart-column`, `sw-chart-callouts`, `sw-chart-callout`, `sw-chart-callout-dot`); no new tokens, no new dependency. Request: `docs/requests/teisoro-column-chart.md`.
|
|
9
|
+
- 8dcd928: `DataBar` `tone="danger"`: a light red range bar for a shortage, never for cash leaving (money out stays `muted`). `DataBarTone` and `dataBarTones` gain `'danger'`, last; its fill is the danger colour at 40 % mixed into the surface, at 1.5:1 or more on it in every palette and scheme like every tone, and red beside the grey of money out. The shared chart fill tones gain it too, so a `ChartLegend` `bar` swatch can name it. New class `sw-data-bar-danger`; no other tone changes; no new tokens, no new dependency. Request: `docs/requests/teisoro-data-bar-short.md`.
|
|
10
|
+
- 88d1b95: `LineChart`: a value over time, `points` (`LineChartPoint`: a `YYYY-MM-DD` `x` and a value `y`, in date order) placed by the days between them, `curve` `'linear'` or `'step'` (each value held until the next date), an optional `area` tinted from the line's stroke `tone` (default `accent`), `markers` (`LineChartMarker`: a downward triangle and words at one of the points' dates, in a stroke tone; words that would meet are left out of the drawing) with `markersLabel` heading their table column, `ticks` (the dates the time axis labels, thinned from the last), a boxed `highlight` period (`LineChartPeriod`), the last value beside the plot (`endLabel`), and `formatValue`, `formatTick` and `formatDate`. It is laid out for its measured width like ColumnChart, never scrolling sideways; the drawing is `aria-hidden` and a visually hidden table captioned by `label` (its value column `valueLabel`) carries every date, value and marker, the boxed rows `aria-current`. Input it cannot draw throws a `RangeError`. New exports `LineChart`, `LineChartProps`, `LineChartPoint`, `LineChartMarker`, `LineChartPeriod`, `LineChartCurve`, `lineChartCurves`; new classes `sw-chart-area`, `sw-chart-marker`, `sw-chart-marker-label`; no new tokens, no new dependency. Request: `docs/requests/teisoro-line-chart.md`.
|
|
11
|
+
|
|
12
|
+
## 1.38.0
|
|
13
|
+
|
|
14
|
+
### Minor Changes
|
|
15
|
+
|
|
16
|
+
- ac063ea: `DataBar`: a decorative (`aria-hidden`) horizontal range bar for a table cell or a list row, from `from` to `to` on a track whose domain runs from `min` to `max`, such as where a statement's line leaves the running total. A domain from below zero to above it draws a zero line, so a column of differences diverges around it, the bar's end at zero square. `tone` (`DataBarTone`: `neutral`, `success`, `muted`, `strong`, `accent`; no danger tone, money out is `muted`) colours it from the semantic colours mixed into the surface. Values past the domain are clamped; non-finite numbers, a `min` not under `max` and an unknown tone throw a `RangeError`. The bar follows the writing direction and is drawn in the system text colour in forced colours. New exports `DataBar`, `DataBarProps`, `DataBarTone`, `dataBarTones`; new classes `sw-data-bar`, `sw-data-bar-fill`, `sw-data-bar-diverging`, `sw-data-bar-zero-start`, `sw-data-bar-zero-end` and `sw-data-bar-<tone>`; no new tokens, no new dependency. Request: `docs/requests/teisoro-data-bar.md`.
|
|
17
|
+
- b95f9ea: `Pagination`: previous and next steps through a list shown a page at a time, in a `nav` landmark named by `aria-label` or `aria-labelledby`. It shows the consumer's `rangeLabel` (such as "1–25 of 130 changes") at the inline start, a polite live region, and the steps at the end: icon-only neutral `sm` Buttons named by `labels` (`PaginationLabels`: `previous`, `next`), square, 44 px on a coarse pointer, with a private chevron mirrored right to left. `page` and `pageCount` count from 1 and `onPageChange` receives the page a step goes to. At either end a step is `aria-disabled` (the neutral grey) but stays a Tab stop, so focus is not dropped at the last page, and a press there does nothing. Invalid pages or an empty step label (`labels.previous` or `labels.next`) throw a `RangeError`. New exports `Pagination`, `PaginationProps`, `PaginationLabels`; new classes `sw-pagination`, `sw-pagination-range`, `sw-pagination-steps`, `sw-pagination-step`, `sw-pagination-glyph`, `sw-pagination-glyph-previous` and `sw-pagination-glyph-next`; no new tokens, no new dependency. Request: `docs/requests/teisoro-pagination.md`.
|
|
18
|
+
- 744ed13: `Table` `maxHeight` (opt-in): bounds a table's height by a row count, `{ rows: number }` (new type `TableMaxHeight`), and the rest of the rows scroll inside the table's own region. The region is as tall as that many single-line body rows plus the header row and the footer row when there is one, from the type and spacing tokens the cells are drawn with (compact density included; a sorting header as tall as its buttons, the 44 px touch target on a coarse pointer). The header pins to the region's top and a `TableFooter sticky` total to its bottom; the region stays a keyboard stop named after the table, and an edge with rows scrolled past it draws the scroll shade, cast from the pinned header or total, or from the region's own edge where nothing is pinned (none in forced colours). While the region has keyboard focus, the pinned header and total carry the part of its focus ring they cover. Printed, every row shows. Page pinning (`stickyHeader="page"`, a sticky footer in a table that fits) is unchanged for tables without `maxHeight`; with it, `stickyHeader` `'page'` or `false` throw a `RangeError`, as does a row count that is not a whole number from 1. New classes `sw-table-bounded`, `sw-scroll-more-top` and `sw-scroll-more-bottom`; no new tokens, no new dependency. Request: `docs/requests/teisoro-table-contained-scroll.md`.
|
|
19
|
+
- b638b5e: `TableRow` `variant="total"`: a total mid-table, such as a subtotal the rows above add up to before the table goes on. Its cells are at weight 600 under a 2 px rule across its top (the cells' own collapsed border, kept in forced colours), as a `TableFooter` total is drawn, with no fill, so accent text keeps the contrast it has on the table's own surface. `tableRowVariants` is now `['default', 'sub', 'total']`. New class `sw-table-row-total`; no new tokens, no new dependency. Request: `docs/requests/teisoro-table-row-total.md`.
|
|
20
|
+
|
|
3
21
|
## 1.37.0
|
|
4
22
|
|
|
5
23
|
### Minor Changes
|
package/README.md
CHANGED
|
@@ -10,6 +10,10 @@ import {
|
|
|
10
10
|
ActionBar,
|
|
11
11
|
ActionMenu,
|
|
12
12
|
BarChart,
|
|
13
|
+
ChartLegend,
|
|
14
|
+
ColumnChart,
|
|
15
|
+
LineChart,
|
|
16
|
+
DataBar,
|
|
13
17
|
Box,
|
|
14
18
|
Button,
|
|
15
19
|
CalendarButton,
|
|
@@ -18,6 +22,7 @@ import {
|
|
|
18
22
|
RadioGroup,
|
|
19
23
|
SectionNav,
|
|
20
24
|
Spinner,
|
|
25
|
+
Pagination,
|
|
21
26
|
Progress,
|
|
22
27
|
Tooltip,
|
|
23
28
|
InfoTip,
|
|
@@ -418,7 +423,7 @@ An option's optional `description` (a `ReactNode`) is secondary text for that op
|
|
|
418
423
|
|
|
419
424
|
A header `TableCell` (`as="th"`) with `sort` (`'ascending'`, `'descending'` or `'none'`) and `onSort` is its column's sort control: its children become a button in the header's own style with a chevron (before the name in a `numeric` or end-aligned column), and the cell carries `aria-sort` only while it is the sorted column. `sort` on a data cell, or one of the two without the other, throws a `TypeError`. `Table` `layout="fixed"` (default `auto`) sizes the columns from the header row alone, so a filter that hides a row moves no column, and `verticalAlign="top"` (default `middle`) starts every cell of a tall row on its first line. `TableCell` `width` is `min` (an auto table's column takes the least room its content allows, on one line; set it on each cell of the column) or a size, `xs` to `xl` (4, 6, 8, 12 and 16 rem); in a fixed table a column without a width shares what is left, so leave one column, such as a description, unsized. The HTML `width` attribute is not accepted. Unknown values throw a `RangeError`.
|
|
420
425
|
|
|
421
|
-
A ledger read across as well as down takes `Table` `columnRules` (a hairline at the inline start of every cell after a row's first, mirrored right to left) and `TableCell` `tone` (`success`, `danger`, `muted` or `accent`: the cell's text in the theme colour, as `Text color` uses it; never the only signal, so keep the sign or the words). `TableRow` `variant="sub"` (default `"default"`) sets a row under the one above, as a batch's items: its row header, a `TableCell as="th" scope="row"`, is indented a `space-6` step with a guide bar at the indent (a sub row without a row header is not indented). `TableRow` `struck` keeps a voided row on the record: the subtle fill and the text muted and struck through, toned cells included; it works with
|
|
426
|
+
A ledger read across as well as down takes `Table` `columnRules` (a hairline at the inline start of every cell after a row's first, mirrored right to left) and `TableCell` `tone` (`success`, `danger`, `muted` or `accent`: the cell's text in the theme colour, as `Text color` uses it; never the only signal, so keep the sign or the words). `TableRow` `variant="sub"` (default `"default"`) sets a row under the one above, as a batch's items: its row header, a `TableCell as="th" scope="row"`, is indented a `space-6` step with a guide bar at the indent (a sub row without a row header is not indented). `TableRow` `variant="total"` is a total mid-table, such as a subtotal the rows above add up to before the table goes on: its cells at weight 600 under a 2 px rule across its top (the cells' own collapsed top border, so forced colors keep it), as a `TableFooter` total is drawn, with no fill, so accent text in it keeps the contrast it has on the table's own surface. Name it with a row header and keep it in the body, after the rows it adds up (not the body's first row under a pinned header, whose rule would sit on its own); a cell's `tone` keeps its colour, and `struck` works on it. Its cells are all at weight 600, so a quiet link or a `TableCell onPress` action in it is no longer set apart by weight: keep a total row's name plain text (a `TableFooter` row is the same). `TableRow` `struck` keeps a voided row on the record: the subtle fill and the text muted and struck through, toned cells included; it works with any variant. A line-through is not read aloud, so say why in the row, such as a `Badge` "Voided": a Badge in a line of text is not struck, but one that is itself a flex item (a direct child of `Inline`) is, so put it in the title's line. A row header in the body or footer (`th scope="row"`) reads as body text. An unknown `tone` or `variant` throws a `RangeError`.
|
|
422
427
|
|
|
423
428
|
`Table` `stickyHeader` pins the header: the whole `TableHeader` as one group, every row in it (the column headings and any row under them, such as a running total) with all its cells, `th` and `td`, each row under the one above, on the glass with its row and column rules. `true` (default) pins it to the top of the table's own scroll region, `false` lets it scroll away, and `'page'` pins it to the page's top as the page scrolls. With `'page'`, while the table fits its width its region stops being a scroll box (`sw-table-sticky-page`), so the header stays in view under any sticky `AppHeader` (which covers it); a table wider than its region scrolls sideways in it and pins inside it. Use it for a long table whose last row, such as a non-sticky `TableFooter` total, should scroll with the rows. An unknown value throws a `RangeError`.
|
|
424
429
|
|
|
@@ -430,8 +435,93 @@ A ledger read across as well as down takes `Table` `columnRules` (a hairline at
|
|
|
430
435
|
|
|
431
436
|
`TableFooter` is the table's `tfoot`, for a total: its cells are set at weight 600 under a 2 px rule across its first row (drawn on each cell's `::before`, so forced colors keep it), and a `TableCell as="th" scope="row"` in it names the total. `sticky` pins the footer to the bottom edge on the header's glass (solid under Reduce Transparency, the system canvas in forced colors), on its own layer over the cells and the pinned header. While the table fits its width, a table with a sticky footer stops being a scroll box, so the footer pins to the page's bottom and the sticky header to its top as the page scrolls; a sticky `AppHeader` covers that header. A table wider than its region scrolls sideways in it and pins to the region instead. A sticky `ActionBar` covers a pinned footer, and so does a `Dialog`'s pinned footer, so do not pair them.
|
|
432
437
|
|
|
438
|
+
`Table` `maxHeight` (opt-in, `{ rows: number }`, type `TableMaxHeight`) bounds the table's height by a row count, and the rest of the rows scroll inside the table's own region: the region is as tall as that many single-line body rows plus the header row and, when there is one, the footer row (from the type and spacing tokens the cells are drawn with, compact density included, a sorting header as tall as its buttons and the 44 px touch target on a coarse pointer; rows of two lines or with controls, a second header or footer row, and a horizontal scrollbar that takes room show fewer at a time). The header pins to the region's top and a `TableFooter sticky` total to its bottom, so a total over every row stays in view while the rows scroll; a non-sticky footer scrolls with the rows. The region stays a keyboard stop named after the table, and while rows are scrolled out past its top or bottom that edge draws the scroll shade (`sw-scroll-more-top`, `sw-scroll-more-bottom`): cast from the pinned header's last row (a header cell spanning rows from an earlier row casts none) or total over the rows under it, or from the region's own edge where nothing is pinned; none in forced colors. While the region has keyboard focus, the pinned header and total carry the part of its focus ring they cover (their side parts on the first and last cells, so avoid `rowSpan` in a bounded table's header). Printed, every row shows. A bounded table never pins to the page, and without `maxHeight` the page pinning above is unchanged. `stickyHeader` `'page'` or `false` with it, a value that is not `{ rows }`, or a row count that is not a whole number from 1 throw a `RangeError`.
|
|
439
|
+
|
|
440
|
+
```tsx
|
|
441
|
+
<Table aria-label="Closeouts" maxHeight={{ rows: 12 }}>
|
|
442
|
+
<TableHeader>…</TableHeader>
|
|
443
|
+
<TableBody>…</TableBody>
|
|
444
|
+
<TableFooter sticky>…</TableFooter>
|
|
445
|
+
</Table>
|
|
446
|
+
```
|
|
447
|
+
|
|
433
448
|
`BarChart` `formatValue` writes the axis's values and each value without its own `valueLabel`, such as a currency. `diverging` puts zero in the middle of each track behind a hairline: a positive bar grows toward the inline end and a negative one toward the start, on one scale, and the axis reads minus the maximum, zero and the maximum. Without it every bar grows from the start. An item's `tone` (`BarChartTone`: `'accent'`, `'success'`, `'warning'` or `'danger'`, listed in the exported `barChartTones`) colors its bar by what it means in place of its sign's default (the accent, or danger for a negative value), such as a diverging chart's overage in `'warning'` and shortage in `'danger'`; a toned bar keeps its sign's place, every tone keeps 3:1 against the track, and an unknown tone throws a `RangeError`. Forced colors draw every bar in the system text color, so the value and its side of zero carry the meaning there.
|
|
434
449
|
|
|
450
|
+
`DataBar` is a horizontal range bar for a table cell or a list row: a bar from `from` to `to` (whichever is larger is its far end) on a track whose domain runs from `min` to `max`, such as where a statement's line leaves the running total. It fills its container's width, a spacing step 4 tall with no track fill, and its ends follow the writing direction. It is decorative (`aria-hidden`): the figure is the cell's own text, so keep the value beside it, and a cell holding both is named by the figure alone. `tone` (`DataBarTone`, listed in `dataBarTones`; default `'neutral'`) says what the bar is: `'neutral'` for where it started, `'success'` for what came in, `'muted'` for what went out (never danger: an outflow is not an error), `'strong'` for a subtotal, `'accent'` for what was counted or recorded, and `'danger'` for a shortage, never for cash leaving (a light red, the danger colour at 40 %). Each is a semantic colour mixed into the surface, 1.5:1 or more on it, the three greys 1.5:1 or more apart, the shortage's red apart from the grey of what went out, in every palette and scheme. A domain from below zero to above it (`min < 0 < max`) draws a hairline at zero, so a column of differences diverges around it (`from={0} to={value}`), and the bar's end at zero is square; a short bar in `'danger'` and an over bar in `'success'` differ by hue alone, so the bar's side of zero and the signed figure carry the meaning. Values past the domain are clamped to it; non-finite numbers, a `min` not under `max`, or an unknown tone throw a `RangeError`. Forced colors draw the bar and the zero line in the system text color.
|
|
451
|
+
|
|
452
|
+
A bar in its own column reads as empty cells to assistive technology, which is fine while the figure's cell carries the value; a bar and its figure can also share one cell, side by side in a `Grid`.
|
|
453
|
+
|
|
454
|
+
```tsx
|
|
455
|
+
<TableCell numeric>
|
|
456
|
+
<Grid align="center" columns={2} gap={2}>
|
|
457
|
+
<DataBar from={0} to={-6} min={-10} max={10} tone="muted" />
|
|
458
|
+
<span>−6</span>
|
|
459
|
+
</Grid>
|
|
460
|
+
</TableCell>
|
|
461
|
+
```
|
|
462
|
+
|
|
463
|
+
`ChartLegend` is a chart's key: a list of its marks, each a swatch and its words, in a wrapping row in the caption type, placed above the chart or the statement bars it names. `items` (`ChartLegendItem[]`) gives each one's `label`, `swatch` (`ChartLegendSwatch`, listed in `chartLegendSwatches`: `'bar'`, `'line'`, `'marker'` or `'box'`) and `tone`. A `'bar'` is a filled mark and takes a fill tone (`ChartFillTone`, listed in `chartFillTones`): a categorical colour (`'green'`, `'blue'`, `'orange'`, `'purple'`, `'teal'`, `'rose'`, `'neutral'`) for a chart's series, or one of `DataBar`'s statement tones (`'success'`, `'muted'`, `'strong'`, `'accent'`, `'danger'`, and `'neutral'`; a `'bar'` in `'danger'` is DataBar's light shortage fill, for a shortage only, never for cash leaving, and not BarChart's full-colour danger bar, which is the `'line'` swatch's colour). A `'line'`, a `'marker'` (a downward triangle) and a `'box'` (the dashed outline of a boxed current period) are strokes and take a stroke tone (`ChartStrokeTone`, listed in `chartStrokeTones`: `'accent'`, `'success'`, `'warning'`, `'danger'`, `'neutral'`). `ChartTone` is either. Each swatch is the colour of the mark it names in the same tone, because the legend, `DataBar` and the charts read one tone table: a fill keeps 1.5:1 on the surface, a stroke 3:1, in every palette and scheme. The swatches are decorative (`aria-hidden`), so each item reads as its words; the list keeps its role (`role="list"`, which Safari drops from an unmarked list) and takes a name with `aria-label`. An empty or blank `label`, or a swatch or tone the item cannot draw, throws a `RangeError`. Forced colors draw every swatch in the system text color. The contrasts hold on the surface, so put a chart and its legend on it (a `Card`). A `BarChart`'s default bar is a `'bar'` swatch in `'accent'`; its toned bars are drawn in the full semantic colour, the `'line'` swatch's colour.
|
|
464
|
+
|
|
465
|
+
```tsx
|
|
466
|
+
<ChartLegend
|
|
467
|
+
aria-label="Key"
|
|
468
|
+
items={[
|
|
469
|
+
{ label: 'Herons', swatch: 'bar', tone: 'blue' },
|
|
470
|
+
{ label: 'Running total', swatch: 'line', tone: 'accent' },
|
|
471
|
+
{ label: 'Flood', swatch: 'marker', tone: 'danger' },
|
|
472
|
+
{ label: 'This week', swatch: 'box', tone: 'accent' },
|
|
473
|
+
]}
|
|
474
|
+
/>
|
|
475
|
+
```
|
|
476
|
+
|
|
477
|
+
`ColumnChart` draws columns for each of its `categories` (weeks, months: each a row header in its table), one per series (`ColumnChartSeries`: a unique `id`, a `label`, a value per category and a categorical `tone`). `mode` (`ColumnChartMode`, listed in `columnChartModes`) is `'stacked'` by default, a category's positive values stacked up from zero and its negative values down from it, or `'grouped'`, side by side, each from zero; negative values always run below the zero line. `line` (`ColumnChartLine`: `label`, a value per category, `endLabel`) draws an accent line over the columns, such as a running total, on its own right-hand scale whose zero and grid lines are level with the columns' (each scale keeps its own round step); its last value is written beside that axis, in the accent's ink, unless `endLabel` is `false`. `highlight` boxes one category (a dashed accent outline, the current period), and its table row is `aria-current`. `callouts` (`ColumnChartCallout[]`: a category `index`, its `text` and an optional categorical `tone` for its words) are pinned in rows above the plot, each beside a dashed leader down to its column. `formatValue` writes the figures (the table and the line's last value; a plain number with at most one decimal by default) and `formatTick` the axes (defaulting to `formatValue`). The chart fills its container's width and is laid out for it in its own pixels, so its words stay the caption size; under 480 px it is compact: a shorter plot, fewer category labels (always the last) and its callouts listed under the plot, so it never scrolls sideways. The drawing is hidden from assistive technology: a visually hidden table captioned by `label` carries every figure, and the callouts stay a list. Fills, strokes and words come from the chart tones, so a `ChartLegend` above it names each series (`bar` in its tone), the line (`line`, `'accent'`) and the box (`box`, `'accent'`); put both on the surface (a `Card`). Forced colors draw the columns in the text color (outlined in the canvas color), the line in the highlight color and every word in the text color. Empty words, a series or line without one finite value per category, a repeated series id, a tone off the categorical scale, or a highlight or callout that is not a category's index throw a `RangeError`.
|
|
478
|
+
|
|
479
|
+
```tsx
|
|
480
|
+
<ColumnChart
|
|
481
|
+
label="Change in birds, last 13 weeks"
|
|
482
|
+
categories={weeks}
|
|
483
|
+
series={[
|
|
484
|
+
{ id: 'herons', label: 'Herons', tone: 'blue', values: herons },
|
|
485
|
+
{ id: 'egrets', label: 'Egrets', tone: 'purple', values: egrets },
|
|
486
|
+
]}
|
|
487
|
+
line={{ label: 'Running total', values: runningTotal }}
|
|
488
|
+
highlight={weeks.length - 1}
|
|
489
|
+
callouts={[{ index: 3, text: 'Aug 4 · Roost found +50', tone: 'purple' }]}
|
|
490
|
+
formatValue={signed}
|
|
491
|
+
/>
|
|
492
|
+
```
|
|
493
|
+
|
|
494
|
+
`LineChart` draws a value over time: `points` (`LineChartPoint[]`: `x` a `YYYY-MM-DD` date, `y` a value), in date order (a date may repeat, as a balance that changes twice in a day), placed by the days between them. `curve` (`LineChartCurve`, listed in `lineChartCurves`) is `'linear'` by default or `'step'`, each value held until the next date. `area` tints the area between the line and zero with its tone, and `tone` (a `ChartStrokeTone`, `'accent'` by default) colors the line, its dot and its last value, written beside the plot's end unless `endLabel` is `false`. `markers` (`LineChartMarker[]`: `x`, one of the points' dates; `label`; a stroke `tone`) put a downward triangle on the line at that date's last value with its words below it (above it where they would pass the plot's bottom), moved in to stay over the plot; words that would meet an earlier marker's are left out of the drawing, never the table. `markersLabel` heads the table's column of marker words and is required with markers. `ticks` are the dates the time axis labels (the first and last point by default), thinned from the last so none meet; `highlight` (`LineChartPeriod`: `from`, `to`) boxes a period, its rows `aria-current`. `formatValue`, `formatTick` (the value axis) and `formatDate` (the time axis and the table's row headers; the date itself by default) write its words. It is laid out for its measured width like `ColumnChart`, never scrolling sideways, and its drawing is hidden: a visually hidden table captioned by `label`, its value column headed by `valueLabel` (default `label`), carries every date, value and marker. A `ChartLegend` above it names the line (`line` in its tone), the markers (`marker` in theirs) and the box (`box`, `'accent'`). Forced colors draw the line in the highlight color, the markers and every word in the text color, and drop the area. Empty words, a date that is not `YYYY-MM-DD`, points out of date order, a value that is not finite, an unknown curve or tone, a marker off the points' dates or without `markersLabel`, a tick outside the points' dates, or a period that ends before it starts throw a `RangeError`.
|
|
495
|
+
|
|
496
|
+
```tsx
|
|
497
|
+
<LineChart
|
|
498
|
+
label="Birds roosting each night"
|
|
499
|
+
valueLabel="Roosting"
|
|
500
|
+
points={nights}
|
|
501
|
+
area
|
|
502
|
+
markers={[{ x: '2026-08-12', label: '2600', tone: 'warning' }]}
|
|
503
|
+
markersLabel="A flock left"
|
|
504
|
+
ticks={['2026-07-27', '2026-08-17', '2026-10-12']}
|
|
505
|
+
highlight={{ from: '2026-10-06', to: '2026-10-12' }}
|
|
506
|
+
formatDate={shortDate}
|
|
507
|
+
/>
|
|
508
|
+
|
|
509
|
+
<LineChart label="Nest boxes lent" curve="step" tone="warning" points={loans} />
|
|
510
|
+
```
|
|
511
|
+
|
|
512
|
+
`Pagination` steps through a list shown a page at a time, for a list with no total to keep in view (a list that adds up scrolls inside itself instead, with `Table` `maxHeight`). It is a `nav` landmark named by `aria-label` or `aria-labelledby` (one is required), holding the consumer's words for what the page shows (`rangeLabel`, such as "1–25 of 130 changes", in the muted caption at the inline start and announced politely when it changes) and the previous and next steps at the end: icon-only neutral `sm` Buttons, square, the 44 px touch target on a coarse pointer, named by `labels` (`PaginationLabels`: `previous`, `next`) with a private chevron glyph mirrored right to left. `page` (from 1) and `pageCount` place it; a step calls `onPageChange` with the page it goes to. At either end its step looks disabled (`aria-disabled`, the neutral grey) but stays a Tab stop, so focus is never dropped when the last page is reached, and a press there does nothing. Pages that are not whole numbers in range, or an empty step label (`labels.previous` or `labels.next`), throw a `RangeError`. The row wraps on a narrow column, the steps staying at the end.
|
|
513
|
+
|
|
514
|
+
```tsx
|
|
515
|
+
<Pagination
|
|
516
|
+
aria-label="Change pages"
|
|
517
|
+
page={page}
|
|
518
|
+
pageCount={6}
|
|
519
|
+
onPageChange={setPage}
|
|
520
|
+
rangeLabel="1–25 of 130 changes"
|
|
521
|
+
labels={{ previous: 'Previous page', next: 'Next page' }}
|
|
522
|
+
/>
|
|
523
|
+
```
|
|
524
|
+
|
|
435
525
|
`Spinner` shows indeterminate loading in small, medium, or large sizes. Supply localized `label` for the one announced status in a loading region. Use `decorative` on additional indicators beside that status so screen readers do not hear the same message repeatedly. Reduced motion leaves a static accented ring.
|
|
436
526
|
|
|
437
527
|
`GlyphTile` is a small rounded square in a categorical colour with the consumer's glyph in white: the mark of a kind beside its name, such as a service in a list. `hue` is one of the `@scalewing/tokens` categorical colours (`green`, `blue`, `orange`, `purple`, `teal`, `rose`, or `neutral`); which kind takes which hue is yours. `size` is `sm` (24), `md` (28, the default) or `lg` (32); the glyph is drawn at spacing step 4 in white, which keeps 3:1 on every fill; pass the SVG (a Lucide component) as the tile's direct child, since only a direct-child SVG is sized. It is always `aria-hidden`, so put the kind's name in visible text beside it. An unknown `hue` or `size` throws a `RangeError`.
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A component's accessible name, given one way: `aria-label` with the words,
|
|
3
|
+
* or `aria-labelledby` with the id of an element that holds them, never both.
|
|
4
|
+
* Shared by the components whose name is required (a group, a list of tabs,
|
|
5
|
+
* a navigation landmark).
|
|
6
|
+
*/
|
|
7
|
+
export type AccessibleName = {
|
|
8
|
+
'aria-label': string;
|
|
9
|
+
'aria-labelledby'?: never;
|
|
10
|
+
} | {
|
|
11
|
+
'aria-label'?: never;
|
|
12
|
+
'aria-labelledby': string;
|
|
13
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { type ChartScale } from './chart-scale.js';
|
|
2
|
+
/** A value axis's grid line and its words, at a height in the plot. */
|
|
3
|
+
export type ChartTick = {
|
|
4
|
+
value: number;
|
|
5
|
+
y: number;
|
|
6
|
+
label: string;
|
|
7
|
+
};
|
|
8
|
+
/** A plot's edges, in the chart's pixels. */
|
|
9
|
+
export type ChartPlot = {
|
|
10
|
+
left: number;
|
|
11
|
+
right: number;
|
|
12
|
+
top: number;
|
|
13
|
+
bottom: number;
|
|
14
|
+
};
|
|
15
|
+
/** The height a value falls at in a plot, on a scale. */
|
|
16
|
+
export declare function valueY(value: number, scale: ChartScale, plot: ChartPlot): number;
|
|
17
|
+
/** A scale's ticks, placed in a plot and written by `format`. */
|
|
18
|
+
export declare function valueTicks(scale: ChartScale, plot: ChartPlot, format: (value: number) => string): ChartTick[];
|
|
19
|
+
/** The room a column of labels takes beside the plot, with its gap. */
|
|
20
|
+
export declare function labelGutter(labels: readonly string[]): number;
|
|
21
|
+
/**
|
|
22
|
+
* Which of `count` evenly spaced places, `slot` pixels apart, get their
|
|
23
|
+
* label: every one when the widest label and a gap either side fit a
|
|
24
|
+
* slot, otherwise every second, third and so on, counted back from the
|
|
25
|
+
* last, so the latest place is always labelled.
|
|
26
|
+
*/
|
|
27
|
+
export declare function labelledPlaces(labels: readonly string[], slot: number): number[];
|
|
28
|
+
/**
|
|
29
|
+
* Drops the ticks whose words would meet a label drawn at `y` in the same
|
|
30
|
+
* column (such as a line's last value beside its axis).
|
|
31
|
+
*/
|
|
32
|
+
export declare function ticksClearOf(ticks: readonly ChartTick[], y: number): ChartTick[];
|
|
33
|
+
/**
|
|
34
|
+
* The room past a plot's end: a line's own axis words and its last value
|
|
35
|
+
* beside them, with a gap either side, or a gap alone with neither. The
|
|
36
|
+
* axis words give way when they would leave the plot under
|
|
37
|
+
* `chartMinPlotWidth`; the last value and the hidden table still say it.
|
|
38
|
+
*/
|
|
39
|
+
export declare function endGutter({ width, left, axisWords, endLabel, }: {
|
|
40
|
+
width: number;
|
|
41
|
+
left: number;
|
|
42
|
+
axisWords: readonly string[];
|
|
43
|
+
endLabel: string | null;
|
|
44
|
+
}): {
|
|
45
|
+
gutter: number;
|
|
46
|
+
axisWordsShown: boolean;
|
|
47
|
+
};
|
|
48
|
+
/**
|
|
49
|
+
* The boxed period around a span of the plot, `inset` inside it (a
|
|
50
|
+
* category's band takes `chartInset`; a period of days takes none) and
|
|
51
|
+
* past the plot's top and bottom; never narrower than a hairline, even on
|
|
52
|
+
* a chart so dense the span is thinner than its inset.
|
|
53
|
+
*/
|
|
54
|
+
export declare function periodBox(start: number, band: number, plot: ChartPlot, inset?: number): {
|
|
55
|
+
x: number;
|
|
56
|
+
y: number;
|
|
57
|
+
width: number;
|
|
58
|
+
height: number;
|
|
59
|
+
};
|
|
60
|
+
/** A label's centre, moved in so its words stay inside the chart's width. */
|
|
61
|
+
export declare function labelInside(x: number, text: string, width: number): number;
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { chartHairline } from './css/chart-marks.js';
|
|
2
|
+
import { chartGap, chartInset, chartMinPlotWidth, chartTextWidth, chartType, } from './chart-metrics.js';
|
|
3
|
+
import { scaleShare } from './chart-scale.js';
|
|
4
|
+
/** The height a value falls at in a plot, on a scale. */
|
|
5
|
+
export function valueY(value, scale, plot) {
|
|
6
|
+
return plot.bottom - scaleShare(value, scale) * (plot.bottom - plot.top);
|
|
7
|
+
}
|
|
8
|
+
/** A scale's ticks, placed in a plot and written by `format`. */
|
|
9
|
+
export function valueTicks(scale, plot, format) {
|
|
10
|
+
return scale.ticks.map((value) => ({
|
|
11
|
+
value,
|
|
12
|
+
y: valueY(value, scale, plot),
|
|
13
|
+
label: format(value),
|
|
14
|
+
}));
|
|
15
|
+
}
|
|
16
|
+
/** The room a column of labels takes beside the plot, with its gap. */
|
|
17
|
+
export function labelGutter(labels) {
|
|
18
|
+
let widest = 0;
|
|
19
|
+
for (const label of labels)
|
|
20
|
+
widest = Math.max(widest, chartTextWidth(label));
|
|
21
|
+
return Math.ceil(widest) + chartGap;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Which of `count` evenly spaced places, `slot` pixels apart, get their
|
|
25
|
+
* label: every one when the widest label and a gap either side fit a
|
|
26
|
+
* slot, otherwise every second, third and so on, counted back from the
|
|
27
|
+
* last, so the latest place is always labelled.
|
|
28
|
+
*/
|
|
29
|
+
export function labelledPlaces(labels, slot) {
|
|
30
|
+
if (labels.length === 0)
|
|
31
|
+
return [];
|
|
32
|
+
let widest = 0;
|
|
33
|
+
for (const label of labels)
|
|
34
|
+
widest = Math.max(widest, chartTextWidth(label));
|
|
35
|
+
const every = Math.max(1, Math.ceil((widest + chartGap * 2) / slot));
|
|
36
|
+
const places = [];
|
|
37
|
+
for (let index = labels.length - 1; index >= 0; index -= every)
|
|
38
|
+
places.unshift(index);
|
|
39
|
+
return places;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Drops the ticks whose words would meet a label drawn at `y` in the same
|
|
43
|
+
* column (such as a line's last value beside its axis).
|
|
44
|
+
*/
|
|
45
|
+
export function ticksClearOf(ticks, y) {
|
|
46
|
+
return ticks.filter((tick) => Math.abs(tick.y - y) >= chartType.lineHeight);
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* The room past a plot's end: a line's own axis words and its last value
|
|
50
|
+
* beside them, with a gap either side, or a gap alone with neither. The
|
|
51
|
+
* axis words give way when they would leave the plot under
|
|
52
|
+
* `chartMinPlotWidth`; the last value and the hidden table still say it.
|
|
53
|
+
*/
|
|
54
|
+
export function endGutter({ width, left, axisWords, endLabel, }) {
|
|
55
|
+
const endRoom = endLabel ? Math.ceil(chartTextWidth(endLabel)) + chartGap : 0;
|
|
56
|
+
const axisRoom = axisWords.length > 0 ? labelGutter(axisWords) : 0;
|
|
57
|
+
const axisWordsShown = axisRoom > 0 &&
|
|
58
|
+
width - left - chartGap - Math.max(axisRoom, endRoom) >= chartMinPlotWidth;
|
|
59
|
+
return {
|
|
60
|
+
gutter: chartGap + Math.max(axisWordsShown ? axisRoom : 0, endRoom),
|
|
61
|
+
axisWordsShown,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* The boxed period around a span of the plot, `inset` inside it (a
|
|
66
|
+
* category's band takes `chartInset`; a period of days takes none) and
|
|
67
|
+
* past the plot's top and bottom; never narrower than a hairline, even on
|
|
68
|
+
* a chart so dense the span is thinner than its inset.
|
|
69
|
+
*/
|
|
70
|
+
export function periodBox(start, band, plot, inset = chartInset) {
|
|
71
|
+
const width = Math.max(band - inset, chartHairline);
|
|
72
|
+
return {
|
|
73
|
+
x: start + (band - width) / 2,
|
|
74
|
+
y: plot.top - chartGap,
|
|
75
|
+
width,
|
|
76
|
+
height: plot.bottom - plot.top + chartGap + chartInset,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
/** A label's centre, moved in so its words stay inside the chart's width. */
|
|
80
|
+
export function labelInside(x, text, width) {
|
|
81
|
+
const half = chartTextWidth(text) / 2;
|
|
82
|
+
return Math.min(Math.max(x, half), width - half);
|
|
83
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { parseDateOnly } from './date-only.js';
|
|
2
|
+
const dayMs = 24 * 60 * 60 * 1000;
|
|
3
|
+
/**
|
|
4
|
+
* A `YYYY-MM-DD` date as a whole day count (days since 1970-01-01), so a
|
|
5
|
+
* time axis places dates by the days between them; `null` for anything
|
|
6
|
+
* that is not a real date.
|
|
7
|
+
*/
|
|
8
|
+
export function dayNumber(value) {
|
|
9
|
+
const parts = parseDateOnly(value);
|
|
10
|
+
if (!parts)
|
|
11
|
+
return null;
|
|
12
|
+
const date = new Date(0);
|
|
13
|
+
date.setUTCFullYear(parts.year, parts.month - 1, parts.day);
|
|
14
|
+
return Math.round(date.getTime() / dayMs);
|
|
15
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/** The type a chart's words are drawn in: the caption variant. */
|
|
2
|
+
export declare const chartType: {
|
|
3
|
+
readonly fontSize: 13;
|
|
4
|
+
readonly lineHeight: 18;
|
|
5
|
+
readonly fontWeight: 400;
|
|
6
|
+
readonly letterSpacing: -0.08;
|
|
7
|
+
};
|
|
8
|
+
/** The gap between a mark and its words, and around the plot. */
|
|
9
|
+
export declare const chartGap: 8;
|
|
10
|
+
/** A small inset: a leader's distance from its column, a box's overhang. */
|
|
11
|
+
export declare const chartInset: 4;
|
|
12
|
+
/**
|
|
13
|
+
* Below this width a chart is compact: its callouts move under the plot as
|
|
14
|
+
* a list, its plot is shorter, and its axes label fewer places. A phone
|
|
15
|
+
* column (a 390 px screen less its margins) is under it; a tablet's is not.
|
|
16
|
+
*/
|
|
17
|
+
export declare const chartCompactWidth: number;
|
|
18
|
+
/**
|
|
19
|
+
* The narrowest plot a chart keeps: under it, a line's own axis gives up
|
|
20
|
+
* its words (the line's last value and the hidden table still say it).
|
|
21
|
+
*/
|
|
22
|
+
export declare const chartMinPlotWidth: number;
|
|
23
|
+
/** The width a chart is drawn at before it has been measured. */
|
|
24
|
+
export declare const chartFallbackWidth: number;
|
|
25
|
+
/** The plot's height: the area the values are drawn in. */
|
|
26
|
+
export declare function chartPlotHeight(compact: boolean): number;
|
|
27
|
+
/** A column's corners, and the boxed period's. */
|
|
28
|
+
export declare const chartColumnRadius: number;
|
|
29
|
+
export declare const chartBoxRadius: number;
|
|
30
|
+
/** A marker's downward triangle: as wide and tall as a legend's marker swatch. */
|
|
31
|
+
export declare const chartMarkerSize: 12;
|
|
32
|
+
/** The dot at a line's last value. */
|
|
33
|
+
export declare const chartDotRadius: number;
|
|
34
|
+
/**
|
|
35
|
+
* About how wide a run of the chart's words is: its characters at 0.6 of
|
|
36
|
+
* the font size, the width of a tabular digit in the system sans. Used to
|
|
37
|
+
* leave room for axis labels and to keep labels from running into one
|
|
38
|
+
* another; the words are short figures and dates.
|
|
39
|
+
*/
|
|
40
|
+
export declare function chartTextWidth(text: string): number;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { radiusScale, spacingScale, typographyVariants, } from '@scalewing/tokens';
|
|
2
|
+
import { chartLineWidth } from './css/chart-marks.js';
|
|
3
|
+
/*
|
|
4
|
+
* The measures a chart's drawing is laid out with, all from tokens: the
|
|
5
|
+
* caption type its words are set in, spacing steps for its gaps, and the
|
|
6
|
+
* radius scale for its corners. The drawing is SVG in the chart's own
|
|
7
|
+
* pixels, so it is laid out for the width it is given (D-39: component
|
|
8
|
+
* geometry, not theme math).
|
|
9
|
+
*/
|
|
10
|
+
/** The type a chart's words are drawn in: the caption variant. */
|
|
11
|
+
export const chartType = typographyVariants.caption;
|
|
12
|
+
/** The gap between a mark and its words, and around the plot. */
|
|
13
|
+
export const chartGap = spacingScale[2];
|
|
14
|
+
/** A small inset: a leader's distance from its column, a box's overhang. */
|
|
15
|
+
export const chartInset = spacingScale[1];
|
|
16
|
+
/**
|
|
17
|
+
* Below this width a chart is compact: its callouts move under the plot as
|
|
18
|
+
* a list, its plot is shorter, and its axes label fewer places. A phone
|
|
19
|
+
* column (a 390 px screen less its margins) is under it; a tablet's is not.
|
|
20
|
+
*/
|
|
21
|
+
export const chartCompactWidth = spacingScale[8] * 10;
|
|
22
|
+
/**
|
|
23
|
+
* The narrowest plot a chart keeps: under it, a line's own axis gives up
|
|
24
|
+
* its words (the line's last value and the hidden table still say it).
|
|
25
|
+
*/
|
|
26
|
+
export const chartMinPlotWidth = spacingScale[8] * 2;
|
|
27
|
+
/** The width a chart is drawn at before it has been measured. */
|
|
28
|
+
export const chartFallbackWidth = spacingScale[8] * 13;
|
|
29
|
+
/** The plot's height: the area the values are drawn in. */
|
|
30
|
+
export function chartPlotHeight(compact) {
|
|
31
|
+
return compact ? spacingScale[6] * 5 : spacingScale[8] * 4;
|
|
32
|
+
}
|
|
33
|
+
/** A column's corners, and the boxed period's. */
|
|
34
|
+
export const chartColumnRadius = radiusScale.sm / 4;
|
|
35
|
+
export const chartBoxRadius = radiusScale.sm / 2;
|
|
36
|
+
/** A marker's downward triangle: as wide and tall as a legend's marker swatch. */
|
|
37
|
+
export const chartMarkerSize = spacingScale[3];
|
|
38
|
+
/** The dot at a line's last value. */
|
|
39
|
+
export const chartDotRadius = chartLineWidth * 1.75;
|
|
40
|
+
/**
|
|
41
|
+
* About how wide a run of the chart's words is: its characters at 0.6 of
|
|
42
|
+
* the font size, the width of a tabular digit in the system sans. Used to
|
|
43
|
+
* leave room for axis labels and to keep labels from running into one
|
|
44
|
+
* another; the words are short figures and dates.
|
|
45
|
+
*/
|
|
46
|
+
export function chartTextWidth(text) {
|
|
47
|
+
return text.length * chartType.fontSize * 0.6;
|
|
48
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A value axis: its ends and the values its grid lines and labels mark,
|
|
3
|
+
* from `min` to `max` a whole `step` apart, always through zero.
|
|
4
|
+
*/
|
|
5
|
+
export type ChartScale = {
|
|
6
|
+
min: number;
|
|
7
|
+
max: number;
|
|
8
|
+
step: number;
|
|
9
|
+
ticks: readonly number[];
|
|
10
|
+
};
|
|
11
|
+
/** How many steps a value axis aims for between its ends. */
|
|
12
|
+
export declare const chartScaleSteps = 4;
|
|
13
|
+
/**
|
|
14
|
+
* The smallest round step (1, 2, 2.5 or 5 times a power of ten) that
|
|
15
|
+
* covers `span` in at most `steps` steps.
|
|
16
|
+
*/
|
|
17
|
+
export declare function niceStep(span: number, steps: number): number;
|
|
18
|
+
/** The least and greatest of some values, both stretched to reach zero. */
|
|
19
|
+
export declare function extentThroughZero(values: Iterable<number>): {
|
|
20
|
+
min: number;
|
|
21
|
+
max: number;
|
|
22
|
+
};
|
|
23
|
+
/**
|
|
24
|
+
* A value axis for an extent through zero: a round step, and as many whole
|
|
25
|
+
* steps either side of zero as the extent needs. An extent of only zero
|
|
26
|
+
* reads 0 to 1.
|
|
27
|
+
*/
|
|
28
|
+
export declare function chartScale(extent: {
|
|
29
|
+
min: number;
|
|
30
|
+
max: number;
|
|
31
|
+
}): ChartScale;
|
|
32
|
+
/**
|
|
33
|
+
* Two value axes drawn on one plot, such as columns on the left and a
|
|
34
|
+
* running total on the right: each keeps its own round step, and both have
|
|
35
|
+
* the same number of steps below and above zero, so zero and every grid
|
|
36
|
+
* line fall at the same height on both. When the two lean different ways
|
|
37
|
+
* (one mostly above zero, the other below), both take fewer, longer steps
|
|
38
|
+
* so the plot keeps about as many grid lines as one scale would.
|
|
39
|
+
*/
|
|
40
|
+
export declare function alignedChartScales(first: {
|
|
41
|
+
min: number;
|
|
42
|
+
max: number;
|
|
43
|
+
}, second: {
|
|
44
|
+
min: number;
|
|
45
|
+
max: number;
|
|
46
|
+
}): [ChartScale, ChartScale];
|
|
47
|
+
/** Where a value falls on a scale, as a share from its bottom (0) to top (1). */
|
|
48
|
+
export declare function scaleShare(value: number, scale: ChartScale): number;
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/** How many steps a value axis aims for between its ends. */
|
|
2
|
+
export const chartScaleSteps = 4;
|
|
3
|
+
/** The multipliers a step takes within each power of ten. */
|
|
4
|
+
const niceMultipliers = [1, 2, 2.5, 5, 10];
|
|
5
|
+
/**
|
|
6
|
+
* The smallest round step (1, 2, 2.5 or 5 times a power of ten) that
|
|
7
|
+
* covers `span` in at most `steps` steps.
|
|
8
|
+
*/
|
|
9
|
+
export function niceStep(span, steps) {
|
|
10
|
+
if (!(span > 0))
|
|
11
|
+
return 1;
|
|
12
|
+
const raw = span / steps;
|
|
13
|
+
const power = 10 ** Math.floor(Math.log10(raw));
|
|
14
|
+
for (const multiplier of niceMultipliers) {
|
|
15
|
+
const step = multiplier * power;
|
|
16
|
+
if (step >= raw * (1 - 1e-9))
|
|
17
|
+
return step;
|
|
18
|
+
}
|
|
19
|
+
return 10 * power;
|
|
20
|
+
}
|
|
21
|
+
/** The least and greatest of some values, both stretched to reach zero. */
|
|
22
|
+
export function extentThroughZero(values) {
|
|
23
|
+
let min = 0;
|
|
24
|
+
let max = 0;
|
|
25
|
+
for (const value of values) {
|
|
26
|
+
if (value < min)
|
|
27
|
+
min = value;
|
|
28
|
+
if (value > max)
|
|
29
|
+
max = value;
|
|
30
|
+
}
|
|
31
|
+
return { min, max };
|
|
32
|
+
}
|
|
33
|
+
/** Rounds away the floating-point dust of a step multiple. */
|
|
34
|
+
function tidy(value, step) {
|
|
35
|
+
// toFixed takes at most 100 digits.
|
|
36
|
+
const decimals = Math.min(100, Math.max(0, -Math.floor(Math.log10(step)) + 2));
|
|
37
|
+
return Number(value.toFixed(decimals)) + 0;
|
|
38
|
+
}
|
|
39
|
+
function scaleFrom(step, below, above) {
|
|
40
|
+
const ticks = [];
|
|
41
|
+
for (let index = -below; index <= above; index += 1)
|
|
42
|
+
ticks.push(tidy(index * step, step));
|
|
43
|
+
return {
|
|
44
|
+
min: tidy(-below * step, step),
|
|
45
|
+
max: tidy(above * step, step),
|
|
46
|
+
step,
|
|
47
|
+
ticks,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
/** Whole steps below and above zero for an extent at one step. */
|
|
51
|
+
function stepsAround(extent, step) {
|
|
52
|
+
return {
|
|
53
|
+
below: Math.ceil(-extent.min / step - 1e-9),
|
|
54
|
+
above: Math.ceil(extent.max / step - 1e-9),
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
/** A scale for an extent aiming at a number of steps. */
|
|
58
|
+
function scaleAiming(extent, steps) {
|
|
59
|
+
const span = extent.max - extent.min;
|
|
60
|
+
if (span === 0)
|
|
61
|
+
return scaleFrom(1, 0, 1);
|
|
62
|
+
let step = niceStep(span, steps);
|
|
63
|
+
let { below, above } = stepsAround(extent, step);
|
|
64
|
+
// Rounding each side up can add a step; widen the step once if so.
|
|
65
|
+
if (below + above > steps + 1 && steps > 1) {
|
|
66
|
+
step = niceStep(span, steps - 1);
|
|
67
|
+
({ below, above } = stepsAround(extent, step));
|
|
68
|
+
}
|
|
69
|
+
return scaleFrom(step, Math.max(below, 0), Math.max(above, 0));
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* A value axis for an extent through zero: a round step, and as many whole
|
|
73
|
+
* steps either side of zero as the extent needs. An extent of only zero
|
|
74
|
+
* reads 0 to 1.
|
|
75
|
+
*/
|
|
76
|
+
export function chartScale(extent) {
|
|
77
|
+
return scaleAiming(extent, chartScaleSteps);
|
|
78
|
+
}
|
|
79
|
+
/** Whole steps a scale has below and above zero. */
|
|
80
|
+
function sides(scale) {
|
|
81
|
+
return {
|
|
82
|
+
below: Math.round(-scale.min / scale.step),
|
|
83
|
+
above: Math.round(scale.max / scale.step),
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Two value axes drawn on one plot, such as columns on the left and a
|
|
88
|
+
* running total on the right: each keeps its own round step, and both have
|
|
89
|
+
* the same number of steps below and above zero, so zero and every grid
|
|
90
|
+
* line fall at the same height on both. When the two lean different ways
|
|
91
|
+
* (one mostly above zero, the other below), both take fewer, longer steps
|
|
92
|
+
* so the plot keeps about as many grid lines as one scale would.
|
|
93
|
+
*/
|
|
94
|
+
export function alignedChartScales(first, second) {
|
|
95
|
+
let aligned = [
|
|
96
|
+
chartScale(first),
|
|
97
|
+
chartScale(second),
|
|
98
|
+
];
|
|
99
|
+
for (let steps = chartScaleSteps; steps >= 1; steps -= 1) {
|
|
100
|
+
const a = scaleAiming(first, steps);
|
|
101
|
+
const b = scaleAiming(second, steps);
|
|
102
|
+
const below = Math.max(sides(a).below, sides(b).below);
|
|
103
|
+
const above = Math.max(sides(a).above, sides(b).above);
|
|
104
|
+
aligned = [
|
|
105
|
+
scaleFrom(a.step, below, above),
|
|
106
|
+
scaleFrom(b.step, below, above),
|
|
107
|
+
];
|
|
108
|
+
if (below + above <= chartScaleSteps + 1)
|
|
109
|
+
break;
|
|
110
|
+
}
|
|
111
|
+
return aligned;
|
|
112
|
+
}
|
|
113
|
+
/** Where a value falls on a scale, as a share from its bottom (0) to top (1). */
|
|
114
|
+
export function scaleShare(value, scale) {
|
|
115
|
+
return (value - scale.min) / (scale.max - scale.min);
|
|
116
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|