@scalewing/react 1.38.0 → 1.40.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 +155 -1
- package/dist/calendar-month.d.ts +6 -0
- package/dist/calendar-month.js +15 -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 +6 -6
- package/dist/components/DateRangeButton.d.ts +65 -0
- package/dist/components/DateRangeButton.js +54 -0
- package/dist/components/LineChart.d.ts +83 -0
- package/dist/components/LineChart.js +77 -0
- package/dist/components/ListGroup.d.ts +33 -0
- package/dist/components/ListGroup.js +19 -0
- package/dist/components/ListRow.d.ts +51 -0
- package/dist/components/ListRow.js +59 -0
- package/dist/components/NavCount.d.ts +21 -0
- package/dist/components/NavCount.js +23 -0
- package/dist/components/SectionNav.js +1 -8
- package/dist/components/StatTile.d.ts +11 -4
- package/dist/components/StatTile.js +11 -4
- package/dist/components/{date-field → calendar}/CalendarGrid.d.ts +17 -2
- package/dist/components/{date-field → calendar}/CalendarGrid.js +11 -3
- package/dist/components/calendar/CalendarPopover.d.ts +25 -0
- package/dist/components/calendar/CalendarPopover.js +22 -0
- package/dist/components/calendar/MonthStep.d.ts +16 -0
- package/dist/components/calendar/MonthStep.js +17 -0
- package/dist/components/calendar/use-grid-focus.d.ts +18 -0
- package/dist/components/calendar/use-grid-focus.js +35 -0
- package/dist/components/calendar/wrap-tab.d.ts +7 -0
- package/dist/components/calendar/wrap-tab.js +29 -0
- 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/date-field/CalendarDialog.js +9 -67
- package/dist/components/date-field/CalendarHeader.js +2 -11
- package/dist/components/date-range/DateRangePopover.d.ts +7 -0
- package/dist/components/date-range/DateRangePopover.js +17 -0
- package/dist/components/date-range/DateRangeSheet.d.ts +7 -0
- package/dist/components/date-range/DateRangeSheet.js +15 -0
- package/dist/components/date-range/RangeMonths.d.ts +28 -0
- package/dist/components/date-range/RangeMonths.js +50 -0
- package/dist/components/date-range/RangePresets.d.ts +16 -0
- package/dist/components/date-range/RangePresets.js +10 -0
- package/dist/components/date-range/RangeSummary.d.ts +15 -0
- package/dist/components/date-range/RangeSummary.js +11 -0
- package/dist/components/date-range/range-panel-props.d.ts +31 -0
- package/dist/components/date-range/range-panel-props.js +1 -0
- package/dist/components/date-range/use-range-popup.d.ts +21 -0
- package/dist/components/date-range/use-range-popup.js +53 -0
- package/dist/components/line-chart/LineChartDrawing.d.ts +17 -0
- package/dist/components/line-chart/LineChartDrawing.js +16 -0
- package/dist/components/link-press.d.ts +7 -0
- package/dist/components/link-press.js +13 -0
- package/dist/components/list/ListRowAccessoryMark.d.ts +5 -0
- package/dist/components/list/ListRowAccessoryMark.js +7 -0
- package/dist/components/list/ListRowContent.d.ts +20 -0
- package/dist/components/list/ListRowContent.js +10 -0
- package/dist/components/list/list-row-container.d.ts +5 -0
- package/dist/components/list/list-row-container.js +6 -0
- package/dist/components/use-media-query.d.ts +5 -0
- package/dist/components/use-media-query.js +18 -0
- 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 -0
- package/dist/css/chevron.js +14 -0
- 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-button-group-attached.js +8 -4
- package/dist/css/css-button-group-track.js +1 -1
- 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-checkbox.js +2 -2
- package/dist/css/css-components.js +23 -2
- package/dist/css/css-count-pill.d.ts +2 -0
- package/dist/css/css-count-pill.js +54 -0
- package/dist/css/css-data-bar.d.ts +4 -9
- package/dist/css/css-data-bar.js +15 -31
- package/dist/css/css-date-field.js +3 -2
- package/dist/css/css-date-range-presets.d.ts +2 -0
- package/dist/css/css-date-range-presets.js +109 -0
- package/dist/css/css-date-range.d.ts +2 -0
- package/dist/css/css-date-range.js +197 -0
- package/dist/css/css-line-chart.d.ts +2 -0
- package/dist/css/css-line-chart.js +31 -0
- package/dist/css/css-list-row.d.ts +2 -0
- package/dist/css/css-list-row.js +193 -0
- package/dist/css/css-list.d.ts +3 -0
- package/dist/css/css-list.js +85 -0
- package/dist/css/css-scroll-region.d.ts +18 -0
- package/dist/css/css-scroll-region.js +41 -0
- package/dist/css/css-section-nav-filled.js +2 -25
- package/dist/css/css-stat-tile.d.ts +7 -0
- package/dist/css/css-stat-tile.js +16 -0
- package/dist/css/css-steps.js +2 -2
- package/dist/css/css-table-bounded-edges.js +4 -27
- package/dist/css/range-band.d.ts +2 -0
- package/dist/css/range-band.js +7 -0
- package/dist/css/stacking.js +2 -2
- package/dist/css/stylesheet.js +16 -2
- package/dist/date-range-draft.d.ts +26 -0
- package/dist/date-range-draft.js +36 -0
- package/dist/date-range-input.d.ts +26 -0
- package/dist/date-range-input.js +58 -0
- package/dist/date-range-labels.d.ts +22 -0
- package/dist/date-range-labels.js +26 -0
- package/dist/date-range-view.d.ts +12 -0
- package/dist/date-range-view.js +29 -0
- package/dist/date-range-words.d.ts +11 -0
- package/dist/date-range-words.js +40 -0
- package/dist/index.d.ts +9 -1
- package/dist/index.js +9 -1
- 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/list-options.d.ts +70 -0
- package/dist/list-options.js +77 -0
- package/dist/styles.css +726 -26
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,23 @@
|
|
|
1
1
|
# @scalewing/react
|
|
2
2
|
|
|
3
|
+
## 1.40.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 935ff2a: `DateRangeButton`: a button showing a range of days (`value` `{ start, end }`, `YYYY-MM-DD`) that opens a calendar to pick a first and a last day. From `md` up it is a popover with the `presets` (`DateRangePreset`: `id`, `label`, `range`; a press applies it at once) beside two months (above them below `lg`), the range being picked in a polite status, and Cancel and Apply; below `md` it is a Dialog bottom sheet with the presets in a row and one month. The first press sets the start and the second the end; days after `max` are disabled, and once a start is pressed so are the days past `maxDays`. Apply calls `onChange` with a changed range; Cancel, Escape, a press outside and the sheet's close keep the value. Its name is `label`, `labels.nameSeparator` and the range it shows. `labels` (`DateRangeButtonLabels`) holds every word, `dayCount` included; `min`, `today`, `weekStartsOn`, `locale`, `disabled`, `id`, `size` and `variant` (default `neutral`) as on CalendarButton. Input it cannot draw throws a `RangeError`. New exports `DateRangeButton`, `DateRangeButtonProps`, `DateRangeButtonValue`, `DateRangePreset`, `DateRangeButtonLabels`; new `sw-date-range-*` classes. It shares CalendarButton's private calendar grid, popover and month steps, whose public behaviour is unchanged. `ButtonGroup attached` now finds its buttons by type, so an open calendar beside one keeps the group's dividers and corners. No new tokens, no new dependency. Request: `docs/requests/teisoro-date-range-button.md`.
|
|
8
|
+
- 3192e17: Web `ListGroup` and `ListRow`, the counterpart of react-native's. `ListGroup` is a list (`ul`, role kept) of rows with hairlines between them, a bordered `panel` or `plain` rows inside a surface, named by `aria-label` or `aria-labelledby`; `maxHeight` (`{ rows }`) bounds it to rows of a title and one detail line that scroll inside a named, focusable region shaded at an edge with more past it, and `footer` stays pinned under the rows, such as a total. `ListRow` has a `title` (`titleAppearance` `strong` or `plain`, `tone` `danger`), a muted `detail`, a `value` with `valueDetail` and `valueTone` (a table cell's tones) and a hidden `leading` mark; `onPress` or `href` makes the whole row a button or link with a chevron, named by its title and value and described by its details (`href` with `onPress` takes a plain press for a client router); `selected` makes it a choice with a check (`aria-pressed` or `aria-current`); `accessory="none"` hides the chevron; `disabled` blocks presses. Input it cannot draw throws a `RangeError`. New exports `ListGroup`, `ListGroupProps`, `ListRow`, `ListRowProps`, `listGroupVariants`, `listRowTones`, `listRowTitleAppearances`, `listRowValueTones`, `ListGroupMaxHeight`, `ListGroupVariant`, `ListRowTone`, `ListRowTitleAppearance`, `ListRowValueTone`; new `sw-list*` classes; no new tokens, no new dependency. Request: `docs/requests/teisoro-list-group.md`.
|
|
9
|
+
- 1d3fafd: `NavCount`: a count after a Nav destination's words, placed inside its link or Button (`<a href="/tasks">Tasks<NavCount count={8} /></a>`), such as the tasks waiting in an area. It is SectionNav's count pill, now one shared rule, and it is read in the destination's name ("Tasks 8"); `countLabel` is read in place of the bare number ("Tasks 8 tasks to do") with the pill hidden from assistive technology; a `count` of 0 shows nothing. A count that is not a whole number from 0 or a blank `countLabel` throws a `RangeError`. New exports `NavCount`, `NavCountProps`; new class `sw-nav-count`; no new tokens, no new dependency. Request: `docs/requests/teisoro-nav-count.md`.
|
|
10
|
+
- 62c33b1: `StatTile` `appearance="plain"` (`StatTileAppearance`, `statTileAppearances`; default `card`): the figure without the tile's surface, edge, radius or padding, top-aligned, for figures inside a card, such as four answers in a row divided by a composed `Separator`. A plain tile cannot be `emphasis="primary"`; that or an unknown appearance throws a `RangeError`. New class `sw-stat-tile-plain`; no new tokens, no new dependency. Request: `docs/requests/teisoro-stat-tile-plain.md`.
|
|
11
|
+
|
|
12
|
+
## 1.39.0
|
|
13
|
+
|
|
14
|
+
### Minor Changes
|
|
15
|
+
|
|
16
|
+
- 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`.
|
|
17
|
+
- 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`.
|
|
18
|
+
- 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`.
|
|
19
|
+
- 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`.
|
|
20
|
+
|
|
3
21
|
## 1.38.0
|
|
4
22
|
|
|
5
23
|
### Minor Changes
|
package/README.md
CHANGED
|
@@ -10,10 +10,14 @@ import {
|
|
|
10
10
|
ActionBar,
|
|
11
11
|
ActionMenu,
|
|
12
12
|
BarChart,
|
|
13
|
+
ChartLegend,
|
|
14
|
+
ColumnChart,
|
|
15
|
+
LineChart,
|
|
13
16
|
DataBar,
|
|
14
17
|
Box,
|
|
15
18
|
Button,
|
|
16
19
|
CalendarButton,
|
|
20
|
+
DateRangeButton,
|
|
17
21
|
Card,
|
|
18
22
|
Checkbox,
|
|
19
23
|
RadioGroup,
|
|
@@ -26,6 +30,8 @@ import {
|
|
|
26
30
|
Separator,
|
|
27
31
|
DescriptionList,
|
|
28
32
|
DescriptionItem,
|
|
33
|
+
ListGroup,
|
|
34
|
+
ListRow,
|
|
29
35
|
DenominationGrid,
|
|
30
36
|
FilterChips,
|
|
31
37
|
Grid,
|
|
@@ -372,6 +378,20 @@ An option's optional `description` (a `ReactNode`) is secondary text for that op
|
|
|
372
378
|
|
|
373
379
|
`appearance="words"` (opt-in; default `"default"`, `navAppearances`) draws every destination, a link or a `Button` of any variant, as plain words: no fill, border or underline, label type, the others in the muted color at medium weight and the current one (`aria-current`, not `"false"`) in the text color at label weight. A Button keeps its size, padding, touch target and focus ring; hover lifts a destination to the text color where the pointer can hover, never a disabled one. The current mark is color and weight, so each destination's words must be visible: a glyph-only destination has no weight to change (keep `default` for a glyph rail). Forced colors draw the current one's border in Highlight and underline its words. An unknown value throws a `RangeError`.
|
|
374
380
|
|
|
381
|
+
`NavCount` puts a count after a destination's words, such as the tasks waiting in an area: place it inside the link or `Button`, after its words (`<a href="/tasks">Tasks<NavCount count={8} /></a>`). It is the pill `SectionNav`'s item `count` draws (the danger color with its on-color figures, an edge in forced colors), a space-2 after the words in a link or a Button, the link keeping its siblings' height, and it is read as part of the destination's name ("Tasks 8"). `count` is a whole number; 0 shows nothing. `countLabel` says what it means in your words ("8 tasks to do"): it is read in place of the bare number ("Tasks 8 tasks to do") and the pill is hidden from assistive technology. A count that is not a whole number from 0, or a blank `countLabel`, throws a `RangeError`.
|
|
382
|
+
|
|
383
|
+
```tsx
|
|
384
|
+
<Nav appearance="words" aria-label="Areas">
|
|
385
|
+
<Box as="a" href="/sightings">
|
|
386
|
+
Sightings
|
|
387
|
+
</Box>
|
|
388
|
+
<Box as="a" href="/tasks" aria-current="page">
|
|
389
|
+
Tasks
|
|
390
|
+
<NavCount count={8} countLabel="8 tasks to do" />
|
|
391
|
+
</Box>
|
|
392
|
+
</Nav>
|
|
393
|
+
```
|
|
394
|
+
|
|
375
395
|
`SectionNav` is the navigation between the sections of one area, such as a portal's pages: a labelled `nav` (`aria-label` or `aria-labelledby`) of links from `items` (`{ id, label, href, current?, icon?, count? }`), quieter than a workspace's `Nav`. `current` is `'page'`, `'location'` (a page inside that section, such as a detail page under its list) or a boolean, written as `aria-current`; the current item is in the text color over an accent underline, or, from `verticalFrom` (a breakpoint) up, where the items stack as a side list, a bar at its start. `icon` is a decorative glyph before the label. `onNavigate` takes a plain press after preventing the browser's navigation, for a client router; a press with a modifier key or the middle button keeps the browser's behavior, and without `onNavigate` the links carry no handler, so a server component can render the nav. `count` (a whole number) is how many things wait in a section, such as tasks: a filled pill after the label, in the danger color with its on-color figures, read as part of the link's name ("Tasks 3"); 0 shows none, and there is no cap. `countLabel` says what the count means in your words ("3 tasks to do"): it is read in place of the bare number and the pill is hidden from assistive technology. `appearance="filled"` (default `"underline"`) is a quiet row of page buttons for an area's sub-navigation beside its name: no hairline and no underline or start bar, every link the `sm` control with the small radius at weight 500, the current one on a soft muted fill in the text color at 600 (a 2 px `Highlight` edge in forced colors), in the row or the side list. A coarse pointer gets 44 px targets. Empty or duplicate ids, empty labels, a `count` that is not a whole number from 0, a blank `countLabel` or one without a `count`, and an unknown `verticalFrom` or `appearance` throw a `RangeError`. Use `Tabs` for panels on one page.
|
|
376
396
|
|
|
377
397
|
```tsx
|
|
@@ -444,7 +464,7 @@ A ledger read across as well as down takes `Table` `columnRules` (a hairline at
|
|
|
444
464
|
|
|
445
465
|
`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.
|
|
446
466
|
|
|
447
|
-
`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 (
|
|
467
|
+
`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.
|
|
448
468
|
|
|
449
469
|
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`.
|
|
450
470
|
|
|
@@ -457,6 +477,55 @@ A bar in its own column reads as empty cells to assistive technology, which is f
|
|
|
457
477
|
</TableCell>
|
|
458
478
|
```
|
|
459
479
|
|
|
480
|
+
`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.
|
|
481
|
+
|
|
482
|
+
```tsx
|
|
483
|
+
<ChartLegend
|
|
484
|
+
aria-label="Key"
|
|
485
|
+
items={[
|
|
486
|
+
{ label: 'Herons', swatch: 'bar', tone: 'blue' },
|
|
487
|
+
{ label: 'Running total', swatch: 'line', tone: 'accent' },
|
|
488
|
+
{ label: 'Flood', swatch: 'marker', tone: 'danger' },
|
|
489
|
+
{ label: 'This week', swatch: 'box', tone: 'accent' },
|
|
490
|
+
]}
|
|
491
|
+
/>
|
|
492
|
+
```
|
|
493
|
+
|
|
494
|
+
`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`.
|
|
495
|
+
|
|
496
|
+
```tsx
|
|
497
|
+
<ColumnChart
|
|
498
|
+
label="Change in birds, last 13 weeks"
|
|
499
|
+
categories={weeks}
|
|
500
|
+
series={[
|
|
501
|
+
{ id: 'herons', label: 'Herons', tone: 'blue', values: herons },
|
|
502
|
+
{ id: 'egrets', label: 'Egrets', tone: 'purple', values: egrets },
|
|
503
|
+
]}
|
|
504
|
+
line={{ label: 'Running total', values: runningTotal }}
|
|
505
|
+
highlight={weeks.length - 1}
|
|
506
|
+
callouts={[{ index: 3, text: 'Aug 4 · Roost found +50', tone: 'purple' }]}
|
|
507
|
+
formatValue={signed}
|
|
508
|
+
/>
|
|
509
|
+
```
|
|
510
|
+
|
|
511
|
+
`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`.
|
|
512
|
+
|
|
513
|
+
```tsx
|
|
514
|
+
<LineChart
|
|
515
|
+
label="Birds roosting each night"
|
|
516
|
+
valueLabel="Roosting"
|
|
517
|
+
points={nights}
|
|
518
|
+
area
|
|
519
|
+
markers={[{ x: '2026-08-12', label: '2600', tone: 'warning' }]}
|
|
520
|
+
markersLabel="A flock left"
|
|
521
|
+
ticks={['2026-07-27', '2026-08-17', '2026-10-12']}
|
|
522
|
+
highlight={{ from: '2026-10-06', to: '2026-10-12' }}
|
|
523
|
+
formatDate={shortDate}
|
|
524
|
+
/>
|
|
525
|
+
|
|
526
|
+
<LineChart label="Nest boxes lent" curve="step" tone="warning" points={loans} />
|
|
527
|
+
```
|
|
528
|
+
|
|
460
529
|
`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.
|
|
461
530
|
|
|
462
531
|
```tsx
|
|
@@ -568,6 +637,65 @@ A bar in its own column reads as empty cells to assistive technology, which is f
|
|
|
568
637
|
</DescriptionList>
|
|
569
638
|
```
|
|
570
639
|
|
|
640
|
+
`StatTile` is one prominent figure with its label: a `label`, a consumer-formatted `value` (tabular figures; `tone` `accent`, `success`, `danger` or `warning` colours it), an optional `caption` under it and an optional `glyph` in a tinted circle. `emphasis="primary"` fills the one figure a page leads with in the accent. `appearance="plain"` (default `"card"`, `statTileAppearances`) drops the tile's surface, edge, radius and padding, top-aligned, for figures inside a card that already has them, such as four answers in a row divided by hairlines: compose a `Grid` and a vertical `Separator` before every tile but the first, hiding the one that would start a row below `md` (`Inline hideBelow="md"` around it). A plain tile cannot be `primary` (there is no surface to fill), and an unknown appearance throws a `RangeError`.
|
|
641
|
+
|
|
642
|
+
```tsx
|
|
643
|
+
<Card padding={5}>
|
|
644
|
+
<Grid columns={4} columnsBelow={{ md: 2 }} gap={4}>
|
|
645
|
+
{answers.map((answer, index) => (
|
|
646
|
+
<Inline align="stretch" gap={4} key={answer.label}>
|
|
647
|
+
{index > 0 ? (
|
|
648
|
+
// 2 is columnsBelow md: an answer that starts a row there has no hairline.
|
|
649
|
+
<Inline
|
|
650
|
+
align="stretch"
|
|
651
|
+
hideBelow={index % 2 === 0 ? 'md' : undefined}
|
|
652
|
+
>
|
|
653
|
+
<Separator orientation="vertical" decorative />
|
|
654
|
+
</Inline>
|
|
655
|
+
) : null}
|
|
656
|
+
<StatTile appearance="plain" {...answer} />
|
|
657
|
+
</Inline>
|
|
658
|
+
))}
|
|
659
|
+
</Grid>
|
|
660
|
+
</Card>
|
|
661
|
+
```
|
|
662
|
+
|
|
663
|
+
`ListGroup` is rows in one list with a hairline between each pair, the web twin of react-native's: a `ul` (its list role kept) named by `aria-label` or `aria-labelledby`. `variant="panel"` (the default) is one bordered panel; `plain` draws no panel and a hairline above the first row too, for rows inside a `Card`. Each `ListRow` has a `title` (weight 600, or the body weight with `titleAppearance="plain"`; `tone="danger"` for a row that ends something), an optional muted `detail` line, an optional `value` at its end (weight 600, tabular figures; `valueTone` `success`, `danger`, `muted` or `accent`; `valueDetail` a muted line under it) and an optional `leading` mark such as a status dot, hidden from assistive technology, so the words say what it means. With `onPress` the whole row is a button, and with `href` a link (`onPress` then takes a plain press for a client router); either adds a chevron, a quiet fill under the pointer and 44 px on a coarse pointer, and the row is named by its title and value and described by its detail lines (`aria-label` replaces the name). `selected` makes a pressable row a choice: a check while true, `aria-pressed` on a button or `aria-current` on a link (on a row with neither `onPress` nor `href` it throws, since nothing could read the state). `accessory="none"` hides the chevron of a row that acts in place; `disabled` blocks presses. A row with neither `onPress` nor `href` only states something. `maxHeight={{ rows }}` bounds the list to that many rows of a title and one detail line, the rest scrolling inside its own named region, which shades an edge with more rows past it; a bounded list must be named. `footer` stays pinned under the rows, outside the scroll, such as a `ListRow` with the count and the total (a row there is not a list item). An empty title or value, an unknown variant or tone, a `valueDetail` without a value, or a bad row count throws a `RangeError`.
|
|
664
|
+
|
|
665
|
+
```tsx
|
|
666
|
+
<Card padding={4}>
|
|
667
|
+
<Text as="h3" id="look" variant="label">
|
|
668
|
+
Worth a look
|
|
669
|
+
</Text>
|
|
670
|
+
<ListGroup aria-labelledby="look" variant="plain">
|
|
671
|
+
<ListRow
|
|
672
|
+
leading={<Circle size={8} fill="currentColor" />}
|
|
673
|
+
title="Wed, Oct 7 · North marsh counted 6 fewer herons"
|
|
674
|
+
detail="Recounted Sat, Oct 10 by Field team A"
|
|
675
|
+
href="/nights/2026-10-07"
|
|
676
|
+
onPress={() => router.push('/nights/2026-10-07')}
|
|
677
|
+
/>
|
|
678
|
+
</ListGroup>
|
|
679
|
+
</Card>
|
|
680
|
+
|
|
681
|
+
<ListGroup
|
|
682
|
+
aria-label="Night counts"
|
|
683
|
+
maxHeight={{ rows: 5 }}
|
|
684
|
+
footer={<ListRow title="8 nights" value="+0" />}
|
|
685
|
+
>
|
|
686
|
+
{nights.map((night) => (
|
|
687
|
+
<ListRow
|
|
688
|
+
key={night.id}
|
|
689
|
+
title={night.name}
|
|
690
|
+
detail={night.detail}
|
|
691
|
+
value={night.change}
|
|
692
|
+
valueTone={night.tone}
|
|
693
|
+
onPress={() => openNight(night)}
|
|
694
|
+
/>
|
|
695
|
+
))}
|
|
696
|
+
</ListGroup>
|
|
697
|
+
```
|
|
698
|
+
|
|
571
699
|
`Card` defaults to `glass`; `outlined` and `elevated` are solid surfaces, and `filled` is a quiet `--sw-color-subtle` fill with no border for plain information apart from pressable rows.
|
|
572
700
|
|
|
573
701
|
`--sw-color-accentSubtle` is the accent tint from `@scalewing/tokens`: a quiet fill behind accent text or glyphs that keeps 4.5:1. The stylesheet and every palette file set it, and a palette that moves the accent or surface retints it.
|
|
@@ -684,6 +812,32 @@ People type the date into a text entry in the locale's numeric order (`MM/DD/YYY
|
|
|
684
812
|
</Inline>
|
|
685
813
|
```
|
|
686
814
|
|
|
815
|
+
`DateRangeButton` shows a range of days and opens a calendar to pick a first and a last day, such as a report's period between its own ‹ and › steps. Supply `label` (what pressing it does, such as "Choose survey period"), `value` (`{ start, end }`, both `YYYY-MM-DD` and included) and `onChange`; optional `min`, `max`, `today`, `disabled`, `locale`, `weekStartsOn`, `id` and a `ref` behave as on `CalendarButton`. The button shows a calendar glyph and the range in the locale, sharing what both ends have ("Oct 6 – 12, 2026"; one day as one date); its name is `label`, `labels.nameSeparator`, then that range. The first press on a day sets the start and the second the end; a press before the start starts over from it, and Apply with only a start applies that one day. Days after `max` are disabled, and once a start is pressed so are the days past `maxDays` (a whole number of days, both ends counted, such as 366). The range being picked and its length (`labels.dayCount`) are a polite status. **Apply** calls `onChange` when the range changed, closes and returns focus to the button; **Cancel**, Escape and a press outside close without a change. `presets` (`{ id, label, range }[]`) are ranges chosen in one press: a press applies its range at once and closes, and the one matching the range shown is pressed; a preset outside `min`/`max` or longer than `maxDays` throws, so compute presets from the same bounds and leave out one that does not fit (such as Last year for a station opened this spring). From `md` up the calendar is a popover with the presets beside two months (above them below `lg`, where both do not fit side by side), the range and the hint, and Cancel and Apply; below `md` it is a `Dialog` bottom sheet titled by `label` with the range under the title, the presets in a row that scrolls sideways, one month and the hint over Cancel and Apply. The keyboard is the calendar's: arrows, Home, End, PageUp and PageDown (with Shift by year) move one focus day across both months, and Enter or Space presses it. `labels` takes `previousMonth`, `nextMonth`, `nameSeparator`, `presets` (the presets group's name), `apply`, `cancel`, `close`, `hint` and `dayCount(days)`, in English by default. `size` (default `md`, at least 44 px tall on a coarse pointer) and `variant` (default `neutral`) follow `Button`. A value outside `min`/`max` or longer than `maxDays` is kept; an empty or malformed date, a start after its end, a `maxDays` under 1 or fractional, repeated preset ids, or an empty word throws a `RangeError`.
|
|
816
|
+
|
|
817
|
+
```tsx
|
|
818
|
+
<ButtonGroup aria-label="Survey period" attached>
|
|
819
|
+
<Button aria-label="The period before" variant="neutral" onPress={previous}>
|
|
820
|
+
‹
|
|
821
|
+
</Button>
|
|
822
|
+
<DateRangeButton
|
|
823
|
+
label="Choose survey period"
|
|
824
|
+
value={period}
|
|
825
|
+
onChange={setPeriod}
|
|
826
|
+
presets={[
|
|
827
|
+
{ id: 'week', label: 'Last 7 days', range: lastSevenDays },
|
|
828
|
+
{ id: 'month', label: 'This month', range: thisMonth },
|
|
829
|
+
]}
|
|
830
|
+
max={stationDay}
|
|
831
|
+
maxDays={366}
|
|
832
|
+
today={stationDay}
|
|
833
|
+
labels={{ apply: 'Show' }}
|
|
834
|
+
/>
|
|
835
|
+
<Button aria-label="The period after" variant="neutral" onPress={next}>
|
|
836
|
+
›
|
|
837
|
+
</Button>
|
|
838
|
+
</ButtonGroup>
|
|
839
|
+
```
|
|
840
|
+
|
|
687
841
|
```tsx
|
|
688
842
|
<Switch
|
|
689
843
|
label="Active habitats only"
|
package/dist/calendar-month.d.ts
CHANGED
|
@@ -17,8 +17,14 @@ export declare function weekdayOf(value: string): number;
|
|
|
17
17
|
export declare function addDays(value: string, delta: number): string;
|
|
18
18
|
/** Same day of the month `delta` months away, capped at that month's length. */
|
|
19
19
|
export declare function addMonths(value: string, delta: number): string;
|
|
20
|
+
/** Days from `start` to `end`: 0 for the same day, negative when `end` is earlier. */
|
|
21
|
+
export declare function daysBetween(start: string, end: string): number;
|
|
20
22
|
export declare function monthOf(value: string): CalendarMonth;
|
|
21
23
|
export declare function shiftMonth(month: CalendarMonth, delta: number): CalendarMonth;
|
|
24
|
+
/** Months from `from` to `to`: 0 for the same month, negative when `to` is earlier. */
|
|
25
|
+
export declare function monthsBetween(from: CalendarMonth, to: CalendarMonth): number;
|
|
26
|
+
/** `value`'s day of the month in `month`, capped at that month's length. */
|
|
27
|
+
export declare function sameDayIn(month: CalendarMonth, value: string): string;
|
|
22
28
|
export declare function firstOfMonth(month: CalendarMonth): string;
|
|
23
29
|
export declare function lastOfMonth(month: CalendarMonth): string;
|
|
24
30
|
/** Whether any day of the month falls inside `[min, max]`. */
|
package/dist/calendar-month.js
CHANGED
|
@@ -61,6 +61,10 @@ export function addMonths(value, delta) {
|
|
|
61
61
|
day: Math.min(parts.day, daysInMonth(month.year, month.month)),
|
|
62
62
|
});
|
|
63
63
|
}
|
|
64
|
+
/** Days from `start` to `end`: 0 for the same day, negative when `end` is earlier. */
|
|
65
|
+
export function daysBetween(start, end) {
|
|
66
|
+
return toDayNumber(requireDate(end)) - toDayNumber(requireDate(start));
|
|
67
|
+
}
|
|
64
68
|
export function monthOf(value) {
|
|
65
69
|
const parts = requireDate(value);
|
|
66
70
|
return { year: parts.year, month: parts.month };
|
|
@@ -69,6 +73,17 @@ export function shiftMonth(month, delta) {
|
|
|
69
73
|
const index = month.year * 12 + (month.month - 1) + delta;
|
|
70
74
|
return { year: Math.floor(index / 12), month: (index % 12) + 1 };
|
|
71
75
|
}
|
|
76
|
+
/** Months from `from` to `to`: 0 for the same month, negative when `to` is earlier. */
|
|
77
|
+
export function monthsBetween(from, to) {
|
|
78
|
+
return to.year * 12 + to.month - (from.year * 12 + from.month);
|
|
79
|
+
}
|
|
80
|
+
/** `value`'s day of the month in `month`, capped at that month's length. */
|
|
81
|
+
export function sameDayIn(month, value) {
|
|
82
|
+
return formatDateOnly({
|
|
83
|
+
...month,
|
|
84
|
+
day: Math.min(requireDate(value).day, daysInMonth(month.year, month.month)),
|
|
85
|
+
});
|
|
86
|
+
}
|
|
72
87
|
export function firstOfMonth(month) {
|
|
73
88
|
return formatDateOnly({ ...month, day: 1 });
|
|
74
89
|
}
|
|
@@ -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;
|