@tulipfarm/sprout 0.3.0 → 0.4.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/README.md +45 -40
- package/dist/components/accordion.d.ts +2 -2
- package/dist/components/accordion.js +9 -9
- package/dist/components/agent-fanout.d.ts +2 -2
- package/dist/components/agent-fanout.js +65 -87
- package/dist/components/alert-dialog.d.ts +6 -6
- package/dist/components/alert-dialog.js +20 -20
- package/dist/components/alert.d.ts +6 -6
- package/dist/components/alert.js +13 -13
- package/dist/components/aspect-ratio.d.ts +2 -2
- package/dist/components/aspect-ratio.js +2 -2
- package/dist/components/attachment.d.ts +17 -17
- package/dist/components/attachment.js +82 -115
- package/dist/components/avatar.d.ts +10 -10
- package/dist/components/avatar.js +14 -14
- package/dist/components/badge.d.ts +4 -4
- package/dist/components/badge.js +18 -18
- package/dist/components/board.d.ts +107 -0
- package/dist/components/board.js +640 -0
- package/dist/components/brand-icon.d.ts +6 -6
- package/dist/components/brand-icon.js +55 -63
- package/dist/components/breadcrumb.d.ts +11 -11
- package/dist/components/breadcrumb.js +16 -18
- package/dist/components/button-group.d.ts +6 -6
- package/dist/components/button-group.js +14 -14
- package/dist/components/button.d.ts +11 -11
- package/dist/components/button.js +46 -56
- package/dist/components/calendar.d.ts +5 -5
- package/dist/components/calendar.js +44 -53
- package/dist/components/canvas.d.ts +4 -4
- package/dist/components/canvas.js +39 -46
- package/dist/components/card.d.ts +6 -6
- package/dist/components/card.js +17 -17
- package/dist/components/carousel.d.ts +7 -7
- package/dist/components/carousel.js +57 -75
- package/dist/components/chart.d.ts +5 -112
- package/dist/components/chart.js +5 -172
- package/dist/components/chat-confirmation.d.ts +13 -11
- package/dist/components/chat-confirmation.js +32 -38
- package/dist/components/chat-input.d.ts +4 -4
- package/dist/components/chat-input.js +74 -81
- package/dist/components/chat-plan.d.ts +2 -2
- package/dist/components/chat-plan.js +15 -20
- package/dist/components/chat-sources.d.ts +8 -8
- package/dist/components/chat-sources.js +18 -27
- package/dist/components/checkbox.d.ts +1 -1
- package/dist/components/checkbox.js +9 -9
- package/dist/components/choice-list.d.ts +2 -2
- package/dist/components/choice-list.js +16 -18
- package/dist/components/code-block.d.ts +5 -5
- package/dist/components/code-block.js +26 -26
- package/dist/components/collapsible.d.ts +1 -1
- package/dist/components/collapsible.js +7 -11
- package/dist/components/combobox.d.ts +3 -3
- package/dist/components/combobox.js +24 -24
- package/dist/components/command.d.ts +13 -13
- package/dist/components/command.js +61 -73
- package/dist/components/context-menu.d.ts +5 -5
- package/dist/components/context-menu.js +22 -22
- package/dist/components/dialog.d.ts +6 -6
- package/dist/components/dialog.js +23 -28
- package/dist/components/direction.d.ts +1 -1
- package/dist/components/direction.js +1 -1
- package/dist/components/dropdown-menu.d.ts +5 -5
- package/dist/components/dropdown-menu.js +20 -20
- package/dist/components/field.d.ts +11 -11
- package/dist/components/field.js +41 -61
- package/dist/components/file-explorer.d.ts +2 -2
- package/dist/components/file-explorer.js +89 -105
- package/dist/components/file-viewer.d.ts +10 -10
- package/dist/components/file-viewer.js +112 -140
- package/dist/components/form.d.ts +7 -7
- package/dist/components/form.js +24 -28
- package/dist/components/glass-banner.d.ts +3 -3
- package/dist/components/glass-banner.js +21 -24
- package/dist/components/gradient-banner.d.ts +5 -5
- package/dist/components/gradient-banner.js +13 -22
- package/dist/components/hover-card.d.ts +4 -4
- package/dist/components/hover-card.js +6 -6
- package/dist/components/input-group.d.ts +11 -11
- package/dist/components/input-group.js +22 -36
- package/dist/components/input-otp.d.ts +10 -10
- package/dist/components/input-otp.js +13 -15
- package/dist/components/input.d.ts +1 -1
- package/dist/components/input.js +7 -7
- package/dist/components/kbd.d.ts +3 -3
- package/dist/components/kbd.js +5 -5
- package/dist/components/label.d.ts +2 -2
- package/dist/components/label.js +2 -2
- package/dist/components/mention-list.d.ts +3 -3
- package/dist/components/mention-list.js +10 -10
- package/dist/components/menubar.d.ts +6 -6
- package/dist/components/menubar.js +22 -22
- package/dist/components/message-actions.d.ts +2 -2
- package/dist/components/message-actions.js +29 -31
- package/dist/components/message-response.d.ts +3 -3
- package/dist/components/message-response.js +18 -18
- package/dist/components/message-scroller.d.ts +12 -12
- package/dist/components/message-scroller.js +44 -56
- package/dist/components/message.d.ts +11 -11
- package/dist/components/message.js +25 -25
- package/dist/components/navigation-menu.d.ts +2 -2
- package/dist/components/navigation-menu.js +15 -15
- package/dist/components/notice-banner.d.ts +2 -2
- package/dist/components/notice-banner.js +2 -2
- package/dist/components/page-content.d.ts +2 -2
- package/dist/components/page-content.js +2 -2
- package/dist/components/page-header.d.ts +1 -1
- package/dist/components/page-header.js +16 -16
- package/dist/components/pagination.d.ts +9 -9
- package/dist/components/pagination.js +17 -17
- package/dist/components/plan-outline.d.ts +2 -2
- package/dist/components/plan-outline.js +43 -60
- package/dist/components/popover.d.ts +6 -6
- package/dist/components/popover.js +15 -15
- package/dist/components/presence-cursor.d.ts +2 -2
- package/dist/components/presence-cursor.js +3 -3
- package/dist/components/progress.d.ts +1 -1
- package/dist/components/progress.js +8 -8
- package/dist/components/prompt-list.d.ts +3 -3
- package/dist/components/prompt-list.js +6 -6
- package/dist/components/questionnaire.d.ts +19 -19
- package/dist/components/questionnaire.js +183 -242
- package/dist/components/radio-group.d.ts +2 -2
- package/dist/components/radio-group.js +6 -6
- package/dist/components/reasoning-effort.d.ts +3 -3
- package/dist/components/reasoning-effort.js +9 -12
- package/dist/components/record-list.d.ts +66 -0
- package/dist/components/record-list.js +151 -0
- package/dist/components/resizable.d.ts +5 -5
- package/dist/components/resizable.js +76 -102
- package/dist/components/result-card.d.ts +10 -5
- package/dist/components/result-card.js +42 -53
- package/dist/components/result-notice.d.ts +18 -0
- package/dist/components/result-notice.js +29 -0
- package/dist/components/review-card.d.ts +6 -6
- package/dist/components/review-card.js +18 -18
- package/dist/components/rich-text-editor.d.ts +2 -2
- package/dist/components/rich-text-editor.js +56 -72
- package/dist/components/schema-diagram.d.ts +3 -3
- package/dist/components/schema-diagram.js +39 -53
- package/dist/components/scroll-area.d.ts +1 -1
- package/dist/components/scroll-area.js +10 -14
- package/dist/components/search-dialog.d.ts +6 -6
- package/dist/components/search-dialog.js +16 -16
- package/dist/components/search-field.d.ts +1 -1
- package/dist/components/search-field.js +11 -11
- package/dist/components/select.d.ts +3 -3
- package/dist/components/select.js +20 -20
- package/dist/components/selection-slot.d.ts +2 -2
- package/dist/components/selection-slot.js +6 -9
- package/dist/components/separator.d.ts +15 -15
- package/dist/components/separator.js +22 -27
- package/dist/components/settings-list.d.ts +3 -3
- package/dist/components/settings-list.js +5 -5
- package/dist/components/sheet.d.ts +6 -6
- package/dist/components/sheet.js +27 -27
- package/dist/components/sidebar.d.ts +22 -22
- package/dist/components/sidebar.js +57 -58
- package/dist/components/skeleton.d.ts +5 -5
- package/dist/components/skeleton.js +14 -21
- package/dist/components/slider.d.ts +2 -2
- package/dist/components/slider.js +6 -6
- package/dist/components/spinner.d.ts +5 -5
- package/dist/components/spinner.js +22 -26
- package/dist/components/sql-viewer.d.ts +14 -14
- package/dist/components/sql-viewer.js +99 -128
- package/dist/components/state-panel-samples.d.ts +2 -2
- package/dist/components/state-panel-samples.js +209 -245
- package/dist/components/state-panel.d.ts +2 -35
- package/dist/components/state-panel.js +2 -29
- package/dist/components/state-sample.d.ts +7 -7
- package/dist/components/state-sample.js +18 -31
- package/dist/components/switch.d.ts +1 -1
- package/dist/components/switch.js +7 -7
- package/dist/components/table-nuqs.d.ts +2 -2
- package/dist/components/table-nuqs.js +11 -12
- package/dist/components/table.d.ts +4 -4
- package/dist/components/table.js +166 -235
- package/dist/components/tabs.d.ts +2 -2
- package/dist/components/tabs.js +12 -12
- package/dist/components/textarea.d.ts +2 -2
- package/dist/components/textarea.js +6 -8
- package/dist/components/timeline.d.ts +3 -0
- package/dist/components/timeline.js +3 -0
- package/dist/components/toast.d.ts +2 -2
- package/dist/components/toast.js +19 -25
- package/dist/components/toggle-group.d.ts +2 -2
- package/dist/components/toggle-group.js +18 -20
- package/dist/components/toggle.d.ts +2 -2
- package/dist/components/toggle.js +8 -8
- package/dist/components/tool-activity.d.ts +1 -1
- package/dist/components/tool-activity.js +83 -110
- package/dist/components/toolbar-status.d.ts +2 -2
- package/dist/components/toolbar-status.js +7 -7
- package/dist/components/tooltip.d.ts +5 -5
- package/dist/components/tooltip.js +12 -11
- package/dist/components/typed-text.d.ts +2 -2
- package/dist/components/typed-text.js +10 -12
- package/dist/components/view-switcher.d.ts +51 -0
- package/dist/components/view-switcher.js +114 -0
- package/dist/index.d.ts +98 -93
- package/dist/index.js +98 -93
- package/dist/internal/chart-kit.d.ts +142 -0
- package/dist/internal/chart-kit.js +200 -0
- package/dist/internal/charts/bars.d.ts +61 -0
- package/dist/internal/charts/bars.js +86 -0
- package/dist/internal/charts/lines.d.ts +67 -0
- package/dist/internal/charts/lines.js +70 -0
- package/dist/internal/charts/more.d.ts +77 -0
- package/dist/internal/charts/more.js +68 -0
- package/dist/internal/charts/parts.d.ts +54 -0
- package/dist/internal/charts/parts.js +82 -0
- package/dist/internal/charts/timeline-dates.d.ts +38 -0
- package/dist/internal/charts/timeline-dates.js +113 -0
- package/dist/internal/charts/timeline-window.d.ts +31 -0
- package/dist/internal/charts/timeline-window.js +26 -0
- package/dist/internal/charts/timeline.d.ts +72 -0
- package/dist/internal/charts/timeline.js +287 -0
- package/dist/internal/classes.d.ts +1 -1
- package/dist/internal/classes.js +7 -9
- package/dist/internal/code-tokens.d.ts +2 -2
- package/dist/internal/code-tokens.js +12 -12
- package/dist/internal/fieldset.d.ts +1 -1
- package/dist/internal/fieldset.js +6 -6
- package/dist/internal/file-kind.d.ts +1 -1
- package/dist/internal/file-kind.js +43 -51
- package/dist/internal/flow.d.ts +4 -4
- package/dist/internal/flow.js +20 -23
- package/dist/internal/languages.js +999 -999
- package/dist/internal/message-scroller.d.ts +3 -3
- package/dist/internal/message-scroller.js +64 -112
- package/dist/internal/pressable.d.ts +1 -1
- package/dist/internal/pressable.js +7 -7
- package/dist/internal/state-panel.d.ts +35 -0
- package/dist/internal/state-panel.js +22 -0
- package/dist/internal/syntax.d.ts +2 -2
- package/dist/internal/syntax.js +36 -47
- package/dist/styles/accordion.css +3 -5
- package/dist/styles/agent-fanout.css +64 -60
- package/dist/styles/alert-dialog.css +12 -26
- package/dist/styles/alert.css +10 -15
- package/dist/styles/attachment.css +51 -93
- package/dist/styles/avatar.css +17 -24
- package/dist/styles/badge.css +11 -36
- package/dist/styles/board.css +571 -0
- package/dist/styles/brand-icon.css +7 -8
- package/dist/styles/breadcrumb.css +4 -7
- package/dist/styles/button-group.css +8 -14
- package/dist/styles/button.css +22 -27
- package/dist/styles/calendar.css +24 -36
- package/dist/styles/canvas.css +2 -3
- package/dist/styles/card.css +7 -23
- package/dist/styles/carousel.css +11 -12
- package/dist/styles/chart.css +215 -31
- package/dist/styles/chat-input.css +15 -39
- package/dist/styles/chat-plan.css +3 -4
- package/dist/styles/chat-sources.css +3 -6
- package/dist/styles/checkbox.css +11 -25
- package/dist/styles/choice-list.css +17 -31
- package/dist/styles/code-block.css +11 -25
- package/dist/styles/collapsible.css +10 -15
- package/dist/styles/combobox.css +11 -15
- package/dist/styles/command.css +5 -8
- package/dist/styles/context-menu.css +2 -2
- package/dist/styles/dialog.css +5 -16
- package/dist/styles/dropdown-menu.css +2 -2
- package/dist/styles/field.css +15 -21
- package/dist/styles/file-explorer.css +19 -33
- package/dist/styles/file-viewer.css +13 -17
- package/dist/styles/form.css +3 -3
- package/dist/styles/glass-banner.css +12 -47
- package/dist/styles/gradient-banner.css +17 -35
- package/dist/styles/hover-card.css +9 -15
- package/dist/styles/index.css +116 -108
- package/dist/styles/input-group.css +25 -41
- package/dist/styles/input-otp.css +5 -6
- package/dist/styles/input.css +5 -5
- package/dist/styles/kbd.css +1 -1
- package/dist/styles/label.css +9 -30
- package/dist/styles/mention-list.css +6 -8
- package/dist/styles/menubar.css +5 -6
- package/dist/styles/message-actions.css +1 -1
- package/dist/styles/message-response.css +3 -5
- package/dist/styles/message-scroller.css +14 -21
- package/dist/styles/message.css +23 -60
- package/dist/styles/navigation-menu.css +14 -29
- package/dist/styles/notice-banner.css +1 -2
- package/dist/styles/page-header.css +7 -10
- package/dist/styles/pagination.css +1 -4
- package/dist/styles/plan-outline.css +20 -37
- package/dist/styles/popover.css +10 -19
- package/dist/styles/presence-cursor.css +1 -2
- package/dist/styles/progress.css +2 -4
- package/dist/styles/prompt-list.css +6 -17
- package/dist/styles/questionnaire.css +16 -40
- package/dist/styles/radio-group.css +12 -23
- package/dist/styles/reasoning-effort.css +8 -15
- package/dist/styles/record-list.css +243 -0
- package/dist/styles/resizable.css +10 -11
- package/dist/styles/result-card.css +17 -3
- package/dist/styles/result-notice.css +80 -0
- package/dist/styles/review-card.css +2 -3
- package/dist/styles/rich-text-editor.css +1 -2
- package/dist/styles/schema-diagram.css +12 -17
- package/dist/styles/scroll-area.css +8 -10
- package/dist/styles/search-dialog.css +1 -5
- package/dist/styles/search-field.css +1 -2
- package/dist/styles/select.css +7 -7
- package/dist/styles/selection-slot.css +1 -2
- package/dist/styles/separator.css +7 -8
- package/dist/styles/settings-list.css +1 -2
- package/dist/styles/sheet.css +40 -73
- package/dist/styles/sidebar.css +17 -21
- package/dist/styles/skeleton.css +9 -11
- package/dist/styles/slider.css +11 -21
- package/dist/styles/spinner.css +2 -2
- package/dist/styles/sql-viewer.css +29 -29
- package/dist/styles/state-panel.css +8 -9
- package/dist/styles/state-sample.css +20 -33
- package/dist/styles/switch.css +11 -18
- package/dist/styles/table.css +51 -77
- package/dist/styles/tabs.css +12 -16
- package/dist/styles/textarea.css +3 -3
- package/dist/styles/themes/chatgpt.css +11 -23
- package/dist/styles/themes/claude.css +12 -24
- package/dist/styles/themes/cloudflare.css +11 -23
- package/dist/styles/themes/discord.css +11 -23
- package/dist/styles/themes/github.css +11 -23
- package/dist/styles/themes/grok.css +11 -23
- package/dist/styles/themes/linear.css +12 -25
- package/dist/styles/themes/neutral.css +11 -23
- package/dist/styles/themes/notion.css +11 -23
- package/dist/styles/themes/perplexity.css +11 -23
- package/dist/styles/themes/raycast.css +11 -23
- package/dist/styles/themes/spotify.css +11 -23
- package/dist/styles/themes/stripe.css +11 -23
- package/dist/styles/themes/supabase.css +11 -23
- package/dist/styles/themes/tulipfarm.css +9 -10
- package/dist/styles/themes/vercel.css +12 -25
- package/dist/styles/timeline.css +276 -0
- package/dist/styles/toast.css +13 -20
- package/dist/styles/toggle-group.css +17 -23
- package/dist/styles/toggle.css +8 -8
- package/dist/styles/tool-activity.css +7 -13
- package/dist/styles/toolbar-status.css +2 -7
- package/dist/styles/tooltip.css +15 -21
- package/dist/styles/view-switcher.css +279 -0
- package/package.json +21 -1
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { type ChartRow, type ChartSeries, type ReadyChartProps, type ValueFormatter } from '../chart-kit.js';
|
|
2
|
+
export interface HeatmapChartProps extends ReadyChartProps {
|
|
3
|
+
/** One row per cell, keyed by `x`, `y`, and `value`. */
|
|
4
|
+
rows: ChartRow[];
|
|
5
|
+
/** The row key naming each column, such as the hour. */
|
|
6
|
+
x: string;
|
|
7
|
+
/** The row key naming each row, such as the weekday. */
|
|
8
|
+
y: string;
|
|
9
|
+
/** The row key holding each cell's number. */
|
|
10
|
+
value: string;
|
|
11
|
+
/** Any CSS color for the darkest cells. Defaults to `--sprout-chart-1`. */
|
|
12
|
+
color?: string;
|
|
13
|
+
/** Words at each end of the scale. */
|
|
14
|
+
scaleLabels?: [low: string, high: string];
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* A grid of cells in five shades of one color, like activity by weekday and
|
|
18
|
+
* hour. Columns and rows keep the order they first appear in `rows`. It is a
|
|
19
|
+
* real table, so screen readers read every value.
|
|
20
|
+
*/
|
|
21
|
+
export declare function HeatmapChart({ rows, x, y, value, color, scaleLabels, title, valueFormatter, className, }: HeatmapChartProps): import("react").JSX.Element;
|
|
22
|
+
/** A series for `ScatterPlotChart`, with its own rows. */
|
|
23
|
+
export type ScatterSeries = ChartSeries & {
|
|
24
|
+
rows: ChartRow[];
|
|
25
|
+
};
|
|
26
|
+
export interface ScatterPlotChartProps extends ReadyChartProps {
|
|
27
|
+
series: ScatterSeries[];
|
|
28
|
+
/** The row key for the x axis. */
|
|
29
|
+
x: string;
|
|
30
|
+
/** The row key for the y axis. */
|
|
31
|
+
y: string;
|
|
32
|
+
/** A row key that sizes each dot, for a bubble chart. */
|
|
33
|
+
size?: string;
|
|
34
|
+
/** Axis names for the tooltip. Default to the keys. */
|
|
35
|
+
labels?: {
|
|
36
|
+
x?: string;
|
|
37
|
+
y?: string;
|
|
38
|
+
size?: string;
|
|
39
|
+
};
|
|
40
|
+
/** Formats x axis ticks. `valueFormatter` formats the y axis. */
|
|
41
|
+
xValueFormatter?: ValueFormatter;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* One dot per row on two number axes, 288px tall, to show how two numbers
|
|
45
|
+
* move together. Pass `size` for bubbles.
|
|
46
|
+
*/
|
|
47
|
+
export declare function ScatterPlotChart({ series, x, y, size, labels, title, valueFormatter, xValueFormatter, className, }: ScatterPlotChartProps): import("react").JSX.Element;
|
|
48
|
+
export interface SankeyChartProps extends ReadyChartProps {
|
|
49
|
+
/** Every stage, in any order. Links point at them by index. */
|
|
50
|
+
nodes: {
|
|
51
|
+
name: string;
|
|
52
|
+
}[];
|
|
53
|
+
/** Flows between nodes, by index into `nodes`. */
|
|
54
|
+
links: {
|
|
55
|
+
source: number;
|
|
56
|
+
target: number;
|
|
57
|
+
value: number;
|
|
58
|
+
}[];
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Flows between stages, with band width by value, 320px tall. The first five
|
|
62
|
+
* nodes get chart colors and the rest are muted; each band takes its
|
|
63
|
+
* source's color.
|
|
64
|
+
*/
|
|
65
|
+
export declare function SankeyChart({ nodes, links, title, valueFormatter, className, }: SankeyChartProps): import("react").JSX.Element;
|
|
66
|
+
export interface RadarScoreChartProps extends ReadyChartProps {
|
|
67
|
+
/** One row per axis, keyed by `category` and each series. */
|
|
68
|
+
rows: ChartRow[];
|
|
69
|
+
/** The row key naming each axis, such as "Speed" or "Price". */
|
|
70
|
+
category: string;
|
|
71
|
+
series: ChartSeries[];
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Scores on many axes at once, one filled shape per series, 320px tall.
|
|
75
|
+
* Good for comparing two or three options.
|
|
76
|
+
*/
|
|
77
|
+
export declare function RadarScoreChart({ rows, category, series, title, valueFormatter, className, }: RadarScoreChartProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useMemo } from 'react';
|
|
4
|
+
import { CartesianGrid, PolarAngleAxis, PolarGrid, Radar, RadarChart, Sankey, Scatter, ScatterChart, XAxis, YAxis, ZAxis, } from 'recharts';
|
|
5
|
+
import { cn } from '../classes.js';
|
|
6
|
+
import { ChartContainer, ChartLegend, ChartLegendContent, ChartTooltip, ChartTooltipContent, chartColor, chartShade, configLegend, formatNumber, partColor, quietAxis, seriesConfig, } from '../chart-kit.js';
|
|
7
|
+
const HEAT_STEPS = 5;
|
|
8
|
+
/**
|
|
9
|
+
* A grid of cells in five shades of one color, like activity by weekday and
|
|
10
|
+
* hour. Columns and rows keep the order they first appear in `rows`. It is a
|
|
11
|
+
* real table, so screen readers read every value.
|
|
12
|
+
*/
|
|
13
|
+
export function HeatmapChart({ rows, x, y, value, color = 'var(--sprout-chart-1)', scaleLabels = ['Less', 'More'], title = `${value} by ${y} and ${x}`, valueFormatter = formatNumber, className, }) {
|
|
14
|
+
const grid = useMemo(() => {
|
|
15
|
+
const columns = [...new Set(rows.map(row => String(row[x])))];
|
|
16
|
+
const lines = [...new Set(rows.map(row => String(row[y])))];
|
|
17
|
+
const cells = new Map(rows.map(row => [`${String(row[y])}\u0000${String(row[x])}`, Number(row[value]) || 0]));
|
|
18
|
+
const max = Math.max(0, ...cells.values());
|
|
19
|
+
return { columns, lines, cells, max };
|
|
20
|
+
}, [rows, x, y, value]);
|
|
21
|
+
const shade = (step) => (step === 0 ? 'var(--sprout-secondary)' : chartShade(color, step));
|
|
22
|
+
const step = (amount) => (grid.max > 0 && amount > 0 ? Math.ceil((amount / grid.max) * HEAT_STEPS) : 0);
|
|
23
|
+
return (_jsxs("div", { "data-slot": "chart", "data-variant": "heatmap", className: cn('sprout-chart-heatmap', className), children: [_jsx("div", { className: "sprout-chart-heatmap-scroll", children: _jsxs("table", { className: "sprout-chart-heatmap-table", style: { '--sprout-heat-columns': grid.columns.length }, children: [_jsx("caption", { className: "sprout-chart-sr-only", children: title }), _jsx("thead", { children: _jsxs("tr", { children: [_jsx("td", {}), grid.columns.map(column => (_jsx("th", { scope: "col", children: column }, column)))] }) }), _jsx("tbody", { children: grid.lines.map(line => (_jsxs("tr", { children: [_jsx("th", { scope: "row", children: line }), grid.columns.map(column => {
|
|
24
|
+
const amount = grid.cells.get(`${line}\u0000${column}`) ?? 0;
|
|
25
|
+
const text = valueFormatter(amount);
|
|
26
|
+
return (_jsx("td", { "data-tip": `${line} · ${column}: ${text}`, style: { backgroundColor: shade(step(amount)) }, children: _jsx("span", { className: "sprout-chart-sr-only", children: text }) }, column));
|
|
27
|
+
})] }, line))) })] }) }), _jsxs("div", { className: "sprout-chart-heatmap-scale", "aria-hidden": "true", children: [_jsx("span", { children: scaleLabels[0] }), Array.from({ length: HEAT_STEPS + 1 }, (_, index) => (_jsx("i", { style: { backgroundColor: shade(index) } }, index))), _jsx("span", { children: scaleLabels[1] })] })] }));
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* One dot per row on two number axes, 288px tall, to show how two numbers
|
|
31
|
+
* move together. Pass `size` for bubbles.
|
|
32
|
+
*/
|
|
33
|
+
export function ScatterPlotChart({ series, x, y, size, labels, title, valueFormatter = formatNumber, xValueFormatter = formatNumber, className, }) {
|
|
34
|
+
const config = useMemo(() => seriesConfig(series), [series]);
|
|
35
|
+
return (_jsx(ChartContainer, { "data-variant": "scatter", className: cn('sprout-chart-scatter', className), config: config, children: _jsxs(ScatterChart, { accessibilityLayer: true, title: title ?? `${labels?.y ?? y} by ${labels?.x ?? x}`, margin: { top: 8, right: 12, bottom: 0, left: 0 }, children: [_jsx(CartesianGrid, {}), _jsx(XAxis, { type: "number", dataKey: x, name: labels?.x ?? x, tickFormatter: xValueFormatter, ...quietAxis }), _jsx(YAxis, { type: "number", dataKey: y, name: labels?.y ?? y, width: "auto", tickFormatter: valueFormatter, ...quietAxis }), size ? (_jsx(ZAxis, { type: "number", dataKey: size, name: labels?.size ?? size, range: [40, 480] })) : (_jsx(ZAxis, { range: [48, 48] })), _jsx(ChartTooltip, { cursor: { strokeDasharray: '3 3' }, content: props => (_jsx(ChartTooltipContent, { active: props.active, hideLabel: true, payload: props.payload?.map(item => ({
|
|
36
|
+
...item,
|
|
37
|
+
value: typeof item.value === 'number'
|
|
38
|
+
? (item.dataKey === x ? xValueFormatter : valueFormatter)(item.value)
|
|
39
|
+
: item.value,
|
|
40
|
+
})) })) }), series.length > 1 ? _jsx(ChartLegend, { content: configLegend(config) }) : null, series.map(item => (_jsx(Scatter, { name: item.label, data: item.rows, fill: chartColor(item.key), fillOpacity: 0.8, isAnimationActive: false }, item.key)))] }) }));
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Flows between stages, with band width by value, 320px tall. The first five
|
|
44
|
+
* nodes get chart colors and the rest are muted; each band takes its
|
|
45
|
+
* source's color.
|
|
46
|
+
*/
|
|
47
|
+
export function SankeyChart({ nodes, links, title = 'Flow between stages', valueFormatter = formatNumber, className, }) {
|
|
48
|
+
const data = useMemo(() => ({ nodes, links }), [nodes, links]);
|
|
49
|
+
const config = useMemo(() => Object.fromEntries(nodes.map((node, index) => [node.name, { label: node.name, color: partColor(index) }])), [nodes]);
|
|
50
|
+
const node = ({ x, y, width, height, index, payload }) => {
|
|
51
|
+
// Recharts names a node's outgoing links `targetLinks`.
|
|
52
|
+
const last = !payload.targetLinks?.length;
|
|
53
|
+
return (_jsxs("g", { children: [_jsx("rect", { x: x, y: y, width: width, height: height, rx: 2, fill: partColor(index) }), _jsxs("text", { x: last ? x - 8 : x + width + 8, y: y + height / 2, textAnchor: last ? 'end' : 'start', dominantBaseline: "middle", className: "sprout-chart-sankey-label", children: [payload.name, _jsx("tspan", { className: "sprout-chart-sankey-value", dx: 6, children: valueFormatter(payload.value) })] })] }));
|
|
54
|
+
};
|
|
55
|
+
const link = ({ sourceX, targetX, sourceY, targetY, sourceControlX, targetControlX, linkWidth, payload, }) => {
|
|
56
|
+
const source = nodes.findIndex(item => item.name === payload.source.name);
|
|
57
|
+
return (_jsx("path", { d: `M${sourceX},${sourceY}C${sourceControlX},${sourceY} ${targetControlX},${targetY} ${targetX},${targetY}`, fill: "none", stroke: partColor(source), strokeOpacity: 0.28, strokeWidth: linkWidth, className: "sprout-chart-sankey-link" }));
|
|
58
|
+
};
|
|
59
|
+
return (_jsx(ChartContainer, { "data-variant": "sankey", className: cn('sprout-chart-sankey', className), config: config, role: "img", "aria-label": title, children: _jsx(Sankey, { data: data, nodePadding: 24, nodeWidth: 10, margin: { top: 8, right: 8, bottom: 8, left: 8 }, node: node, link: link, children: _jsx(ChartTooltip, { content: _jsx(ChartTooltipContent, { hideLabel: true, valueFormatter: valueFormatter }) }) }) }));
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Scores on many axes at once, one filled shape per series, 320px tall.
|
|
63
|
+
* Good for comparing two or three options.
|
|
64
|
+
*/
|
|
65
|
+
export function RadarScoreChart({ rows, category, series, title, valueFormatter = formatNumber, className, }) {
|
|
66
|
+
const config = useMemo(() => seriesConfig(series), [series]);
|
|
67
|
+
return (_jsx(ChartContainer, { "data-variant": "radar", className: cn('sprout-chart-radar', className), config: config, children: _jsxs(RadarChart, { data: rows, accessibilityLayer: true, title: title ?? `${series.map(item => item.label).join(', ')} by ${category}`, outerRadius: "72%", children: [_jsx(PolarGrid, {}), _jsx(PolarAngleAxis, { dataKey: category }), _jsx(ChartTooltip, { content: _jsx(ChartTooltipContent, { indicator: "line", valueFormatter: valueFormatter }) }), series.length > 1 ? _jsx(ChartLegend, { itemSorter: null, content: _jsx(ChartLegendContent, {}) }) : null, series.map(item => (_jsx(Radar, { dataKey: item.key, stroke: chartColor(item.key), strokeWidth: 2, fill: chartColor(item.key), fillOpacity: 0.18, dot: { r: 3, fillOpacity: 1 }, isAnimationActive: false }, item.key)))] }) }));
|
|
68
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { type ChartRow, type ReadyChartProps } from '../chart-kit.js';
|
|
2
|
+
interface PartChartProps extends ReadyChartProps {
|
|
3
|
+
/** One row per part, keyed by `category` and `value`. */
|
|
4
|
+
rows: ChartRow[];
|
|
5
|
+
/** The row key whose value names each part. */
|
|
6
|
+
category: string;
|
|
7
|
+
/** The row key holding each part's number. */
|
|
8
|
+
value: string;
|
|
9
|
+
}
|
|
10
|
+
export interface DonutChartProps extends PartChartProps {
|
|
11
|
+
/** `donut` leaves a hole with the total in it; `pie` fills the circle. */
|
|
12
|
+
variant?: 'donut' | 'pie';
|
|
13
|
+
/** Words under the total in the hole, such as "Visitors". */
|
|
14
|
+
totalLabel?: string;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Shares of a whole as slices, largest first, 288px tall with a legend. The
|
|
18
|
+
* top five parts get chart colors; the rest fold into one muted "N more".
|
|
19
|
+
*/
|
|
20
|
+
export declare function DonutChart({ rows, category, value, variant, totalLabel, title, valueFormatter, className, }: DonutChartProps): import("react").JSX.Element;
|
|
21
|
+
export interface StepFunnelChartProps extends PartChartProps {
|
|
22
|
+
/** Any CSS color for the first step. Later steps get lighter shades. */
|
|
23
|
+
color?: string;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Drop-off from step to step, as centered bars that narrow like a funnel.
|
|
27
|
+
* Step names sit on the left; each step's count and share of the first sit
|
|
28
|
+
* on the right. One 40px row per step.
|
|
29
|
+
*/
|
|
30
|
+
export declare function StepFunnelChart({ rows, category, value, color, title, valueFormatter, className, }: StepFunnelChartProps): import("react").JSX.Element;
|
|
31
|
+
export type TreemapChartProps = PartChartProps;
|
|
32
|
+
/**
|
|
33
|
+
* Boxes sized by value, largest top left, 288px tall. The top five get chart
|
|
34
|
+
* colors; the rest fold into one muted "N more" box.
|
|
35
|
+
*/
|
|
36
|
+
export declare function TreemapChart({ rows, category, value, title, valueFormatter, className, }: TreemapChartProps): import("react").JSX.Element;
|
|
37
|
+
export interface GaugeChartProps {
|
|
38
|
+
/** How far along, in the same unit as `max`. */
|
|
39
|
+
value: number;
|
|
40
|
+
/** The goal. Defaults to 100. */
|
|
41
|
+
max?: number;
|
|
42
|
+
/** Words under the number, such as "of quota". */
|
|
43
|
+
label?: string;
|
|
44
|
+
/** Names the gauge for screen readers, such as "Q3 quota attainment". */
|
|
45
|
+
title: string;
|
|
46
|
+
/** Any CSS color. Defaults to `--sprout-chart-1`. */
|
|
47
|
+
color?: string;
|
|
48
|
+
/** Formats the number in the middle. Defaults to percent of `max`. */
|
|
49
|
+
valueFormatter?: (value: number, max: number) => string;
|
|
50
|
+
className?: string;
|
|
51
|
+
}
|
|
52
|
+
/** Progress toward a goal as a half ring with the number in the middle. */
|
|
53
|
+
export declare function GaugeChart({ value, max, label, title, color, valueFormatter, className, }: GaugeChartProps): import("react").JSX.Element;
|
|
54
|
+
export {};
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useMemo } from 'react';
|
|
4
|
+
import { Bar, BarChart, Label, Pie, PieChart, PolarAngleAxis, PolarRadiusAxis, RadialBar, RadialBarChart, Treemap, XAxis, YAxis, } from 'recharts';
|
|
5
|
+
import { cn } from '../classes.js';
|
|
6
|
+
import { ChartContainer, ChartLegend, ChartTooltip, ChartTooltipContent, chartShade, configLegend, foldParts, formatNumber, partConfig, quietAxis, rowFillShape, tooltipCursor, } from '../chart-kit.js';
|
|
7
|
+
const centerText = (viewBox, main, sub, lift = 0) => {
|
|
8
|
+
// A Pie hands its Label the plot box; a polar axis hands it the circle.
|
|
9
|
+
const box = viewBox;
|
|
10
|
+
const cx = box?.cx ?? (box?.x ?? 0) + (box?.width ?? NaN) / 2;
|
|
11
|
+
const cy = box?.cy ?? (box?.y ?? 0) + (box?.height ?? NaN) / 2;
|
|
12
|
+
if (!Number.isFinite(cx) || !Number.isFinite(cy))
|
|
13
|
+
return null;
|
|
14
|
+
const y = cy - lift;
|
|
15
|
+
return (_jsxs("text", { x: cx, y: y, textAnchor: "middle", dominantBaseline: "middle", className: "sprout-chart-center", children: [_jsx("tspan", { x: cx, y: sub ? y - 8 : y, className: "sprout-chart-center-value", children: main }), sub ? (_jsx("tspan", { x: cx, y: y + 14, className: "sprout-chart-center-label", children: sub })) : null] }));
|
|
16
|
+
};
|
|
17
|
+
/**
|
|
18
|
+
* Shares of a whole as slices, largest first, 288px tall with a legend. The
|
|
19
|
+
* top five parts get chart colors; the rest fold into one muted "N more".
|
|
20
|
+
*/
|
|
21
|
+
export function DonutChart({ rows, category, value, variant = 'donut', totalLabel, title, valueFormatter = formatNumber, className, }) {
|
|
22
|
+
const parts = useMemo(() => foldParts(rows, category, value), [rows, category, value]);
|
|
23
|
+
const config = useMemo(() => partConfig(parts), [parts]);
|
|
24
|
+
const total = parts.reduce((sum, part) => sum + part.value, 0);
|
|
25
|
+
const donut = variant === 'donut';
|
|
26
|
+
return (_jsx(ChartContainer, { "data-variant": variant, className: cn('sprout-chart-donut', className), config: config, children: _jsxs(PieChart, { accessibilityLayer: true, title: title ?? `${value} by ${category}`, children: [_jsx(ChartTooltip, { content: _jsx(ChartTooltipContent, { hideLabel: true, valueFormatter: valueFormatter }) }), _jsx(ChartLegend, { content: configLegend(config) }), _jsx(Pie, { data: parts, dataKey: "value", nameKey: "name", innerRadius: donut ? '62%' : 0, outerRadius: "92%", paddingAngle: donut && parts.length > 1 ? 2 : 0, cornerRadius: donut ? 3 : 0, stroke: "var(--sprout-surface)", strokeWidth: donut ? 0 : 1, isAnimationActive: false, children: donut ? (_jsx(Label, { position: "center", content: ({ viewBox }) => centerText(viewBox, valueFormatter(total), totalLabel) })) : null })] }) }));
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Drop-off from step to step, as centered bars that narrow like a funnel.
|
|
30
|
+
* Step names sit on the left; each step's count and share of the first sit
|
|
31
|
+
* on the right. One 40px row per step.
|
|
32
|
+
*/
|
|
33
|
+
export function StepFunnelChart({ rows, category, value, color = 'var(--sprout-chart-1)', title = 'Conversion by step', valueFormatter = formatNumber, className, }) {
|
|
34
|
+
const steps = useMemo(() => {
|
|
35
|
+
const values = rows.map(row => Math.max(0, Number(row[value]) || 0));
|
|
36
|
+
const top = Math.max(0, ...values);
|
|
37
|
+
const first = values[0] || 1;
|
|
38
|
+
return rows.map((row, index) => {
|
|
39
|
+
const count = values[index] ?? 0;
|
|
40
|
+
const share = Math.round((count / first) * 100);
|
|
41
|
+
return {
|
|
42
|
+
name: String(row[category] ?? ''),
|
|
43
|
+
detail: `${valueFormatter(count)} · ${share}%`,
|
|
44
|
+
count,
|
|
45
|
+
range: [(top - count) / 2, (top + count) / 2],
|
|
46
|
+
// Fades only to about half, so the last, smallest step stays visible.
|
|
47
|
+
fill: chartShade(color, rows.length * 2 - index, rows.length * 2),
|
|
48
|
+
};
|
|
49
|
+
});
|
|
50
|
+
}, [rows, category, value, color, valueFormatter]);
|
|
51
|
+
const config = useMemo(() => ({ count: { label: value, color } }), [value, color]);
|
|
52
|
+
return (_jsx(ChartContainer, { "data-variant": "funnel", className: cn('sprout-chart-funnel', className), style: { height: `${steps.length * 2.5 + 1}rem` }, config: config, children: _jsxs(BarChart, { data: steps, accessibilityLayer: true, title: title, layout: "vertical", barCategoryGap: "12%", margin: { top: 8, right: 0, bottom: 8, left: 0 }, children: [_jsx(XAxis, { type: "number", hide: true, domain: [0, 'dataMax'] }), _jsx(YAxis, { yAxisId: "name", type: "category", dataKey: "name", width: "auto", ...quietAxis, interval: 0 }), _jsx(YAxis, { yAxisId: "detail", type: "category", dataKey: "detail", orientation: "right", width: "auto", ...quietAxis, interval: 0 }), _jsx(ChartTooltip, { cursor: tooltipCursor, content: props => (_jsx(ChartTooltipContent, { active: props.active, label: props.label, valueFormatter: valueFormatter, payload: props.payload?.map(item => ({
|
|
53
|
+
...item,
|
|
54
|
+
name: 'count',
|
|
55
|
+
value: item.payload.count,
|
|
56
|
+
color,
|
|
57
|
+
})) })) }), _jsx(Bar, { yAxisId: "name", dataKey: "range", radius: 4, shape: rowFillShape, isAnimationActive: false })] }) }));
|
|
58
|
+
}
|
|
59
|
+
const treemapBox = (node, format) => {
|
|
60
|
+
const { x, y, width, height, depth } = node;
|
|
61
|
+
if (depth !== 1)
|
|
62
|
+
return _jsx("g", {});
|
|
63
|
+
const fill = node.fill;
|
|
64
|
+
const roomy = width > 64 && height > 36;
|
|
65
|
+
return (_jsxs("g", { children: [_jsx("rect", { x: x, y: y, width: width, height: height, rx: 4, fill: fill, stroke: "var(--sprout-surface)", strokeWidth: 2 }), roomy ? (_jsxs("text", { x: x + 8, y: y + 18, className: "sprout-chart-treemap-label", children: [_jsx("tspan", { x: x + 8, children: node.name }), _jsx("tspan", { x: x + 8, dy: 16, className: "sprout-chart-treemap-value", children: format(node.value) })] })) : null] }));
|
|
66
|
+
};
|
|
67
|
+
/**
|
|
68
|
+
* Boxes sized by value, largest top left, 288px tall. The top five get chart
|
|
69
|
+
* colors; the rest fold into one muted "N more" box.
|
|
70
|
+
*/
|
|
71
|
+
export function TreemapChart({ rows, category, value, title, valueFormatter = formatNumber, className, }) {
|
|
72
|
+
const parts = useMemo(() => foldParts(rows, category, value), [rows, category, value]);
|
|
73
|
+
const config = useMemo(() => partConfig(parts), [parts]);
|
|
74
|
+
return (_jsx(ChartContainer, { "data-variant": "treemap", className: cn('sprout-chart-treemap', className), config: config, role: "img", "aria-label": title ?? `${value} by ${category}`, children: _jsx(Treemap, { data: parts, dataKey: "value", nameKey: "name", aspectRatio: 4 / 3, isAnimationActive: false, content: (node) => treemapBox(node, valueFormatter), children: _jsx(ChartTooltip, { content: _jsx(ChartTooltipContent, { hideLabel: true, valueFormatter: valueFormatter }) }) }) }));
|
|
75
|
+
}
|
|
76
|
+
const percentOfMax = (value, max) => `${Math.round((value / (max || 1)) * 100)}%`;
|
|
77
|
+
/** Progress toward a goal as a half ring with the number in the middle. */
|
|
78
|
+
export function GaugeChart({ value, max = 100, label, title, color = 'var(--sprout-chart-1)', valueFormatter = percentOfMax, className, }) {
|
|
79
|
+
const data = useMemo(() => [{ name: 'value', value: Math.min(Math.max(value, 0), max), fill: color }], [value, max, color]);
|
|
80
|
+
const config = useMemo(() => ({ value: { label: title, color } }), [title, color]);
|
|
81
|
+
return (_jsx(ChartContainer, { "data-variant": "gauge", className: cn('sprout-chart-gauge', className), config: config, role: "meter", "aria-label": title, "aria-valuemin": 0, "aria-valuemax": max, "aria-valuenow": data[0].value, "aria-valuetext": valueFormatter(value, max), children: _jsxs(RadialBarChart, { data: data, startAngle: 180, endAngle: 0, cy: "78%", innerRadius: "118%", outerRadius: "158%", children: [_jsx(PolarAngleAxis, { type: "number", domain: [0, max], tick: false }), _jsx(RadialBar, { dataKey: "value", background: true, cornerRadius: 6, isAnimationActive: false }), _jsx(PolarRadiusAxis, { tick: false, axisLine: false, children: _jsx(Label, { content: ({ viewBox }) => centerText(viewBox, valueFormatter(value, max), label, 12) }) })] }) }));
|
|
82
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
export declare const timelineZoomNames: readonly ["week", "month", "quarter"];
|
|
2
|
+
/** How much time one timeline window shows. */
|
|
3
|
+
export type TimelineZoom = (typeof timelineZoomNames)[number];
|
|
4
|
+
/** A window's first and last day, both `YYYY-MM-DD` and inclusive. */
|
|
5
|
+
export interface TimelineRange {
|
|
6
|
+
start: string;
|
|
7
|
+
end: string;
|
|
8
|
+
}
|
|
9
|
+
export declare const DAY = 86400000;
|
|
10
|
+
/** True for a real calendar date written `YYYY-MM-DD`. */
|
|
11
|
+
export declare const isCalendarDate: (value: unknown) => value is string;
|
|
12
|
+
/** Midnight UTC of a `YYYY-MM-DD` date, in ms. Calendar dates have no time zone. */
|
|
13
|
+
export declare const dayTime: (date: string) => number;
|
|
14
|
+
/** The `YYYY-MM-DD` date of a UTC time in ms. */
|
|
15
|
+
export declare const dayString: (time: number) => string;
|
|
16
|
+
/**
|
|
17
|
+
* The window of `zoom` that contains `from`: its first and last day,
|
|
18
|
+
* inclusive. Pure calendar math, so it gives the same answer in every time
|
|
19
|
+
* zone. Use it for the query too, so the drawn window and the read window
|
|
20
|
+
* match. Weeks start on Monday unless `weekStartsOn` is 0 (Sunday).
|
|
21
|
+
*/
|
|
22
|
+
export declare function timelineRange(zoom: TimelineZoom, from: string, weekStartsOn?: 0 | 1): TimelineRange;
|
|
23
|
+
/**
|
|
24
|
+
* The first day of the window before (`-1`) or after (`1`) the one that
|
|
25
|
+
* contains `from`.
|
|
26
|
+
*/
|
|
27
|
+
export declare function shiftTimeline(zoom: TimelineZoom, from: string, step: -1 | 1, weekStartsOn?: 0 | 1): string;
|
|
28
|
+
/** The window's name: "October 2026", "12–18 Oct 2026", or "Q4 2026". */
|
|
29
|
+
/** ICU versions differ on thin and narrow spaces, so server and browser text would not match. */
|
|
30
|
+
export declare const plainSpaces: (text: string) => string;
|
|
31
|
+
export declare function timelineTitle(zoom: TimelineZoom, from: string, weekStartsOn?: 0 | 1, locale?: string): string;
|
|
32
|
+
/**
|
|
33
|
+
* The wall-clock time of `time` in `timeZone` (default: the reader's), as ms
|
|
34
|
+
* read as if it were UTC. It puts timestamps on the same axis as dates.
|
|
35
|
+
*/
|
|
36
|
+
export declare function wallClock(time: number, timeZone?: string): number;
|
|
37
|
+
/** Today as `YYYY-MM-DD` in `timeZone`, or in the reader's time zone. */
|
|
38
|
+
export declare const timelineToday: (timeZone?: string, now?: number) => string;
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
export const timelineZoomNames = ['week', 'month', 'quarter'];
|
|
2
|
+
export const DAY = 86_400_000;
|
|
3
|
+
const DATE = /^(\d{4})-(\d{2})-(\d{2})$/;
|
|
4
|
+
const utc = (year, month, day) => {
|
|
5
|
+
const date = new Date(0);
|
|
6
|
+
date.setUTCFullYear(year, month, day);
|
|
7
|
+
return date.getTime();
|
|
8
|
+
};
|
|
9
|
+
const parts = (date) => {
|
|
10
|
+
const match = DATE.exec(date);
|
|
11
|
+
const [year, month, day] = match ? [Number(match[1]), Number(match[2]) - 1, Number(match[3])] : [];
|
|
12
|
+
if (year === undefined || month === undefined || day === undefined)
|
|
13
|
+
throw new RangeError(`Not a YYYY-MM-DD date: ${date}`);
|
|
14
|
+
const time = utc(year, month, day);
|
|
15
|
+
const check = new Date(time);
|
|
16
|
+
if (check.getUTCMonth() !== month || check.getUTCDate() !== day)
|
|
17
|
+
throw new RangeError(`Not a calendar date: ${date}`);
|
|
18
|
+
return { year, month, day, time };
|
|
19
|
+
};
|
|
20
|
+
/** True for a real calendar date written `YYYY-MM-DD`. */
|
|
21
|
+
export const isCalendarDate = (value) => {
|
|
22
|
+
if (typeof value !== 'string' || !DATE.test(value))
|
|
23
|
+
return false;
|
|
24
|
+
try {
|
|
25
|
+
parts(value);
|
|
26
|
+
return true;
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
/** Midnight UTC of a `YYYY-MM-DD` date, in ms. Calendar dates have no time zone. */
|
|
33
|
+
export const dayTime = (date) => parts(date).time;
|
|
34
|
+
/** The `YYYY-MM-DD` date of a UTC time in ms. */
|
|
35
|
+
export const dayString = (time) => {
|
|
36
|
+
const date = new Date(time);
|
|
37
|
+
const year = String(date.getUTCFullYear()).padStart(4, '0');
|
|
38
|
+
const month = String(date.getUTCMonth() + 1).padStart(2, '0');
|
|
39
|
+
const day = String(date.getUTCDate()).padStart(2, '0');
|
|
40
|
+
return `${year}-${month}-${day}`;
|
|
41
|
+
};
|
|
42
|
+
/**
|
|
43
|
+
* The window of `zoom` that contains `from`: its first and last day,
|
|
44
|
+
* inclusive. Pure calendar math, so it gives the same answer in every time
|
|
45
|
+
* zone. Use it for the query too, so the drawn window and the read window
|
|
46
|
+
* match. Weeks start on Monday unless `weekStartsOn` is 0 (Sunday).
|
|
47
|
+
*/
|
|
48
|
+
export function timelineRange(zoom, from, weekStartsOn = 1) {
|
|
49
|
+
const { year, month, time } = parts(from);
|
|
50
|
+
if (zoom === 'week') {
|
|
51
|
+
const offset = (new Date(time).getUTCDay() - weekStartsOn + 7) % 7;
|
|
52
|
+
const start = time - offset * DAY;
|
|
53
|
+
return { start: dayString(start), end: dayString(start + 6 * DAY) };
|
|
54
|
+
}
|
|
55
|
+
const first = zoom === 'month' ? month : month - (month % 3);
|
|
56
|
+
const months = zoom === 'month' ? 1 : 3;
|
|
57
|
+
return { start: dayString(utc(year, first, 1)), end: dayString(utc(year, first + months, 0)) };
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* The first day of the window before (`-1`) or after (`1`) the one that
|
|
61
|
+
* contains `from`.
|
|
62
|
+
*/
|
|
63
|
+
export function shiftTimeline(zoom, from, step, weekStartsOn = 1) {
|
|
64
|
+
const { start } = timelineRange(zoom, from, weekStartsOn);
|
|
65
|
+
const { year, month, time } = parts(start);
|
|
66
|
+
if (zoom === 'week')
|
|
67
|
+
return dayString(time + step * 7 * DAY);
|
|
68
|
+
return dayString(utc(year, month + step * (zoom === 'month' ? 1 : 3), 1));
|
|
69
|
+
}
|
|
70
|
+
/** The window's name: "October 2026", "12–18 Oct 2026", or "Q4 2026". */
|
|
71
|
+
/** ICU versions differ on thin and narrow spaces, so server and browser text would not match. */
|
|
72
|
+
export const plainSpaces = (text) => text.replaceAll(/[\u2009\u202f]/g, ' ');
|
|
73
|
+
export function timelineTitle(zoom, from, weekStartsOn = 1, locale) {
|
|
74
|
+
const range = timelineRange(zoom, from, weekStartsOn);
|
|
75
|
+
const start = parts(range.start);
|
|
76
|
+
if (zoom === 'quarter')
|
|
77
|
+
return `Q${Math.floor(start.month / 3) + 1} ${start.year}`;
|
|
78
|
+
if (zoom === 'month')
|
|
79
|
+
return new Intl.DateTimeFormat(locale, { month: 'long', year: 'numeric', timeZone: 'UTC' }).format(start.time);
|
|
80
|
+
return plainSpaces(new Intl.DateTimeFormat(locale, {
|
|
81
|
+
day: 'numeric',
|
|
82
|
+
month: 'short',
|
|
83
|
+
year: 'numeric',
|
|
84
|
+
timeZone: 'UTC',
|
|
85
|
+
}).formatRange(start.time, dayTime(range.end)));
|
|
86
|
+
}
|
|
87
|
+
const clocks = new Map();
|
|
88
|
+
/**
|
|
89
|
+
* The wall-clock time of `time` in `timeZone` (default: the reader's), as ms
|
|
90
|
+
* read as if it were UTC. It puts timestamps on the same axis as dates.
|
|
91
|
+
*/
|
|
92
|
+
export function wallClock(time, timeZone) {
|
|
93
|
+
const key = timeZone ?? '';
|
|
94
|
+
let clock = clocks.get(key);
|
|
95
|
+
if (!clock) {
|
|
96
|
+
clock = new Intl.DateTimeFormat('en-US', {
|
|
97
|
+
timeZone,
|
|
98
|
+
hourCycle: 'h23',
|
|
99
|
+
year: 'numeric',
|
|
100
|
+
month: 'numeric',
|
|
101
|
+
day: 'numeric',
|
|
102
|
+
hour: 'numeric',
|
|
103
|
+
minute: 'numeric',
|
|
104
|
+
second: 'numeric',
|
|
105
|
+
});
|
|
106
|
+
clocks.set(key, clock);
|
|
107
|
+
}
|
|
108
|
+
const part = Object.fromEntries(clock.formatToParts(time).map(item => [item.type, Number(item.value)]));
|
|
109
|
+
const seconds = ((part.hour ?? 0) * 60 + (part.minute ?? 0)) * 60 + (part.second ?? 0);
|
|
110
|
+
return (utc(part.year ?? 1970, (part.month ?? 1) - 1, part.day ?? 1) + seconds * 1000 + (((time % 1000) + 1000) % 1000));
|
|
111
|
+
}
|
|
112
|
+
/** Today as `YYYY-MM-DD` in `timeZone`, or in the reader's time zone. */
|
|
113
|
+
export const timelineToday = (timeZone, now = Date.now()) => dayString(Math.floor(wallClock(now, timeZone) / DAY) * DAY);
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
import { type TimelineZoom } from './timeline-dates.js';
|
|
3
|
+
export interface TimelineWindowBarProps {
|
|
4
|
+
/** The window size. */
|
|
5
|
+
zoom: TimelineZoom;
|
|
6
|
+
/** Any day in the window, as `YYYY-MM-DD`. */
|
|
7
|
+
from: string;
|
|
8
|
+
/** Gets the next size and day after Previous, Today, Next, or a size change. */
|
|
9
|
+
onChange: (next: {
|
|
10
|
+
zoom: TimelineZoom;
|
|
11
|
+
from: string;
|
|
12
|
+
}) => void;
|
|
13
|
+
/** Today in the reader's time zone, as `YYYY-MM-DD`. */
|
|
14
|
+
today: string;
|
|
15
|
+
/** 1 starts weeks on Monday (default), 0 on Sunday. */
|
|
16
|
+
weekStartsOn?: 0 | 1 | undefined;
|
|
17
|
+
/** Locale for the window title. Defaults to the reader's. */
|
|
18
|
+
locale?: string | undefined;
|
|
19
|
+
/** Extra controls at the end, such as start and end field pickers. */
|
|
20
|
+
actions?: ReactNode;
|
|
21
|
+
/** Turns off every control, for example while the app saves. */
|
|
22
|
+
disabled?: boolean | undefined;
|
|
23
|
+
className?: string | undefined;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Previous, Today, and Next buttons, the window title, and a Week, Month,
|
|
27
|
+
* Quarter switch for a timeline. The app keeps `zoom` and `from`, for
|
|
28
|
+
* example in the link, and passes `timelineRange(zoom, from)` to
|
|
29
|
+
* `TimelineChart` as its `window`.
|
|
30
|
+
*/
|
|
31
|
+
export declare function TimelineWindowBar({ zoom, from, onChange, today, weekStartsOn, locale, actions, disabled, className, }: TimelineWindowBarProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { ChevronLeft } from 'reicon-react/icons/ChevronLeft';
|
|
4
|
+
import { ChevronRight } from 'reicon-react/icons/ChevronRight';
|
|
5
|
+
import { Button } from '../../components/button.js';
|
|
6
|
+
import { ToggleGroup, ToggleGroupItem } from '../../components/toggle-group.js';
|
|
7
|
+
import { cn } from '../classes.js';
|
|
8
|
+
import { shiftTimeline, timelineTitle, timelineZoomNames } from './timeline-dates.js';
|
|
9
|
+
const zoomLabels = {
|
|
10
|
+
week: 'Week',
|
|
11
|
+
month: 'Month',
|
|
12
|
+
quarter: 'Quarter',
|
|
13
|
+
};
|
|
14
|
+
/**
|
|
15
|
+
* Previous, Today, and Next buttons, the window title, and a Week, Month,
|
|
16
|
+
* Quarter switch for a timeline. The app keeps `zoom` and `from`, for
|
|
17
|
+
* example in the link, and passes `timelineRange(zoom, from)` to
|
|
18
|
+
* `TimelineChart` as its `window`.
|
|
19
|
+
*/
|
|
20
|
+
export function TimelineWindowBar({ zoom, from, onChange, today, weekStartsOn = 1, locale, actions, disabled, className, }) {
|
|
21
|
+
return (_jsxs("div", { "data-slot": "timeline-window-bar", className: cn('sprout-timeline-window-bar', className), children: [_jsxs("div", { className: "sprout-timeline-window-nav", children: [_jsx(Button, { variant: "outline", size: "icon-sm", "aria-label": `Previous ${zoom}`, disabled: disabled, onClick: () => onChange({ zoom, from: shiftTimeline(zoom, from, -1, weekStartsOn) }), children: _jsx(ChevronLeft, { size: 16, "aria-hidden": "true" }) }), _jsx(Button, { variant: "outline", size: "sm", disabled: disabled, onClick: () => onChange({ zoom, from: today }), children: "Today" }), _jsx(Button, { variant: "outline", size: "icon-sm", "aria-label": `Next ${zoom}`, disabled: disabled, onClick: () => onChange({ zoom, from: shiftTimeline(zoom, from, 1, weekStartsOn) }), children: _jsx(ChevronRight, { size: 16, "aria-hidden": "true" }) })] }), _jsx("p", { className: "sprout-timeline-window-title", "aria-live": "polite", "aria-atomic": "true", children: timelineTitle(zoom, from, weekStartsOn, locale) }), actions ? _jsx("div", { className: "sprout-timeline-window-actions", children: actions }) : null, _jsx(ToggleGroup, { "aria-label": "Window size", variant: "outline", size: "sm", spacing: 0, className: "sprout-timeline-window-zoom", value: [zoom], disabled: disabled, onValueChange: value => {
|
|
22
|
+
const next = value[0];
|
|
23
|
+
if (next && next !== zoom)
|
|
24
|
+
onChange({ zoom: next, from });
|
|
25
|
+
}, children: timelineZoomNames.map(name => (_jsx(ToggleGroupItem, { value: name, children: zoomLabels[name] }, name))) })] }));
|
|
26
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { type ReactNode } from 'react';
|
|
2
|
+
import { type ChartRow, type ChartSeries, type ReadyChartProps } from '../chart-kit.js';
|
|
3
|
+
import { type TimelineRange } from './timeline-dates.js';
|
|
4
|
+
/** Rows the app read but could not draw, because their start is empty. */
|
|
5
|
+
export interface TimelineUndated {
|
|
6
|
+
/** How many rows have no start. A string passes through, such as "1,204". */
|
|
7
|
+
count: number | string;
|
|
8
|
+
/** The start field's name, as in "3 rows without Due date". */
|
|
9
|
+
field: string;
|
|
10
|
+
/** A link or button, such as one that opens Table filtered to those rows. */
|
|
11
|
+
action?: ReactNode;
|
|
12
|
+
}
|
|
13
|
+
export type TimelineStatus = 'ready' | 'loading' | 'error';
|
|
14
|
+
export interface TimelineChartProps<T extends ChartRow = ChartRow> extends ReadyChartProps {
|
|
15
|
+
/** One row per task or record. Rows with no start are not drawn. */
|
|
16
|
+
rows: T[];
|
|
17
|
+
/** The row key whose value names each row. */
|
|
18
|
+
category: string;
|
|
19
|
+
/** The row key holding the start, as `YYYY-MM-DD` or a time in ms. */
|
|
20
|
+
start: string;
|
|
21
|
+
/**
|
|
22
|
+
* The row key holding the end, in the same form as `start`. Without it,
|
|
23
|
+
* or when a row's end is empty, the row is a point. An end before the
|
|
24
|
+
* start is a point marked "End before start".
|
|
25
|
+
*/
|
|
26
|
+
end?: string | undefined;
|
|
27
|
+
/** The row key naming each row's series, for color and legend. */
|
|
28
|
+
seriesKey?: string | undefined;
|
|
29
|
+
/** Colors and legend labels, matched by `seriesKey`. */
|
|
30
|
+
series?: ChartSeries[] | undefined;
|
|
31
|
+
/** Locale for dates. Defaults to the reader's. */
|
|
32
|
+
locale?: string | undefined;
|
|
33
|
+
/**
|
|
34
|
+
* The days to show, both inclusive, from `timelineRange`. Bars are cut at
|
|
35
|
+
* its edges and the cut ends are marked. Rows outside it are left out.
|
|
36
|
+
* Without it the axis fits the rows.
|
|
37
|
+
*/
|
|
38
|
+
window?: TimelineRange | undefined;
|
|
39
|
+
/** Today as `YYYY-MM-DD`. Draws a line labeled "Today". */
|
|
40
|
+
today?: string | undefined;
|
|
41
|
+
/** Time zone for times in ms. Defaults to the reader's when a list or window is shown. */
|
|
42
|
+
timeZone?: string | undefined;
|
|
43
|
+
/** The row key holding a stable id. Defaults to the row's position. */
|
|
44
|
+
rowKey?: string | undefined;
|
|
45
|
+
/** Title content in the list beside the chart. Defaults to the `category` value. */
|
|
46
|
+
renderLabel?: ((row: T) => ReactNode) | undefined;
|
|
47
|
+
/** Actions at the end of each list row, such as an edit button and a menu. */
|
|
48
|
+
renderActions?: ((row: T) => ReactNode) | undefined;
|
|
49
|
+
/** Click or Enter on a row. Open your own Sheet or Dialog here. */
|
|
50
|
+
onRowOpen?: ((row: T) => void) | undefined;
|
|
51
|
+
/** Counts rows with no start, under the chart. */
|
|
52
|
+
undated?: TimelineUndated | undefined;
|
|
53
|
+
/** A line above the chart, such as "Showing the first 100 of 340 rows". */
|
|
54
|
+
notice?: ReactNode;
|
|
55
|
+
/** `loading` keeps drawn rows under `aria-busy`, or shows placeholders. */
|
|
56
|
+
status?: TimelineStatus | undefined;
|
|
57
|
+
/** What went wrong, under "Couldn't load the timeline". */
|
|
58
|
+
error?: ReactNode;
|
|
59
|
+
/** Shows "Try again" on the error. */
|
|
60
|
+
onRetry?: (() => void) | undefined;
|
|
61
|
+
/** Replaces the empty message when the window has no rows. */
|
|
62
|
+
empty?: ReactNode;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Rows as bars on a time axis, like a project plan. A row with only a start
|
|
66
|
+
* is a diamond. Give it a `window`, `renderLabel`, `renderActions`, or
|
|
67
|
+
* `onRowOpen` and it becomes a record timeline: a list of titles and dates
|
|
68
|
+
* in words beside the chart, in start order, with one tab stop and arrow
|
|
69
|
+
* keys. Without them it is a plain chart with names on the axis, one 32px
|
|
70
|
+
* row per task, colored by `seriesKey`.
|
|
71
|
+
*/
|
|
72
|
+
export declare function TimelineChart<T extends ChartRow = ChartRow>({ rows, category, start, end, seriesKey, series, locale, window, today, timeZone, rowKey, renderLabel, renderActions, onRowOpen, undated, notice, status, error, onRetry, empty, title, className, }: TimelineChartProps<T>): import("react").JSX.Element;
|