@teamofsilicons/silicon-ui 0.0.0-stage → 0.2.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/LICENSE +22 -0
- package/README.md +81 -2
- package/check.mjs +236 -0
- package/components/accordion/accordion.d.ts +13 -0
- package/components/accordion/accordion.js +35 -0
- package/components/accordion/accordion.module.css +15 -0
- package/components/action-button/action-button.d.ts +11 -0
- package/components/action-button/action-button.js +57 -0
- package/components/action-button/action-button.module.css +15 -0
- package/components/activity-heatmap/activity-heatmap.d.ts +37 -0
- package/components/activity-heatmap/activity-heatmap.js +303 -0
- package/components/activity-heatmap/activity-heatmap.module.css +83 -0
- package/components/alert/alert.d.ts +13 -0
- package/components/alert/alert.js +79 -0
- package/components/alert/alert.module.css +15 -0
- package/components/animated-counter/animated-counter.d.ts +13 -0
- package/components/animated-counter/animated-counter.js +73 -0
- package/components/animated-counter/animated-counter.module.css +12 -0
- package/components/announcement-bar/announcement-bar.d.ts +53 -0
- package/components/announcement-bar/announcement-bar.js +187 -0
- package/components/announcement-bar/announcement-bar.module.css +60 -0
- package/components/avatar/avatar.d.ts +8 -0
- package/components/avatar/avatar.js +18 -0
- package/components/avatar/avatar.module.css +60 -0
- package/components/avatar-group/avatar-group.d.ts +13 -0
- package/components/avatar-group/avatar-group.js +24 -0
- package/components/avatar-group/avatar-group.module.css +27 -0
- package/components/badge/badge.d.ts +10 -0
- package/components/badge/badge.js +66 -0
- package/components/badge/badge.module.css +52 -0
- package/components/bar-chart/bar-chart.d.ts +32 -0
- package/components/bar-chart/bar-chart.js +327 -0
- package/components/bar-chart/bar-chart.module.css +49 -0
- package/components/billing-toggle/billing-toggle.d.ts +35 -0
- package/components/billing-toggle/billing-toggle.js +83 -0
- package/components/billing-toggle/billing-toggle.module.css +139 -0
- package/components/bottom-sheet/bottom-sheet.d.ts +27 -0
- package/components/bottom-sheet/bottom-sheet.js +386 -0
- package/components/bottom-sheet/bottom-sheet.module.css +69 -0
- package/components/breadcrumb/breadcrumb.d.ts +13 -0
- package/components/breadcrumb/breadcrumb.js +19 -0
- package/components/breadcrumb/breadcrumb.module.css +12 -0
- package/components/brush-chart/brush-chart.d.ts +45 -0
- package/components/brush-chart/brush-chart.js +426 -0
- package/components/brush-chart/brush-chart.module.css +69 -0
- package/components/button/button.d.ts +10 -0
- package/components/button/button.js +97 -0
- package/components/button/button.module.css +39 -0
- package/components/button-group/button-group.d.ts +59 -0
- package/components/button-group/button-group.js +280 -0
- package/components/button-group/button-group.module.css +87 -0
- package/components/button-link/button-link.d.ts +24 -0
- package/components/button-link/button-link.js +11 -0
- package/components/button-link/button-link.module.css +7 -0
- package/components/calendar/calendar.d.ts +22 -0
- package/components/calendar/calendar.js +253 -0
- package/components/calendar/calendar.module.css +51 -0
- package/components/card/card.d.ts +19 -0
- package/components/card/card.js +108 -0
- package/components/card/card.module.css +51 -0
- package/components/card-stack/card-stack.d.ts +34 -0
- package/components/card-stack/card-stack.js +291 -0
- package/components/card-stack/card-stack.module.css +63 -0
- package/components/carousel/carousel.d.ts +28 -0
- package/components/carousel/carousel.js +332 -0
- package/components/carousel/carousel.module.css +42 -0
- package/components/chat-thread/chat-thread.d.ts +135 -0
- package/components/chat-thread/chat-thread.js +406 -0
- package/components/chat-thread/chat-thread.module.css +242 -0
- package/components/checkbox/checkbox.d.ts +7 -0
- package/components/checkbox/checkbox.js +23 -0
- package/components/checkbox/checkbox.module.css +15 -0
- package/components/chip-group/chip-group.d.ts +24 -0
- package/components/chip-group/chip-group.js +196 -0
- package/components/chip-group/chip-group.module.css +38 -0
- package/components/code-block/code-block.d.ts +11 -0
- package/components/code-block/code-block.js +133 -0
- package/components/code-block/code-block.module.css +43 -0
- package/components/color-picker/color-picker.d.ts +40 -0
- package/components/color-picker/color-picker.js +449 -0
- package/components/color-picker/color-picker.module.css +111 -0
- package/components/combobox/combobox.d.ts +19 -0
- package/components/combobox/combobox.js +167 -0
- package/components/combobox/combobox.module.css +28 -0
- package/components/command-palette/command-palette.d.ts +20 -0
- package/components/command-palette/command-palette.js +151 -0
- package/components/command-palette/command-palette.module.css +94 -0
- package/components/comment-thread/comment-thread.d.ts +120 -0
- package/components/comment-thread/comment-thread.js +343 -0
- package/components/comment-thread/comment-thread.module.css +195 -0
- package/components/confirm-morph/confirm-morph.d.ts +65 -0
- package/components/confirm-morph/confirm-morph.js +355 -0
- package/components/confirm-morph/confirm-morph.module.css +146 -0
- package/components/context-menu/context-menu.d.ts +18 -0
- package/components/context-menu/context-menu.js +148 -0
- package/components/context-menu/context-menu.module.css +17 -0
- package/components/copy-button/copy-button.d.ts +10 -0
- package/components/copy-button/copy-button.js +69 -0
- package/components/copy-button/copy-button.module.css +21 -0
- package/components/countdown/countdown.d.ts +31 -0
- package/components/countdown/countdown.js +177 -0
- package/components/countdown/countdown.module.css +49 -0
- package/components/date-picker/date-picker.d.ts +17 -0
- package/components/date-picker/date-picker.js +100 -0
- package/components/date-picker/date-picker.module.css +34 -0
- package/components/date-range-picker/date-range-picker.d.ts +44 -0
- package/components/date-range-picker/date-range-picker.js +534 -0
- package/components/date-range-picker/date-range-picker.module.css +214 -0
- package/components/dialog/dialog.d.ts +11 -0
- package/components/dialog/dialog.js +52 -0
- package/components/dialog/dialog.module.css +36 -0
- package/components/donut-chart/donut-chart.d.ts +46 -0
- package/components/donut-chart/donut-chart.js +382 -0
- package/components/donut-chart/donut-chart.module.css +48 -0
- package/components/drawer/drawer.d.ts +20 -0
- package/components/drawer/drawer.js +111 -0
- package/components/drawer/drawer.module.css +97 -0
- package/components/dropdown-menu/dropdown-menu.d.ts +16 -0
- package/components/dropdown-menu/dropdown-menu.js +39 -0
- package/components/dropdown-menu/dropdown-menu.module.css +38 -0
- package/components/elastic-slider/elastic-slider.d.ts +39 -0
- package/components/elastic-slider/elastic-slider.js +261 -0
- package/components/elastic-slider/elastic-slider.module.css +40 -0
- package/components/empty-state/empty-state.d.ts +12 -0
- package/components/empty-state/empty-state.js +69 -0
- package/components/empty-state/empty-state.module.css +13 -0
- package/components/expandable-card/expandable-card.d.ts +13 -0
- package/components/expandable-card/expandable-card.js +89 -0
- package/components/expandable-card/expandable-card.module.css +22 -0
- package/components/expanding-button-group/expanding-button-group.d.ts +41 -0
- package/components/expanding-button-group/expanding-button-group.js +258 -0
- package/components/expanding-button-group/expanding-button-group.module.css +44 -0
- package/components/expanding-search/expanding-search.d.ts +38 -0
- package/components/expanding-search/expanding-search.js +242 -0
- package/components/expanding-search/expanding-search.module.css +52 -0
- package/components/file-dropzone/file-dropzone.d.ts +44 -0
- package/components/file-dropzone/file-dropzone.js +379 -0
- package/components/file-dropzone/file-dropzone.module.css +115 -0
- package/components/file-upload/file-upload.d.ts +22 -0
- package/components/file-upload/file-upload.js +192 -0
- package/components/file-upload/file-upload.module.css +49 -0
- package/components/filter-toolbar/filter-toolbar.d.ts +48 -0
- package/components/filter-toolbar/filter-toolbar.js +446 -0
- package/components/filter-toolbar/filter-toolbar.module.css +70 -0
- package/components/floating-button-group/floating-button-group.d.ts +51 -0
- package/components/floating-button-group/floating-button-group.js +215 -0
- package/components/floating-button-group/floating-button-group.module.css +74 -0
- package/components/gauge/gauge.d.ts +18 -0
- package/components/gauge/gauge.js +101 -0
- package/components/gauge/gauge.module.css +22 -0
- package/components/hold-to-confirm/hold-to-confirm.d.ts +25 -0
- package/components/hold-to-confirm/hold-to-confirm.js +165 -0
- package/components/hold-to-confirm/hold-to-confirm.module.css +20 -0
- package/components/hover-card/hover-card.d.ts +38 -0
- package/components/hover-card/hover-card.js +145 -0
- package/components/hover-card/hover-card.module.css +20 -0
- package/components/image-compare/image-compare.d.ts +27 -0
- package/components/image-compare/image-compare.js +267 -0
- package/components/image-compare/image-compare.module.css +25 -0
- package/components/in-view-title/in-view-title.d.ts +17 -0
- package/components/in-view-title/in-view-title.js +70 -0
- package/components/in-view-title/in-view-title.module.css +13 -0
- package/components/inline-edit/inline-edit.d.ts +27 -0
- package/components/inline-edit/inline-edit.js +315 -0
- package/components/inline-edit/inline-edit.module.css +73 -0
- package/components/input/input.d.ts +7 -0
- package/components/input/input.js +60 -0
- package/components/input/input.module.css +22 -0
- package/components/json-viewer/json-viewer.d.ts +48 -0
- package/components/json-viewer/json-viewer.js +463 -0
- package/components/json-viewer/json-viewer.module.css +92 -0
- package/components/line-chart/line-chart.d.ts +58 -0
- package/components/line-chart/line-chart.js +383 -0
- package/components/line-chart/line-chart.module.css +60 -0
- package/components/mention-input/mention-input.d.ts +72 -0
- package/components/mention-input/mention-input.js +417 -0
- package/components/mention-input/mention-input.module.css +134 -0
- package/components/metric-card/metric-card.d.ts +8 -0
- package/components/metric-card/metric-card.js +44 -0
- package/components/metric-card/metric-card.module.css +18 -0
- package/components/money-input/money-input.d.ts +48 -0
- package/components/money-input/money-input.js +503 -0
- package/components/money-input/money-input.module.css +77 -0
- package/components/morph-select/morph-select.d.ts +50 -0
- package/components/morph-select/morph-select.js +395 -0
- package/components/morph-select/morph-select.module.css +88 -0
- package/components/multi-select/multi-select.d.ts +18 -0
- package/components/multi-select/multi-select.js +105 -0
- package/components/multi-select/multi-select.module.css +37 -0
- package/components/notification-center/notification-center.d.ts +24 -0
- package/components/notification-center/notification-center.js +146 -0
- package/components/notification-center/notification-center.module.css +79 -0
- package/components/number-field/number-field.d.ts +41 -0
- package/components/number-field/number-field.js +455 -0
- package/components/number-field/number-field.module.css +71 -0
- package/components/otp-input/otp-input.d.ts +13 -0
- package/components/otp-input/otp-input.js +129 -0
- package/components/otp-input/otp-input.module.css +28 -0
- package/components/pagination/pagination.d.ts +7 -0
- package/components/pagination/pagination.js +60 -0
- package/components/pagination/pagination.module.css +16 -0
- package/components/password-field/password-field.d.ts +14 -0
- package/components/password-field/password-field.js +51 -0
- package/components/password-field/password-field.module.css +27 -0
- package/components/password-strength/password-strength.d.ts +107 -0
- package/components/password-strength/password-strength.js +161 -0
- package/components/password-strength/password-strength.module.css +57 -0
- package/components/phone-input/phone-input.d.ts +73 -0
- package/components/phone-input/phone-input.js +612 -0
- package/components/phone-input/phone-input.module.css +98 -0
- package/components/popover/popover.d.ts +6 -0
- package/components/popover/popover.js +19 -0
- package/components/popover/popover.module.css +32 -0
- package/components/progress/progress.d.ts +8 -0
- package/components/progress/progress.js +42 -0
- package/components/progress/progress.module.css +1 -0
- package/components/radio-cards/radio-cards.d.ts +35 -0
- package/components/radio-cards/radio-cards.js +110 -0
- package/components/radio-cards/radio-cards.module.css +84 -0
- package/components/radio-group/radio-group.d.ts +13 -0
- package/components/radio-group/radio-group.js +55 -0
- package/components/radio-group/radio-group.module.css +30 -0
- package/components/resizable-panels/resizable-panels.d.ts +37 -0
- package/components/resizable-panels/resizable-panels.js +338 -0
- package/components/resizable-panels/resizable-panels.module.css +40 -0
- package/components/rich-text-editor/rich-text-editor.d.ts +55 -0
- package/components/rich-text-editor/rich-text-editor.js +1733 -0
- package/components/rich-text-editor/rich-text-editor.module.css +201 -0
- package/components/ridgeline/ridgeline.d.ts +37 -0
- package/components/ridgeline/ridgeline.js +346 -0
- package/components/ridgeline/ridgeline.module.css +58 -0
- package/components/scroll-area/scroll-area.d.ts +40 -0
- package/components/scroll-area/scroll-area.js +153 -0
- package/components/scroll-area/scroll-area.module.css +79 -0
- package/components/search-field/search-field.d.ts +7 -0
- package/components/search-field/search-field.js +20 -0
- package/components/search-field/search-field.module.css +17 -0
- package/components/segmented-control/segmented-control.d.ts +16 -0
- package/components/segmented-control/segmented-control.js +56 -0
- package/components/segmented-control/segmented-control.module.css +15 -0
- package/components/select/select.d.ts +15 -0
- package/components/select/select.js +36 -0
- package/components/select/select.module.css +35 -0
- package/components/shortcut-recorder/shortcut-recorder.d.ts +108 -0
- package/components/shortcut-recorder/shortcut-recorder.js +380 -0
- package/components/shortcut-recorder/shortcut-recorder.module.css +85 -0
- package/components/signature-pad/signature-pad.d.ts +72 -0
- package/components/signature-pad/signature-pad.js +450 -0
- package/components/signature-pad/signature-pad.module.css +84 -0
- package/components/skeleton/skeleton.d.ts +13 -0
- package/components/skeleton/skeleton.js +53 -0
- package/components/skeleton/skeleton.module.css +11 -0
- package/components/slider/slider.d.ts +45 -0
- package/components/slider/slider.js +347 -0
- package/components/slider/slider.module.css +45 -0
- package/components/slope-chart/slope-chart.d.ts +38 -0
- package/components/slope-chart/slope-chart.js +183 -0
- package/components/slope-chart/slope-chart.module.css +51 -0
- package/components/slot-text/slot-text.d.ts +26 -0
- package/components/slot-text/slot-text.js +119 -0
- package/components/slot-text/slot-text.module.css +19 -0
- package/components/sortable-data-table/sortable-data-table.d.ts +38 -0
- package/components/sortable-data-table/sortable-data-table.js +240 -0
- package/components/sortable-data-table/sortable-data-table.module.css +116 -0
- package/components/sparkline/sparkline.d.ts +19 -0
- package/components/sparkline/sparkline.js +239 -0
- package/components/sparkline/sparkline.module.css +26 -0
- package/components/split-button/split-button.d.ts +19 -0
- package/components/split-button/split-button.js +33 -0
- package/components/split-button/split-button.module.css +51 -0
- package/components/stepper/stepper.d.ts +36 -0
- package/components/stepper/stepper.js +108 -0
- package/components/stepper/stepper.module.css +74 -0
- package/components/streamgraph/streamgraph.d.ts +51 -0
- package/components/streamgraph/streamgraph.js +376 -0
- package/components/streamgraph/streamgraph.module.css +55 -0
- package/components/swipe-actions/swipe-actions.d.ts +35 -0
- package/components/swipe-actions/swipe-actions.js +330 -0
- package/components/swipe-actions/swipe-actions.module.css +64 -0
- package/components/switch/switch.d.ts +7 -0
- package/components/switch/switch.js +45 -0
- package/components/switch/switch.module.css +12 -0
- package/components/tabs/tabs.d.ts +8 -0
- package/components/tabs/tabs.js +138 -0
- package/components/tabs/tabs.module.css +27 -0
- package/components/tag-input/tag-input.d.ts +15 -0
- package/components/tag-input/tag-input.js +150 -0
- package/components/tag-input/tag-input.module.css +34 -0
- package/components/text-morph/text-morph.d.ts +13 -0
- package/components/text-morph/text-morph.js +69 -0
- package/components/text-morph/text-morph.module.css +8 -0
- package/components/text-reveal/text-reveal.d.ts +14 -0
- package/components/text-reveal/text-reveal.js +20 -0
- package/components/text-reveal/text-reveal.module.css +13 -0
- package/components/text-shimmer/text-shimmer.d.ts +18 -0
- package/components/text-shimmer/text-shimmer.js +90 -0
- package/components/text-shimmer/text-shimmer.module.css +9 -0
- package/components/textarea/textarea.d.ts +7 -0
- package/components/textarea/textarea.js +56 -0
- package/components/textarea/textarea.module.css +22 -0
- package/components/theme-switch/theme-switch.d.ts +11 -0
- package/components/theme-switch/theme-switch.js +37 -0
- package/components/theme-switch/theme-switch.module.css +31 -0
- package/components/time-picker/time-picker.d.ts +13 -0
- package/components/time-picker/time-picker.js +95 -0
- package/components/time-picker/time-picker.module.css +23 -0
- package/components/timeline/timeline.d.ts +47 -0
- package/components/timeline/timeline.js +171 -0
- package/components/timeline/timeline.module.css +51 -0
- package/components/toast/toast.d.ts +8 -0
- package/components/toast/toast.js +118 -0
- package/components/toast/toast.module.css +100 -0
- package/components/toast-stack/toast-stack.d.ts +57 -0
- package/components/toast-stack/toast-stack.js +399 -0
- package/components/toast-stack/toast-stack.module.css +61 -0
- package/components/tooltip/tooltip.d.ts +8 -0
- package/components/tooltip/tooltip.js +62 -0
- package/components/tooltip/tooltip.module.css +18 -0
- package/components/tree-view/tree-view.d.ts +17 -0
- package/components/tree-view/tree-view.js +147 -0
- package/components/tree-view/tree-view.module.css +22 -0
- package/components/treemap/treemap.d.ts +35 -0
- package/components/treemap/treemap.js +375 -0
- package/components/treemap/treemap.module.css +59 -0
- package/components/usage-meter/usage-meter.d.ts +24 -0
- package/components/usage-meter/usage-meter.js +296 -0
- package/components/usage-meter/usage-meter.module.css +61 -0
- package/components/user-menu/user-menu.d.ts +62 -0
- package/components/user-menu/user-menu.js +316 -0
- package/components/user-menu/user-menu.module.css +97 -0
- package/components/waffle-chart/waffle-chart.d.ts +39 -0
- package/components/waffle-chart/waffle-chart.js +244 -0
- package/components/waffle-chart/waffle-chart.module.css +52 -0
- package/components.json +1330 -0
- package/eslint.js +53 -0
- package/lib/escape.d.ts +19 -0
- package/lib/escape.js +50 -0
- package/lib/media.d.ts +353 -0
- package/lib/media.js +65 -0
- package/lib/menu-highlight.d.ts +28 -0
- package/lib/menu-highlight.js +43 -0
- package/lib/motion-tokens.d.ts +60 -0
- package/lib/motion-tokens.js +25 -0
- package/lib/return-focus.d.ts +9 -0
- package/lib/return-focus.js +53 -0
- package/lib/silicon-provider.d.ts +60 -0
- package/lib/silicon-provider.js +47 -0
- package/lib/use-copy-feedback.d.ts +8 -0
- package/lib/use-copy-feedback.js +36 -0
- package/lib/use-today.d.ts +2 -0
- package/lib/use-today.js +29 -0
- package/package.json +76 -4
- package/styles.css +218 -0
- package/styles.css.d.ts +1 -0
|
@@ -0,0 +1,376 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useCallback, useEffect, useImperativeHandle, useLayoutEffect, useMemo, useRef, useState, useSyncExternalStore } from "react";
|
|
4
|
+
import { animate, motion, useInView, useMotionValue, useReducedMotion, useSpring } from "motion/react";
|
|
5
|
+
import { motionTokens } from "../../lib/motion-tokens.js";
|
|
6
|
+
import styles from "./streamgraph.module.css";
|
|
7
|
+
const { spring } = motionTokens;
|
|
8
|
+
const physical = ({ visualDuration, bounce }, restDelta = .0005) => { const root = (2 * Math.PI) / (visualDuration * 1.2); return { type: "spring", stiffness: root * root, damping: 2 * (1 - bounce) * root, restDelta, restSpeed: restDelta * 2 }; };
|
|
9
|
+
const settle = physical(spring.smooth);
|
|
10
|
+
const swell = physical({ visualDuration: .9, bounce: 0 });
|
|
11
|
+
const glide = physical(spring.snappy, .01);
|
|
12
|
+
const follow = { stiffness: glide.stiffness, damping: glide.damping, restDelta: .01 };
|
|
13
|
+
/** Samples across the plot: every layer is resampled onto the same grid, so datasets of any length morph into each other. */
|
|
14
|
+
const N = 161;
|
|
15
|
+
const PAD = 10, GAP = 14;
|
|
16
|
+
/** Neutral steps by layer. Stacked from the centre out, neighbours always sit at least ten steps apart; the accent is kept for the layer in focus. */
|
|
17
|
+
/** The first four layers take the chart series in order (the first in the accent); later layers fall back to neutral steps that alternate in lightness. */
|
|
18
|
+
const SHADES = [30, 18, 24, 14];
|
|
19
|
+
const colorOf = (item, index) => item.color ?? (index < 4 ? `var(--series-${index + 1})` : `color-mix(in oklab, var(--foreground) ${SHADES[(index - 4) % SHADES.length]}%, var(--surface))`);
|
|
20
|
+
/** The first layer sits in the middle of the stream and the rest alternate above and below it; a flat baseline stacks them in order. */
|
|
21
|
+
function stackOrder(count, mode) {
|
|
22
|
+
const order = Array.from({ length: count }, (_, index) => index);
|
|
23
|
+
if (mode === "zero")
|
|
24
|
+
return order;
|
|
25
|
+
return order.reduce((list, index) => index === 0 ? [0] : index % 2 ? [...list, index] : [index, ...list], []);
|
|
26
|
+
}
|
|
27
|
+
const grouped = new Intl.NumberFormat("en-US", { maximumFractionDigits: 1 });
|
|
28
|
+
const clamp = (value, low, high) => Math.min(high, Math.max(low, value));
|
|
29
|
+
/** Resamples values onto the shared grid with monotone cubic tangents, so curves never swing past the data or below zero. */
|
|
30
|
+
function resample(raw) {
|
|
31
|
+
const values = raw.map(value => Math.max(0, value || 0));
|
|
32
|
+
const out = new Array(N);
|
|
33
|
+
if (!values.length)
|
|
34
|
+
return out.fill(0);
|
|
35
|
+
if (values.length === 1)
|
|
36
|
+
return out.fill(values[0]);
|
|
37
|
+
const last = values.length - 1;
|
|
38
|
+
const slopes = values.slice(1).map((value, index) => value - values[index]);
|
|
39
|
+
const tangents = values.map((_, index) => index === 0 || index === last ? 0 : (Math.sign(slopes[index - 1]) + Math.sign(slopes[index])) * Math.min(Math.abs(slopes[index - 1]), Math.abs(slopes[index]), Math.abs(slopes[index - 1] + slopes[index]) / 4) || 0);
|
|
40
|
+
if (last > 1) {
|
|
41
|
+
tangents[0] = (3 * slopes[0] - tangents[1]) / 2;
|
|
42
|
+
tangents[last] = (3 * slopes[last - 1] - tangents[last - 1]) / 2;
|
|
43
|
+
}
|
|
44
|
+
else {
|
|
45
|
+
tangents[0] = tangents[1] = slopes[0];
|
|
46
|
+
}
|
|
47
|
+
for (let k = 0; k < N; k++) {
|
|
48
|
+
const at = (k / (N - 1)) * last, seg = Math.min(last - 1, Math.floor(at)), t = at - seg, t2 = t * t, t3 = t2 * t;
|
|
49
|
+
const a = values[seg], b = values[seg + 1];
|
|
50
|
+
const value = (2 * t3 - 3 * t2 + 1) * a + (t3 - 2 * t2 + t) * tangents[seg] + (3 * t2 - 2 * t3) * b + (t3 - t2) * tangents[seg + 1];
|
|
51
|
+
out[k] = clamp(value, Math.min(a, b), Math.max(a, b));
|
|
52
|
+
}
|
|
53
|
+
return out;
|
|
54
|
+
}
|
|
55
|
+
/** The baseline under the stack. Wiggle follows Byron and Wattenberg, then removes its linear drift so the stream stays level. */
|
|
56
|
+
function baseline(layers, mode) {
|
|
57
|
+
const g = new Array(N).fill(0);
|
|
58
|
+
const totals = g.map((_, j) => layers.reduce((sum, layer) => sum + layer[j], 0));
|
|
59
|
+
if (mode === "zero" || !layers.length)
|
|
60
|
+
return g;
|
|
61
|
+
if (mode === "silhouette")
|
|
62
|
+
return totals.map(total => -total / 2);
|
|
63
|
+
for (let j = 1; j < N; j++) {
|
|
64
|
+
let s1 = 0, s2 = 0, below = 0;
|
|
65
|
+
for (const layer of layers) {
|
|
66
|
+
const change = layer[j] - layer[j - 1];
|
|
67
|
+
s1 += layer[j];
|
|
68
|
+
s2 += (below + change / 2) * layer[j];
|
|
69
|
+
below += change;
|
|
70
|
+
}
|
|
71
|
+
g[j] = g[j - 1] - (s1 ? s2 / s1 : 0);
|
|
72
|
+
}
|
|
73
|
+
// Least squares line through the centre of the stream, subtracted so it never tilts off the plot.
|
|
74
|
+
const centers = g.map((value, j) => value + totals[j] / 2);
|
|
75
|
+
const mx = (N - 1) / 2, my = centers.reduce((sum, value) => sum + value, 0) / N;
|
|
76
|
+
let num = 0, den = 0;
|
|
77
|
+
centers.forEach((value, j) => { num += (j - mx) * (value - my); den += (j - mx) ** 2; });
|
|
78
|
+
const slope = den ? num / den : 0;
|
|
79
|
+
return g.map((value, j) => value - (my + slope * (j - mx)));
|
|
80
|
+
}
|
|
81
|
+
const subscribeNothing = () => () => { };
|
|
82
|
+
function useReducedMotionSafe() {
|
|
83
|
+
const hydrated = useSyncExternalStore(subscribeNothing, () => true, () => false);
|
|
84
|
+
return !!useReducedMotion() && hydrated;
|
|
85
|
+
}
|
|
86
|
+
/** Axis labels that fit: at most one per 72 pixels, counted back from the latest. */
|
|
87
|
+
function axisPicks(data, width) {
|
|
88
|
+
const labeled = data.flatMap((item, index) => item.axisLabel ? [index] : []);
|
|
89
|
+
if (!width || !labeled.length)
|
|
90
|
+
return [];
|
|
91
|
+
const stride = Math.ceil(labeled.length / Math.max(2, Math.floor(width / 72)));
|
|
92
|
+
return labeled.reverse().filter((_, rank) => rank % stride === 0).reverse();
|
|
93
|
+
}
|
|
94
|
+
export function Streamgraph({ data, series, label, unit = "", height = 260, offset = "wiggle", formatValue, hiddenSeries, defaultHiddenSeries, onHiddenSeriesChange, onActiveChange, legend = true, directLabels = true, categoryLabel = "Date", emptyLabel = "No data for this range", ref, className }) {
|
|
95
|
+
const reduced = useReducedMotionSafe();
|
|
96
|
+
const figure = useRef(null);
|
|
97
|
+
const plot = useRef(null);
|
|
98
|
+
const tip = useRef(null);
|
|
99
|
+
const paths = useRef(new Map());
|
|
100
|
+
const labels = useRef(new Map());
|
|
101
|
+
useImperativeHandle(ref, () => figure.current);
|
|
102
|
+
const inView = useInView(figure, { once: true, amount: .3 });
|
|
103
|
+
const last = data.length - 1;
|
|
104
|
+
const empty = data.length === 0;
|
|
105
|
+
const [ownHidden, setOwnHidden] = useState(defaultHiddenSeries ?? []);
|
|
106
|
+
const hidden = hiddenSeries ?? ownHidden;
|
|
107
|
+
const toggle = (key) => {
|
|
108
|
+
const next = hidden.includes(key) ? hidden.filter(item => item !== key) : [...hidden, key];
|
|
109
|
+
if (hiddenSeries === undefined)
|
|
110
|
+
setOwnHidden(next);
|
|
111
|
+
onHiddenSeriesChange?.(next);
|
|
112
|
+
};
|
|
113
|
+
const visible = series.filter(item => !hidden.includes(item.key));
|
|
114
|
+
// Each layer becomes a dense curve on the shared grid; the painter blends from what is on screen to it.
|
|
115
|
+
const signature = `${series.map(item => item.key).join(",")}|${data.map(item => series.map(line => item.values[line.key] ?? 0).join(",")).join(";")}`;
|
|
116
|
+
const targets = useMemo(() => new Map(series.map(line => [line.key, resample(data.map(item => item.values[line.key] ?? 0))])), [signature]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
117
|
+
const [hover, setHover] = useState(null);
|
|
118
|
+
const [seenSignature, setSeenSignature] = useState(signature);
|
|
119
|
+
if (seenSignature !== signature) {
|
|
120
|
+
setSeenSignature(signature);
|
|
121
|
+
setHover(null);
|
|
122
|
+
}
|
|
123
|
+
const index = hover && !empty ? Math.min(hover.index, last) : null;
|
|
124
|
+
const [legendKey, setLegendKey] = useState(null);
|
|
125
|
+
const wanted = hover?.key ?? legendKey;
|
|
126
|
+
const activeKey = wanted && visible.some(item => item.key === wanted) ? wanted : null;
|
|
127
|
+
const width = useRef(0);
|
|
128
|
+
const [plotWidth, setPlotWidth] = useState(0);
|
|
129
|
+
const shapes = useRef(new Map());
|
|
130
|
+
const presence = useRef(new Map());
|
|
131
|
+
const frame = useRef(null);
|
|
132
|
+
const spots = useRef(new Map());
|
|
133
|
+
const grow = useMotionValue(0);
|
|
134
|
+
const live = useRef({ series, offset });
|
|
135
|
+
useLayoutEffect(() => { live.current = { series, offset }; });
|
|
136
|
+
// One writer for every drawn attribute: morphs, toggles, the first swell, and resizes all repaint through here.
|
|
137
|
+
const paint = useCallback(() => {
|
|
138
|
+
const w = width.current;
|
|
139
|
+
if (!w)
|
|
140
|
+
return;
|
|
141
|
+
const { series: order, offset: mode } = live.current;
|
|
142
|
+
const layers = order.map(line => { const shape = shapes.current.get(line.key); const p = presence.current.get(line.key)?.value ?? 1; return shape ? shape.map(value => value * p) : new Array(N).fill(0); });
|
|
143
|
+
const stack = stackOrder(order.length, mode);
|
|
144
|
+
const g = baseline(stack.map(i => layers[i]), mode);
|
|
145
|
+
const lower = new Array(order.length), upper = new Array(order.length);
|
|
146
|
+
let running = g.slice();
|
|
147
|
+
for (const i of stack) {
|
|
148
|
+
const layer = layers[i];
|
|
149
|
+
lower[i] = running;
|
|
150
|
+
running = running.map((value, j) => value + layer[j]);
|
|
151
|
+
upper[i] = running;
|
|
152
|
+
}
|
|
153
|
+
let lo = Infinity, hi = -Infinity;
|
|
154
|
+
for (let j = 0; j < N; j++) {
|
|
155
|
+
lo = Math.min(lo, g[j]);
|
|
156
|
+
hi = Math.max(hi, running[j]);
|
|
157
|
+
}
|
|
158
|
+
if (!Number.isFinite(lo) || hi - lo < 1e-9) {
|
|
159
|
+
lo = -1;
|
|
160
|
+
hi = 1;
|
|
161
|
+
}
|
|
162
|
+
const span = hi - lo, s = grow.get();
|
|
163
|
+
const center = g.map((value, j) => (value + running[j]) / 2);
|
|
164
|
+
const toY = (value) => PAD + (1 - (value - lo) / span) * (height - PAD * 2);
|
|
165
|
+
const yAt = (value, j) => toY(center[j] + (value - center[j]) * s);
|
|
166
|
+
frame.current = { lower, upper, toY, center };
|
|
167
|
+
const xs = Array.from({ length: N }, (_, j) => (j / (N - 1)) * w);
|
|
168
|
+
order.forEach((line, i) => {
|
|
169
|
+
const top = upper[i], bottom = lower[i];
|
|
170
|
+
const node = paths.current.get(line.key);
|
|
171
|
+
if (node) {
|
|
172
|
+
let d = "";
|
|
173
|
+
for (let j = 0; j < N; j++)
|
|
174
|
+
d += `${j ? "L" : "M"}${xs[j].toFixed(1)},${yAt(top[j], j).toFixed(1)}`;
|
|
175
|
+
for (let j = N - 1; j >= 0; j--)
|
|
176
|
+
d += `L${xs[j].toFixed(1)},${yAt(bottom[j], j).toFixed(1)}`;
|
|
177
|
+
node.setAttribute("d", `${d}Z`);
|
|
178
|
+
}
|
|
179
|
+
// The label sits where its layer is thickest, and holds its spot until another stretch is clearly thicker.
|
|
180
|
+
const text = labels.current.get(line.key);
|
|
181
|
+
if (!text)
|
|
182
|
+
return;
|
|
183
|
+
let best = 4;
|
|
184
|
+
for (let j = 4; j < N - 4; j++)
|
|
185
|
+
if (top[j] - bottom[j] > top[best] - bottom[best])
|
|
186
|
+
best = j;
|
|
187
|
+
const held = spots.current.get(line.key);
|
|
188
|
+
const j = held !== undefined && top[held] - bottom[held] >= (top[best] - bottom[best]) * .8 ? held : best;
|
|
189
|
+
spots.current.set(line.key, j);
|
|
190
|
+
const thickness = (top[j] - bottom[j]) / span * (height - PAD * 2) * s;
|
|
191
|
+
const room = text.getComputedTextLength?.() ?? 60;
|
|
192
|
+
const x = clamp(xs[j], room / 2 + 6, w - room / 2 - 6);
|
|
193
|
+
text.setAttribute("x", x.toFixed(1));
|
|
194
|
+
text.setAttribute("y", yAt((top[j] + bottom[j]) / 2, j).toFixed(1));
|
|
195
|
+
text.style.opacity = thickness >= 19 && room + 16 < w ? "1" : "0";
|
|
196
|
+
});
|
|
197
|
+
}, [grow, height]);
|
|
198
|
+
useLayoutEffect(() => {
|
|
199
|
+
const node = plot.current;
|
|
200
|
+
if (!node)
|
|
201
|
+
return;
|
|
202
|
+
const read = () => { width.current = node.clientWidth; setPlotWidth(node.clientWidth); paint(); };
|
|
203
|
+
read();
|
|
204
|
+
if (typeof ResizeObserver === "undefined")
|
|
205
|
+
return;
|
|
206
|
+
const observer = new ResizeObserver(read);
|
|
207
|
+
observer.observe(node);
|
|
208
|
+
return () => observer.disconnect();
|
|
209
|
+
}, [paint]);
|
|
210
|
+
// New data blends every layer from the shape on screen; a layer that arrives grows out of the one below it.
|
|
211
|
+
const morph = useRef(null);
|
|
212
|
+
useEffect(() => {
|
|
213
|
+
const moves = [];
|
|
214
|
+
for (const [key, to] of targets) {
|
|
215
|
+
const from = shapes.current.get(key);
|
|
216
|
+
if (!from || reduced) {
|
|
217
|
+
shapes.current.set(key, to);
|
|
218
|
+
continue;
|
|
219
|
+
}
|
|
220
|
+
moves.push({ key, from, to });
|
|
221
|
+
}
|
|
222
|
+
for (const key of [...shapes.current.keys()])
|
|
223
|
+
if (!targets.has(key))
|
|
224
|
+
shapes.current.delete(key);
|
|
225
|
+
morph.current?.stop();
|
|
226
|
+
if (!moves.length) {
|
|
227
|
+
paint();
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
morph.current = animate(0, 1, { ...settle, onUpdate: t => { for (const move of moves)
|
|
231
|
+
shapes.current.set(move.key, move.from.map((value, j) => value + (move.to[j] - value) * t)); paint(); }, onComplete: () => { for (const move of moves)
|
|
232
|
+
shapes.current.set(move.key, move.to); paint(); } });
|
|
233
|
+
return () => morph.current?.stop();
|
|
234
|
+
}, [paint, reduced, targets]);
|
|
235
|
+
// A hidden layer thins to nothing while the others close over it.
|
|
236
|
+
const hiddenKey = hidden.join("|");
|
|
237
|
+
useEffect(() => {
|
|
238
|
+
for (const line of series) {
|
|
239
|
+
const to = hidden.includes(line.key) ? 0 : 1;
|
|
240
|
+
const track = presence.current.get(line.key);
|
|
241
|
+
if (!track) {
|
|
242
|
+
presence.current.set(line.key, { value: to, stop: null });
|
|
243
|
+
continue;
|
|
244
|
+
}
|
|
245
|
+
if (track.value === to)
|
|
246
|
+
continue;
|
|
247
|
+
track.stop?.();
|
|
248
|
+
if (reduced) {
|
|
249
|
+
track.value = to;
|
|
250
|
+
track.stop = null;
|
|
251
|
+
continue;
|
|
252
|
+
}
|
|
253
|
+
const controls = animate(track.value, to, { ...settle, onUpdate: value => { track.value = value; paint(); } });
|
|
254
|
+
track.stop = () => controls.stop();
|
|
255
|
+
}
|
|
256
|
+
paint();
|
|
257
|
+
}, [hiddenKey, paint, reduced, series.length]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
258
|
+
// The first time the chart is seen, the stream swells out of a hairline along its centre.
|
|
259
|
+
useEffect(() => {
|
|
260
|
+
if (empty)
|
|
261
|
+
return;
|
|
262
|
+
if (reduced) {
|
|
263
|
+
grow.jump(1);
|
|
264
|
+
paint();
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
if (!inView || grow.get() >= 1)
|
|
268
|
+
return;
|
|
269
|
+
const controls = animate(grow, 1, { ...swell, onUpdate: paint });
|
|
270
|
+
return () => controls.stop();
|
|
271
|
+
}, [empty, grow, inView, paint, reduced]);
|
|
272
|
+
// The crosshair glides between points; the tooltip follows on its own spring and never leaves the plot.
|
|
273
|
+
const cursor = useMotionValue(0);
|
|
274
|
+
const cursorSpring = useSpring(cursor, follow);
|
|
275
|
+
const tipX = useMotionValue(0), tipY = useMotionValue(0);
|
|
276
|
+
const tipSpringX = useSpring(tipX, follow), tipSpringY = useSpring(tipY, follow);
|
|
277
|
+
const wasActive = useRef(false);
|
|
278
|
+
useLayoutEffect(() => {
|
|
279
|
+
const bubble = tip.current, w = width.current;
|
|
280
|
+
if (index === null || !bubble || !w) {
|
|
281
|
+
wasActive.current = false;
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
const x = last > 0 ? (index / last) * w : w / 2;
|
|
285
|
+
let y = hover?.y ?? null;
|
|
286
|
+
const f = frame.current;
|
|
287
|
+
if (y === null && f && activeKey) {
|
|
288
|
+
const i = series.findIndex(line => line.key === activeKey), j = Math.round((x / w) * (N - 1));
|
|
289
|
+
if (i >= 0)
|
|
290
|
+
y = f.toY((f.upper[i][j] + f.lower[i][j]) / 2);
|
|
291
|
+
}
|
|
292
|
+
const tw = bubble.offsetWidth, th = bubble.offsetHeight;
|
|
293
|
+
let left = x + GAP;
|
|
294
|
+
if (left + tw > w)
|
|
295
|
+
left = x - GAP - tw;
|
|
296
|
+
const top = clamp((y ?? height / 2) - th / 2, 0, Math.max(0, height - th));
|
|
297
|
+
const instant = !wasActive.current || reduced;
|
|
298
|
+
cursor.set(x);
|
|
299
|
+
tipX.set(clamp(left, 0, Math.max(0, w - tw)));
|
|
300
|
+
tipY.set(top);
|
|
301
|
+
if (instant) {
|
|
302
|
+
cursorSpring.jump(x);
|
|
303
|
+
tipSpringX.jump(tipX.get());
|
|
304
|
+
tipSpringY.jump(top);
|
|
305
|
+
}
|
|
306
|
+
wasActive.current = true;
|
|
307
|
+
});
|
|
308
|
+
const onActive = useRef(onActiveChange);
|
|
309
|
+
useLayoutEffect(() => { onActive.current = onActiveChange; });
|
|
310
|
+
useEffect(() => { onActive.current?.(index, activeKey); }, [index, activeKey]);
|
|
311
|
+
// The layer under the pointer, found in the frame that is on screen.
|
|
312
|
+
const read = (clientX, clientY) => {
|
|
313
|
+
const rect = plot.current?.getBoundingClientRect(), f = frame.current;
|
|
314
|
+
if (!rect?.width || empty)
|
|
315
|
+
return null;
|
|
316
|
+
const share = clamp((clientX - rect.left) / rect.width, 0, 1), y = clientY - rect.top;
|
|
317
|
+
const at = last > 0 ? Math.round(share * last) : 0, j = Math.round(share * (N - 1));
|
|
318
|
+
let key = null;
|
|
319
|
+
if (f)
|
|
320
|
+
series.forEach((line, i) => { if (hidden.includes(line.key))
|
|
321
|
+
return; const top = f.toY(f.upper[i][j]), bottom = f.toY(f.lower[i][j]); if (y >= top - 1 && y <= bottom + 1)
|
|
322
|
+
key = line.key; });
|
|
323
|
+
return { index: at, key, y };
|
|
324
|
+
};
|
|
325
|
+
const onPointerMove = (event) => { if (event.pointerType === "mouse" || event.buttons)
|
|
326
|
+
setHover(read(event.clientX, event.clientY)); };
|
|
327
|
+
const onPointerDown = (event) => { if (event.pointerType !== "mouse")
|
|
328
|
+
event.currentTarget.setPointerCapture?.(event.pointerId); setHover(read(event.clientX, event.clientY)); };
|
|
329
|
+
const onKeyDown = (event) => {
|
|
330
|
+
if (empty)
|
|
331
|
+
return;
|
|
332
|
+
if (event.key === "Escape" && hover) {
|
|
333
|
+
event.preventDefault();
|
|
334
|
+
setHover(null);
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
const from = index ?? last;
|
|
338
|
+
const keys = visible.map(line => line.key), at = activeKey ? keys.indexOf(activeKey) : -1;
|
|
339
|
+
const page = Math.max(1, Math.round(data.length / 6));
|
|
340
|
+
const moves = {
|
|
341
|
+
ArrowLeft: () => ({ index: clamp(from - 1, 0, last), key: activeKey, y: null }),
|
|
342
|
+
ArrowRight: () => ({ index: clamp(index === null ? last : from + 1, 0, last), key: activeKey, y: null }),
|
|
343
|
+
PageDown: () => ({ index: clamp(from - page, 0, last), key: activeKey, y: null }),
|
|
344
|
+
PageUp: () => ({ index: clamp(from + page, 0, last), key: activeKey, y: null }),
|
|
345
|
+
Home: () => ({ index: 0, key: activeKey, y: null }),
|
|
346
|
+
End: () => ({ index: last, key: activeKey, y: null }),
|
|
347
|
+
ArrowUp: () => ({ index: from, key: keys[at < 0 ? keys.length - 1 : Math.min(keys.length - 1, at + 1)] ?? null, y: null }),
|
|
348
|
+
ArrowDown: () => ({ index: from, key: keys[at < 0 ? 0 : Math.max(0, at - 1)] ?? null, y: null }),
|
|
349
|
+
};
|
|
350
|
+
const move = moves[event.key];
|
|
351
|
+
if (!move)
|
|
352
|
+
return;
|
|
353
|
+
event.preventDefault();
|
|
354
|
+
setHover(move());
|
|
355
|
+
};
|
|
356
|
+
const format = (value, line) => `${formatValue ? formatValue(value, line) : grouped.format(value)}${unit ? ` ${unit}` : ""}`;
|
|
357
|
+
const reading = index === null ? null : data[index];
|
|
358
|
+
const total = (datum) => visible.reduce((sum, line) => sum + Math.max(0, datum.values[line.key] ?? 0), 0);
|
|
359
|
+
const announce = reading ? `${reading.label}. ${activeKey ? `${series.find(line => line.key === activeKey)?.label} ${format(reading.values[activeKey] ?? 0, series.find(line => line.key === activeKey))}, ` : ""}${visible.length} layers, ${grouped.format(total(reading))}${unit ? ` ${unit}` : ""} in total` : "";
|
|
360
|
+
const summary = empty ? `${label}. ${emptyLabel}.` : `${label}, ${data[0].label} to ${data[last].label}. ${visible.map(line => `${line.label}, latest ${format(data[last].values[line.key] ?? 0, line)}`).join(". ")}.`;
|
|
361
|
+
const picks = axisPicks(data, plotWidth);
|
|
362
|
+
const tipLeft = reduced ? tipX : tipSpringX, tipTop = reduced ? tipY : tipSpringY;
|
|
363
|
+
return _jsxs("figure", { ref: figure, className: [styles.figure, className].filter(Boolean).join(" "), "aria-label": label, children: [legend && series.length > 1 && _jsx("div", { className: styles.legend, role: "group", "aria-label": `${label} layers`, onPointerLeave: () => setLegendKey(null), children: series.map((line, at) => _jsxs("button", { type: "button", className: styles.toggle, "aria-pressed": !hidden.includes(line.key), "data-active": activeKey === line.key || undefined, "data-dim": (activeKey !== null && activeKey !== line.key) || undefined, onClick: () => toggle(line.key), onPointerEnter: event => { if (event.pointerType === "mouse")
|
|
364
|
+
setLegendKey(line.key); }, onFocus: event => { if (event.currentTarget.matches(":focus-visible"))
|
|
365
|
+
setLegendKey(line.key); }, onBlur: () => setLegendKey(null), style: { "--series": colorOf(line, at) }, children: [_jsx("span", { className: styles.swatch, "aria-hidden": "true" }), _jsx("span", { className: styles.toggleLabel, children: line.label })] }, line.key)) }), _jsxs("div", { className: styles.chart, "data-active": index !== null || undefined, "data-layer": activeKey ? true : undefined, children: [_jsxs("div", { ref: plot, className: styles.plot, style: { height }, role: "group", tabIndex: empty ? -1 : 0, "aria-roledescription": "stream graph", "aria-label": `${label}. Use left and right arrows to move through time, up and down to change layer.`, onPointerMove: onPointerMove, onPointerDown: onPointerDown, onPointerUp: event => { if (event.pointerType !== "mouse")
|
|
366
|
+
setHover(null); }, onPointerCancel: () => setHover(null), onPointerLeave: event => { if (event.pointerType === "mouse")
|
|
367
|
+
setHover(null); }, onKeyDown: onKeyDown, onBlur: () => setHover(null), onFocus: event => { if (event.currentTarget.matches(":focus-visible") && !empty)
|
|
368
|
+
setHover(current => current ?? { index: last, key: null, y: null }); }, children: [_jsxs("svg", { className: styles.svg, width: "100%", height: height, "aria-hidden": "true", focusable: "false", children: [series.map((line, at) => _jsx("path", { ref: node => { if (node)
|
|
369
|
+
paths.current.set(line.key, node);
|
|
370
|
+
else
|
|
371
|
+
paths.current.delete(line.key); }, className: styles.layer, "data-active": activeKey === line.key || undefined, "data-custom": line.color ? true : undefined, style: { "--series": colorOf(line, at) } }, line.key)), _jsx(motion.line, { className: styles.crosshair, x1: 0, x2: 0, y1: 0, y2: height, style: { x: reduced ? cursor : cursorSpring } }), directLabels && series.map(line => _jsx("text", { ref: node => { if (node)
|
|
372
|
+
labels.current.set(line.key, node);
|
|
373
|
+
else
|
|
374
|
+
labels.current.delete(line.key); }, className: styles.label, "data-active": activeKey === line.key || undefined, "data-custom": line.color ? true : undefined, "data-hidden": hidden.includes(line.key) || undefined, "data-dim": (activeKey !== null && activeKey !== line.key) || undefined, textAnchor: "middle", dominantBaseline: "central", children: line.label }, line.key))] }), _jsxs(motion.div, { ref: tip, className: styles.tooltip, style: { x: tipLeft, y: tipTop }, "aria-hidden": "true", children: [_jsx("p", { className: styles.tipTitle, children: reading?.label ?? "" }), stackOrder(series.length, offset).reverse().map(at => series[at]).filter(line => !hidden.includes(line.key)).map(line => _jsxs("p", { className: styles.tipRow, "data-active": activeKey === line.key || undefined, "data-dim": (activeKey !== null && activeKey !== line.key) || undefined, style: { "--series": colorOf(line, series.indexOf(line)) }, children: [_jsx("span", { className: styles.tipSwatch }), _jsx("span", { className: styles.tipName, children: line.label }), _jsx("span", { className: styles.tipValue, children: reading ? format(reading.values[line.key] ?? 0, line) : "" })] }, line.key))] }), (empty || visible.length === 0) && _jsx("p", { className: styles.message, children: empty ? emptyLabel : "Choose a layer to show" })] }), _jsx("div", { className: styles.axis, "aria-hidden": "true", children: picks.map(at => { const share = last > 0 ? at / last : .5; const item = data[at]; return _jsx("span", { className: styles.axisLabel, style: { left: `${share * 100}%`, translate: `${-share * 100}% 0` }, children: item.axisLabel }, `${item.key}`); }) })] }), _jsx("p", { className: styles.srOnly, "aria-live": "polite", "aria-atomic": "true", children: announce }), _jsx("p", { className: styles.srOnly, children: summary }), !empty && _jsx("div", { className: styles.srOnly, children: _jsxs("table", { children: [_jsx("caption", { children: label }), _jsx("thead", { children: _jsxs("tr", { children: [_jsx("th", { scope: "col", children: categoryLabel }), series.map(line => _jsx("th", { scope: "col", children: line.label }, line.key))] }) }), _jsx("tbody", { children: data.map(item => _jsxs("tr", { children: [_jsx("th", { scope: "row", children: item.label }), series.map(line => _jsx("td", { children: format(item.values[line.key] ?? 0, line) }, line.key))] }, item.key)) })] }) })] });
|
|
375
|
+
}
|
|
376
|
+
export default Streamgraph;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
.figure { display: grid; min-width: 0; gap: var(--space-inset); margin: 0; color: var(--foreground); font-family: var(--font-body); letter-spacing: var(--tracking-body); }
|
|
2
|
+
|
|
3
|
+
/* Series colors (--series-1 to --series-4) come from registry/foundation.css. */
|
|
4
|
+
|
|
5
|
+
/* Layer toggles: a rect swatch mirrors the area it names. Hover or focus previews that layer in the plot. */
|
|
6
|
+
.legend { display: flex; flex-wrap: wrap; gap: 2px 4px; min-width: 0; margin-inline: -8px; }
|
|
7
|
+
.toggle { display: inline-flex; min-height: 32px; align-items: center; gap: 8px; padding: 0 10px 0 8px; border: 0; border-radius: var(--radius-full); corner-shape: round; background: transparent; color: var(--text-secondary); cursor: pointer; font: inherit; font-size: var(--text-sm); line-height: var(--leading-body); -webkit-tap-highlight-color: transparent; transition: color var(--duration-fast) var(--ease-standard), background-color var(--duration-fast) var(--ease-standard), opacity var(--duration-fast) var(--ease-standard), transform var(--duration-instant) var(--ease-standard); }
|
|
8
|
+
.toggle:active { transform: scale(.97); }
|
|
9
|
+
.toggle[data-active] { color: var(--foreground); background: color-mix(in oklab, var(--foreground) 5%, transparent); }
|
|
10
|
+
.toggle[data-dim] { opacity: .56; }
|
|
11
|
+
.toggle[aria-pressed="false"] { color: var(--text-muted); }
|
|
12
|
+
.toggleLabel { white-space: nowrap; }
|
|
13
|
+
.toggle[aria-pressed="false"] .toggleLabel { opacity: .72; text-decoration: line-through; text-decoration-color: color-mix(in oklab, currentColor 55%, transparent); }
|
|
14
|
+
.swatch { width: 10px; height: 10px; flex: none; border-radius: var(--radius-detail); corner-shape: var(--corner-shape); background: var(--series); transition: transform var(--duration-standard) var(--ease-spring), opacity var(--duration-fast) var(--ease-standard); }
|
|
15
|
+
.toggle[aria-pressed="false"] .swatch { opacity: .35; transform: scale(.6); }
|
|
16
|
+
@media (hover: hover) and (pointer: fine) { .toggle:hover { color: var(--foreground); } }
|
|
17
|
+
|
|
18
|
+
.chart { position: relative; display: grid; grid-template-rows: auto 26px; min-width: 0; }
|
|
19
|
+
.plot { position: relative; min-width: 0; outline: none; touch-action: pan-y; user-select: none; -webkit-user-select: none; -webkit-tap-highlight-color: transparent; cursor: crosshair; }
|
|
20
|
+
.svg { display: block; overflow: visible; }
|
|
21
|
+
|
|
22
|
+
/* A 2px surface stroke is the gap between neighbouring layers. */
|
|
23
|
+
.layer { fill: color-mix(in oklab, var(--series) 82%, var(--surface)); stroke: var(--surface); stroke-width: 2; stroke-linejoin: round; transition: opacity var(--duration-fast) var(--ease-standard), fill var(--duration-standard) var(--ease-standard); }
|
|
24
|
+
.chart[data-layer] .layer:not([data-active]) { opacity: .34; }
|
|
25
|
+
.layer[data-active] { fill: var(--series); }
|
|
26
|
+
|
|
27
|
+
.crosshair { stroke: var(--foreground); stroke-opacity: .5; stroke-width: 1; shape-rendering: crispEdges; opacity: 0; transition: opacity var(--duration-instant) var(--ease-standard); pointer-events: none; }
|
|
28
|
+
.chart[data-active] .crosshair { opacity: 1; }
|
|
29
|
+
|
|
30
|
+
.label { fill: var(--foreground); font-size: var(--text-xs); font-weight: 500; paint-order: stroke; stroke: var(--surface); stroke-width: 3px; stroke-opacity: .85; stroke-linejoin: round; pointer-events: none; transition: opacity var(--duration-standard) var(--ease-standard), fill var(--duration-fast) var(--ease-standard); }
|
|
31
|
+
.label[data-hidden] { opacity: 0 !important; }
|
|
32
|
+
.chart[data-layer] .label[data-dim] { opacity: 0 !important; }
|
|
33
|
+
|
|
34
|
+
.tooltip { position: absolute; top: 0; left: 0; z-index: 1; display: grid; min-width: 156px; max-width: min(256px, 100%); gap: 4px; padding: 10px 12px; border: 1px solid var(--border); border-radius: var(--radius-control); corner-shape: var(--corner-shape); background: var(--surface-raised); box-shadow: var(--shadow-floating); opacity: 0; pointer-events: none; scale: .96; transition: opacity var(--duration-fast) var(--ease-standard), scale var(--duration-fast) var(--ease-standard); }
|
|
35
|
+
.chart[data-active] .tooltip { opacity: 1; scale: 1; }
|
|
36
|
+
.tipTitle { margin: 0 0 2px; color: var(--text-secondary); font-size: var(--text-xs); line-height: var(--leading-body); white-space: nowrap; font-variant-numeric: tabular-nums; }
|
|
37
|
+
.tipRow { display: flex; min-width: 0; align-items: center; gap: 8px; margin: 0; font-size: var(--text-sm); line-height: var(--leading-body); transition: opacity var(--duration-fast) var(--ease-standard); }
|
|
38
|
+
.tipRow[data-dim] { opacity: .5; }
|
|
39
|
+
.tipSwatch { width: 10px; height: 2.5px; flex: none; border-radius: var(--radius-full); corner-shape: round; background: var(--series); }
|
|
40
|
+
.tipName { min-width: 0; flex: 1; overflow: hidden; color: var(--text-secondary); text-overflow: ellipsis; white-space: nowrap; }
|
|
41
|
+
.tipRow[data-active] .tipName { color: var(--foreground); }
|
|
42
|
+
.tipValue { flex: none; margin-left: 8px; color: var(--foreground); font-weight: 500; font-variant-numeric: tabular-nums; white-space: nowrap; }
|
|
43
|
+
|
|
44
|
+
.message { position: absolute; inset: 0; display: grid; place-items: center; margin: 0; color: var(--text-muted); font-size: var(--text-sm); pointer-events: none; }
|
|
45
|
+
|
|
46
|
+
.axis { position: relative; min-width: 0; }
|
|
47
|
+
.axisLabel { position: absolute; top: 8px; color: var(--text-muted); font-size: var(--text-xs); line-height: 1; white-space: nowrap; font-variant-numeric: tabular-nums; transition: opacity var(--duration-standard) var(--ease-standard); }
|
|
48
|
+
/* New labels fade in while the stream morphs to a new range instead of popping in a single frame. */
|
|
49
|
+
@starting-style { .axisLabel { opacity: 0; } }
|
|
50
|
+
|
|
51
|
+
.srOnly { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }
|
|
52
|
+
|
|
53
|
+
@media (prefers-reduced-motion: reduce) {
|
|
54
|
+
.toggle, .swatch, .layer, .crosshair, .label, .tooltip, .tipRow, .axisLabel { transition: none; }
|
|
55
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
export interface SwipeAction {
|
|
3
|
+
label: string;
|
|
4
|
+
icon: ReactNode;
|
|
5
|
+
/** Fill of the revealed action. Pair `danger` with a label that names the destructive result. */
|
|
6
|
+
tone?: "neutral" | "accent" | "danger";
|
|
7
|
+
/** Runs for a full swipe, a tap on the revealed action, or the More actions menu. Remove the item here unless `keepRow` is set. */
|
|
8
|
+
onSelect: () => void;
|
|
9
|
+
/** The row springs home after the action instead of sliding away and collapsing, for actions such as Mark as unread. */
|
|
10
|
+
keepRow?: boolean;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* A list whose rows reveal actions on a horizontal swipe, the way a mail inbox does. Use it for short lists where people triage items quickly.
|
|
14
|
+
* Only one row stays open at a time; touching anywhere else or pressing Escape puts it away. Every row also has a More actions menu with the same
|
|
15
|
+
* actions, so keyboard and screen reader users never need the gesture.
|
|
16
|
+
*/
|
|
17
|
+
export interface SwipeActionsProps {
|
|
18
|
+
label: string;
|
|
19
|
+
children: ReactNode;
|
|
20
|
+
className?: string;
|
|
21
|
+
}
|
|
22
|
+
/** One row. `leading` actions sit under the left edge and `trailing` actions under the right; the outermost action on each side commits on a full swipe. */
|
|
23
|
+
export interface SwipeActionsRowProps {
|
|
24
|
+
/** Names the row in its menu button, for example the message subject. */
|
|
25
|
+
label: string;
|
|
26
|
+
leading?: SwipeAction[];
|
|
27
|
+
trailing?: SwipeAction[];
|
|
28
|
+
/** Lets a long swipe commit the outermost action without a tap. On by default. */
|
|
29
|
+
fullSwipe?: boolean;
|
|
30
|
+
children: ReactNode;
|
|
31
|
+
className?: string;
|
|
32
|
+
}
|
|
33
|
+
export declare function SwipeActions({ label, children, className }: SwipeActionsProps): import("react").JSX.Element;
|
|
34
|
+
export declare function SwipeActionsRow({ label, leading, trailing, fullSwipe, children, className }: SwipeActionsRowProps): import("react").JSX.Element;
|
|
35
|
+
export default SwipeActions;
|