@keyfunc/keybricks-ds 1.0.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/DEPLOY.md +204 -0
- package/DESIGN.md +269 -0
- package/INTEGRATION.md +417 -0
- package/SKILL.md +32 -0
- package/_adherence.oxlintrc.json +1469 -0
- package/_scripts/build-index.mjs +80 -0
- package/_scripts/build-tokens.mjs +135 -0
- package/assets/icons/icon-index.js +1 -0
- package/assets/icons/icon-index.json +1 -0
- package/assets/icons/unicons-index.js +1 -0
- package/assets/icons/unicons-index.json +1 -0
- package/assets/keybricks-logo.png +0 -0
- package/components/_cards/accordion.card.html +32 -0
- package/components/_cards/action-chip.card.html +22 -0
- package/components/_cards/activity.card.html +34 -0
- package/components/_cards/alert.card.html +32 -0
- package/components/_cards/app-chrome.card.html +36 -0
- package/components/_cards/banner.card.html +21 -0
- package/components/_cards/brand-visual.card.html +43 -0
- package/components/_cards/breadcrumb.card.html +23 -0
- package/components/_cards/button-1-base.card.html +39 -0
- package/components/_cards/button-2-states.card.html +56 -0
- package/components/_cards/button-3-sizes-icons.card.html +32 -0
- package/components/_cards/button-4-group.card.html +47 -0
- package/components/_cards/canvas-zoom-controls.card.html +19 -0
- package/components/_cards/cards-indicators.card.html +59 -0
- package/components/_cards/charts-1-sparkline.card.html +55 -0
- package/components/_cards/charts-10-gauge.card.html +31 -0
- package/components/_cards/charts-11-funnel.card.html +28 -0
- package/components/_cards/charts-12-treemap.card.html +23 -0
- package/components/_cards/charts-14-radar.card.html +31 -0
- package/components/_cards/charts-15-distribution.card.html +32 -0
- package/components/_cards/charts-16-pareto.card.html +25 -0
- package/components/_cards/charts-17-candlestick.card.html +34 -0
- package/components/_cards/charts-18-sankey.card.html +29 -0
- package/components/_cards/charts-19-chord.card.html +31 -0
- package/components/_cards/charts-2-line.card.html +46 -0
- package/components/_cards/charts-20-sunburst.card.html +38 -0
- package/components/_cards/charts-21-worldmap.card.html +33 -0
- package/components/_cards/charts-22-gantt.card.html +37 -0
- package/components/_cards/charts-23-bar3d.card.html +26 -0
- package/components/_cards/charts-24-pie3d.card.html +32 -0
- package/components/_cards/charts-25-surface3d.card.html +24 -0
- package/components/_cards/charts-26-scatter3d.card.html +24 -0
- package/components/_cards/charts-27-ribbon3d.card.html +23 -0
- package/components/_cards/charts-3-bar.card.html +71 -0
- package/components/_cards/charts-4-barlist.card.html +30 -0
- package/components/_cards/charts-5-donut.card.html +35 -0
- package/components/_cards/charts-6-scatter.card.html +26 -0
- package/components/_cards/charts-7-heatmap.card.html +28 -0
- package/components/_cards/charts-8-waterfall.card.html +25 -0
- package/components/_cards/charts-9-bullet.card.html +31 -0
- package/components/_cards/charts-99-fullpage.card.html +194 -0
- package/components/_cards/checkbox.card.html +34 -0
- package/components/_cards/codeblock.card.html +19 -0
- package/components/_cards/combobox.card.html +25 -0
- package/components/_cards/context-menu.card.html +21 -0
- package/components/_cards/datatable.card.html +48 -0
- package/components/_cards/date-picker.card.html +31 -0
- package/components/_cards/diffvalue.card.html +21 -0
- package/components/_cards/divider.card.html +24 -0
- package/components/_cards/drawer.card.html +30 -0
- package/components/_cards/dropdown-menu.card.html +38 -0
- package/components/_cards/dropzone.card.html +22 -0
- package/components/_cards/empty-state.card.html +29 -0
- package/components/_cards/filter-bar.card.html +34 -0
- package/components/_cards/icon-button.card.html +27 -0
- package/components/_cards/icons-1-core.card.html +49 -0
- package/components/_cards/icons-2-feather.card.html +39 -0
- package/components/_cards/icons-3-unicons.card.html +44 -0
- package/components/_cards/input.card.html +25 -0
- package/components/_cards/kbd.card.html +20 -0
- package/components/_cards/keyvalue.card.html +23 -0
- package/components/_cards/listrow.card.html +30 -0
- package/components/_cards/loading.card.html +33 -0
- package/components/_cards/log-viewer.card.html +23 -0
- package/components/_cards/modal.card.html +35 -0
- package/components/_cards/multi-select.card.html +26 -0
- package/components/_cards/number-stepper.card.html +24 -0
- package/components/_cards/pagination.card.html +25 -0
- package/components/_cards/people.card.html +46 -0
- package/components/_cards/pipeline-node.card.html +76 -0
- package/components/_cards/pipeline-palette.card.html +54 -0
- package/components/_cards/search.card.html +23 -0
- package/components/_cards/select.card.html +23 -0
- package/components/_cards/slider.card.html +25 -0
- package/components/_cards/smart-table.card.html +37 -0
- package/components/_cards/spreadsheet.card.html +31 -0
- package/components/_cards/stepper.card.html +25 -0
- package/components/_cards/tabs.card.html +25 -0
- package/components/_cards/tag.card.html +26 -0
- package/components/_cards/textarea.card.html +27 -0
- package/components/_cards/timeline.card.html +32 -0
- package/components/_cards/toast.card.html +33 -0
- package/components/_cards/toggles.card.html +68 -0
- package/components/_cards/tooltip.card.html +22 -0
- package/components/_cards/tree.card.html +150 -0
- package/components/_shared/frame-theme.js +16 -0
- package/components/_shared/src-brand.js +1 -0
- package/components/_shared/src-buttons.js +1 -0
- package/components/_shared/src-charts.js +1 -0
- package/components/_shared/src-data.js +1 -0
- package/components/_shared/src-display.js +1 -0
- package/components/_shared/src-feedback.js +1 -0
- package/components/_shared/src-forms.js +1 -0
- package/components/_shared/src-icons.js +1 -0
- package/components/_shared/src-navigation.js +1 -0
- package/components/_shared/src-pipeline.js +1 -0
- package/components/_shared/src-tabs.js +1 -0
- package/components/_shared/svg-download.js +19 -0
- package/components/_shared/view-source.js +185 -0
- package/components/_vue/index.mjs +114 -0
- package/components/brand/BrandVisual.d.ts +17 -0
- package/components/brand/BrandVisual.jsx +29 -0
- package/components/brand/BrandVisual.vue +34 -0
- package/components/brand/_dir.card.html +13 -0
- package/components/brand/brandMotifs.js +210 -0
- package/components/buttons/ActionChip.d.ts +10 -0
- package/components/buttons/ActionChip.jsx +20 -0
- package/components/buttons/ActionChip.vue +18 -0
- package/components/buttons/Button.d.ts +40 -0
- package/components/buttons/Button.jsx +101 -0
- package/components/buttons/Button.vue +87 -0
- package/components/buttons/IconButton.d.ts +26 -0
- package/components/buttons/IconButton.jsx +28 -0
- package/components/buttons/IconButton.vue +21 -0
- package/components/buttons/_dir.card.html +13 -0
- package/components/buttons/buttons.prompt.md +19 -0
- package/components/charts/Bar3D.d.ts +15 -0
- package/components/charts/Bar3D.jsx +93 -0
- package/components/charts/Bar3D.vue +79 -0
- package/components/charts/BarChart.d.ts +23 -0
- package/components/charts/BarChart.jsx +135 -0
- package/components/charts/BarChart.vue +139 -0
- package/components/charts/BarList.d.ts +13 -0
- package/components/charts/BarList.jsx +31 -0
- package/components/charts/BarList.vue +25 -0
- package/components/charts/BoxPlot.d.ts +18 -0
- package/components/charts/BoxPlot.jsx +62 -0
- package/components/charts/BoxPlot.vue +58 -0
- package/components/charts/BulletChart.d.ts +12 -0
- package/components/charts/BulletChart.jsx +41 -0
- package/components/charts/BulletChart.vue +33 -0
- package/components/charts/CandlestickChart.d.ts +15 -0
- package/components/charts/CandlestickChart.jsx +63 -0
- package/components/charts/CandlestickChart.vue +62 -0
- package/components/charts/ChartKit.d.ts +8 -0
- package/components/charts/ChartKit.jsx +76 -0
- package/components/charts/ChartKit.vue +47 -0
- package/components/charts/ChordChart.d.ts +13 -0
- package/components/charts/ChordChart.jsx +55 -0
- package/components/charts/ChordChart.vue +54 -0
- package/components/charts/DonutChart.d.ts +15 -0
- package/components/charts/DonutChart.jsx +73 -0
- package/components/charts/DonutChart.vue +61 -0
- package/components/charts/FunnelChart.d.ts +11 -0
- package/components/charts/FunnelChart.jsx +37 -0
- package/components/charts/FunnelChart.vue +29 -0
- package/components/charts/GanttChart.d.ts +20 -0
- package/components/charts/GanttChart.jsx +80 -0
- package/components/charts/GanttChart.vue +60 -0
- package/components/charts/GaugeChart.d.ts +13 -0
- package/components/charts/GaugeChart.jsx +33 -0
- package/components/charts/GaugeChart.vue +29 -0
- package/components/charts/HeatmapChart.d.ts +13 -0
- package/components/charts/HeatmapChart.jsx +62 -0
- package/components/charts/HeatmapChart.vue +38 -0
- package/components/charts/Histogram.d.ts +16 -0
- package/components/charts/Histogram.jsx +56 -0
- package/components/charts/Histogram.vue +53 -0
- package/components/charts/LineChart.d.ts +17 -0
- package/components/charts/LineChart.jsx +84 -0
- package/components/charts/LineChart.vue +86 -0
- package/components/charts/ParetoChart.d.ts +13 -0
- package/components/charts/ParetoChart.jsx +54 -0
- package/components/charts/ParetoChart.vue +55 -0
- package/components/charts/Pie3D.d.ts +17 -0
- package/components/charts/Pie3D.jsx +145 -0
- package/components/charts/Pie3D.vue +115 -0
- package/components/charts/RadarChart.d.ts +14 -0
- package/components/charts/RadarChart.jsx +79 -0
- package/components/charts/RadarChart.vue +73 -0
- package/components/charts/Ribbon3D.d.ts +13 -0
- package/components/charts/Ribbon3D.jsx +89 -0
- package/components/charts/Ribbon3D.vue +79 -0
- package/components/charts/SankeyChart.d.ts +15 -0
- package/components/charts/SankeyChart.jsx +59 -0
- package/components/charts/SankeyChart.vue +53 -0
- package/components/charts/Scatter3D.d.ts +12 -0
- package/components/charts/Scatter3D.jsx +69 -0
- package/components/charts/Scatter3D.vue +59 -0
- package/components/charts/ScatterChart.d.ts +12 -0
- package/components/charts/ScatterChart.jsx +53 -0
- package/components/charts/ScatterChart.vue +57 -0
- package/components/charts/Sparkline.d.ts +19 -0
- package/components/charts/Sparkline.jsx +63 -0
- package/components/charts/Sparkline.vue +54 -0
- package/components/charts/SunburstChart.d.ts +12 -0
- package/components/charts/SunburstChart.jsx +61 -0
- package/components/charts/SunburstChart.vue +60 -0
- package/components/charts/Surface3D.d.ts +13 -0
- package/components/charts/Surface3D.jsx +70 -0
- package/components/charts/Surface3D.vue +63 -0
- package/components/charts/TreemapChart.d.ts +11 -0
- package/components/charts/TreemapChart.jsx +35 -0
- package/components/charts/TreemapChart.vue +27 -0
- package/components/charts/WaterfallChart.d.ts +11 -0
- package/components/charts/WaterfallChart.jsx +59 -0
- package/components/charts/WaterfallChart.vue +46 -0
- package/components/charts/WorldMap.d.ts +21 -0
- package/components/charts/WorldMap.jsx +107 -0
- package/components/charts/WorldMap.vue +73 -0
- package/components/charts/_dir.card.html +7 -0
- package/components/charts/chart-3d.js +84 -0
- package/components/charts/chart-geometry.js +223 -0
- package/components/charts/charts.prompt.md +24 -0
- package/components/data/CodeBlock.d.ts +9 -0
- package/components/data/CodeBlock.jsx +19 -0
- package/components/data/CodeBlock.vue +22 -0
- package/components/data/DataTable.d.ts +28 -0
- package/components/data/DataTable.jsx +32 -0
- package/components/data/DataTable.vue +25 -0
- package/components/data/DiffValue.d.ts +11 -0
- package/components/data/DiffValue.jsx +15 -0
- package/components/data/DiffValue.vue +13 -0
- package/components/data/FilterBar.d.ts +16 -0
- package/components/data/FilterBar.jsx +22 -0
- package/components/data/FilterBar.vue +23 -0
- package/components/data/KeyValue.d.ts +16 -0
- package/components/data/KeyValue.jsx +25 -0
- package/components/data/KeyValue.vue +26 -0
- package/components/data/ListRow.d.ts +14 -0
- package/components/data/ListRow.jsx +25 -0
- package/components/data/ListRow.vue +25 -0
- package/components/data/Pagination.d.ts +12 -0
- package/components/data/Pagination.jsx +27 -0
- package/components/data/Pagination.vue +24 -0
- package/components/data/SmartTable.d.ts +30 -0
- package/components/data/SmartTable.jsx +73 -0
- package/components/data/SmartTable.vue +74 -0
- package/components/data/Spreadsheet.d.ts +33 -0
- package/components/data/Spreadsheet.jsx +110 -0
- package/components/data/Spreadsheet.vue +95 -0
- package/components/data/_dir.card.html +13 -0
- package/components/data/data.prompt.md +33 -0
- package/components/display/Avatar.d.ts +21 -0
- package/components/display/Avatar.jsx +31 -0
- package/components/display/Avatar.vue +34 -0
- package/components/display/Badge.d.ts +27 -0
- package/components/display/Badge.jsx +40 -0
- package/components/display/Badge.vue +47 -0
- package/components/display/Card.d.ts +27 -0
- package/components/display/Card.jsx +27 -0
- package/components/display/Card.vue +28 -0
- package/components/display/Divider.d.ts +8 -0
- package/components/display/Divider.jsx +14 -0
- package/components/display/Divider.vue +13 -0
- package/components/display/Kbd.d.ts +7 -0
- package/components/display/Kbd.jsx +12 -0
- package/components/display/Kbd.vue +9 -0
- package/components/display/ProgressBar.d.ts +42 -0
- package/components/display/ProgressBar.jsx +70 -0
- package/components/display/ProgressBar.vue +69 -0
- package/components/display/Tag.d.ts +9 -0
- package/components/display/Tag.jsx +15 -0
- package/components/display/Tag.vue +15 -0
- package/components/display/_dir.card.html +13 -0
- package/components/display/display.prompt.md +30 -0
- package/components/feedback/Accordion.d.ts +13 -0
- package/components/feedback/Accordion.jsx +21 -0
- package/components/feedback/Accordion.vue +22 -0
- package/components/feedback/ActivityItem.d.ts +25 -0
- package/components/feedback/ActivityItem.jsx +39 -0
- package/components/feedback/ActivityItem.vue +46 -0
- package/components/feedback/Alert.d.ts +36 -0
- package/components/feedback/Alert.jsx +48 -0
- package/components/feedback/Alert.vue +59 -0
- package/components/feedback/Banner.d.ts +15 -0
- package/components/feedback/Banner.jsx +25 -0
- package/components/feedback/Banner.vue +25 -0
- package/components/feedback/ContextMenu.d.ts +8 -0
- package/components/feedback/ContextMenu.jsx +18 -0
- package/components/feedback/ContextMenu.vue +19 -0
- package/components/feedback/Drawer.d.ts +18 -0
- package/components/feedback/Drawer.jsx +26 -0
- package/components/feedback/Drawer.vue +24 -0
- package/components/feedback/Menu.d.ts +10 -0
- package/components/feedback/Menu.jsx +24 -0
- package/components/feedback/Menu.vue +21 -0
- package/components/feedback/Modal.d.ts +38 -0
- package/components/feedback/Modal.jsx +69 -0
- package/components/feedback/Modal.vue +65 -0
- package/components/feedback/Stepper.d.ts +9 -0
- package/components/feedback/Stepper.jsx +24 -0
- package/components/feedback/Stepper.vue +18 -0
- package/components/feedback/Timeline.d.ts +15 -0
- package/components/feedback/Timeline.jsx +27 -0
- package/components/feedback/Timeline.vue +21 -0
- package/components/feedback/_dir.card.html +13 -0
- package/components/feedback/feedback.prompt.md +35 -0
- package/components/forms/Checkbox.d.ts +13 -0
- package/components/forms/Checkbox.jsx +17 -0
- package/components/forms/Checkbox.vue +18 -0
- package/components/forms/Combobox.d.ts +13 -0
- package/components/forms/Combobox.jsx +34 -0
- package/components/forms/Combobox.vue +35 -0
- package/components/forms/DatePicker.d.ts +12 -0
- package/components/forms/DatePicker.jsx +48 -0
- package/components/forms/DatePicker.vue +46 -0
- package/components/forms/Dropzone.d.ts +20 -0
- package/components/forms/Dropzone.jsx +36 -0
- package/components/forms/Dropzone.vue +28 -0
- package/components/forms/Field.d.ts +13 -0
- package/components/forms/Field.jsx +13 -0
- package/components/forms/Field.vue +12 -0
- package/components/forms/Input.d.ts +18 -0
- package/components/forms/Input.jsx +35 -0
- package/components/forms/Input.vue +38 -0
- package/components/forms/MultiSelect.d.ts +14 -0
- package/components/forms/MultiSelect.jsx +33 -0
- package/components/forms/MultiSelect.vue +36 -0
- package/components/forms/NumberStepper.d.ts +13 -0
- package/components/forms/NumberStepper.jsx +15 -0
- package/components/forms/NumberStepper.vue +17 -0
- package/components/forms/Radio.vue +16 -0
- package/components/forms/RadioGroup.vue +11 -0
- package/components/forms/SearchInput.d.ts +12 -0
- package/components/forms/SearchInput.jsx +17 -0
- package/components/forms/SearchInput.vue +14 -0
- package/components/forms/Select.d.ts +14 -0
- package/components/forms/Select.jsx +17 -0
- package/components/forms/Select.vue +16 -0
- package/components/forms/Slider.d.ts +15 -0
- package/components/forms/Slider.jsx +21 -0
- package/components/forms/Slider.vue +23 -0
- package/components/forms/Switch.d.ts +31 -0
- package/components/forms/Switch.jsx +40 -0
- package/components/forms/Switch.vue +16 -0
- package/components/forms/_dir.card.html +13 -0
- package/components/forms/forms.prompt.md +23 -0
- package/components/icons/Icon.d.ts +19 -0
- package/components/icons/Icon.jsx +93 -0
- package/components/icons/Icon.prompt.md +8 -0
- package/components/icons/Icon.vue +12 -0
- package/components/icons/Unicon.d.ts +12 -0
- package/components/icons/Unicon.jsx +16 -0
- package/components/icons/Unicon.vue +15 -0
- package/components/icons/_dir.card.html +13 -0
- package/components/icons/feather-paths.js +2 -0
- package/components/icons/icon-paths.js +73 -0
- package/components/index.d.ts +87 -0
- package/components/index.js +110 -0
- package/components/navigation/Breadcrumb.d.ts +24 -0
- package/components/navigation/Breadcrumb.jsx +120 -0
- package/components/navigation/Breadcrumb.vue +94 -0
- package/components/navigation/DocTree.d.ts +18 -0
- package/components/navigation/DocTree.jsx +37 -0
- package/components/navigation/DocTree.vue +25 -0
- package/components/navigation/Sidebar.d.ts +29 -0
- package/components/navigation/Sidebar.jsx +75 -0
- package/components/navigation/Sidebar.vue +54 -0
- package/components/navigation/TopBar.d.ts +27 -0
- package/components/navigation/TopBar.jsx +44 -0
- package/components/navigation/TopBar.vue +51 -0
- package/components/navigation/Tree.d.ts +83 -0
- package/components/navigation/Tree.jsx +347 -0
- package/components/navigation/Tree.vue +258 -0
- package/components/navigation/_dir.card.html +13 -0
- package/components/navigation/navigation.prompt.md +22 -0
- package/components/pipeline/ConfigPanel.d.ts +18 -0
- package/components/pipeline/ConfigPanel.jsx +30 -0
- package/components/pipeline/ConfigPanel.vue +32 -0
- package/components/pipeline/DataPreview.d.ts +14 -0
- package/components/pipeline/DataPreview.jsx +24 -0
- package/components/pipeline/DataPreview.vue +21 -0
- package/components/pipeline/NodeCard.d.ts +47 -0
- package/components/pipeline/NodeCard.jsx +97 -0
- package/components/pipeline/NodeCard.vue +75 -0
- package/components/pipeline/PaletteItem.vue +15 -0
- package/components/pipeline/PaletteListItem.vue +15 -0
- package/components/pipeline/PalettePanel.d.ts +20 -0
- package/components/pipeline/PalettePanel.jsx +74 -0
- package/components/pipeline/PalettePanel.vue +45 -0
- package/components/pipeline/PipelineCanvas.d.ts +25 -0
- package/components/pipeline/PipelineCanvas.jsx +94 -0
- package/components/pipeline/PipelineCanvas.vue +73 -0
- package/components/pipeline/ZoomControls.d.ts +9 -0
- package/components/pipeline/ZoomControls.jsx +15 -0
- package/components/pipeline/ZoomControls.vue +15 -0
- package/components/pipeline/_dir.card.html +13 -0
- package/components/pipeline/pipeline.prompt.md +22 -0
- package/components/tabs/SegmentedControl.d.ts +11 -0
- package/components/tabs/SegmentedControl.jsx +21 -0
- package/components/tabs/SegmentedControl.vue +15 -0
- package/components/tabs/Tabs.d.ts +13 -0
- package/components/tabs/Tabs.jsx +52 -0
- package/components/tabs/Tabs.vue +39 -0
- package/components/tabs/ViewToggle.d.ts +8 -0
- package/components/tabs/ViewToggle.jsx +12 -0
- package/components/tabs/ViewToggle.vue +12 -0
- package/components/tabs/_dir.card.html +13 -0
- package/components/tabs/tabs.prompt.md +14 -0
- package/package.json +76 -0
- package/readme.md +150 -0
- package/styles.css +10 -0
- package/tokens/alpha.css +27 -0
- package/tokens/charts.css +55 -0
- package/tokens/colors-extended.css +40 -0
- package/tokens/colors.css +39 -0
- package/tokens/dark.css +96 -0
- package/tokens/fonts.css +4 -0
- package/tokens/interaction.css +48 -0
- package/tokens/layout.css +30 -0
- package/tokens/motion.css +22 -0
- package/tokens/tokens.d.ts +680 -0
- package/tokens/tokens.js +1018 -0
- package/tokens/tokens.json +2928 -0
- package/tokens/typography.css +22 -0
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
export interface DonutSegment { label: string; value: number; color?: string; }
|
|
3
|
+
/** Donut with a KPI center (or pie). Hovering a segment or its legend row lifts it. */
|
|
4
|
+
export interface DonutChartProps {
|
|
5
|
+
segments?: DonutSegment[];
|
|
6
|
+
size?: number; thickness?: number;
|
|
7
|
+
/** Fill to the middle, no center KPI */ pie?: boolean;
|
|
8
|
+
centerLabel?: string;
|
|
9
|
+
/** Pinned center stat — when set, hover no longer swaps the center (the tooltip carries detail) */
|
|
10
|
+
centerValue?: React.ReactNode;
|
|
11
|
+
legend?: boolean; interactive?: boolean;
|
|
12
|
+
valueFormat?: (v: number) => string;
|
|
13
|
+
style?: React.CSSProperties;
|
|
14
|
+
}
|
|
15
|
+
export declare function DonutChart(props: DonutChartProps): JSX.Element;
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { fmtNum, arcPath, polar, seriesColor } from "./chart-geometry.js";
|
|
3
|
+
import { ChartTooltip, wrapStyle } from "./ChartKit.jsx";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Donut / pie. segments: [{label, value, color?}]. The donut keeps a KPI center
|
|
7
|
+
* (total by default, or a fixed centerValue); pie={true} fills to the middle and
|
|
8
|
+
* relies on the tooltip. Hovering lifts a segment and pins a tooltip at its arc.
|
|
9
|
+
*/
|
|
10
|
+
export function DonutChart({ segments = [], size = 180, thickness = 22, pie = false, centerLabel, centerValue, legend = true, interactive = true, valueFormat = fmtNum, style }) {
|
|
11
|
+
const [hover, setHover] = React.useState(null);
|
|
12
|
+
const total = segments.reduce((s, x) => s + x.value, 0) || 1;
|
|
13
|
+
const cx = size / 2, cy = size / 2;
|
|
14
|
+
const rO = size / 2 - 4, rI = pie ? 0 : rO - thickness;
|
|
15
|
+
let a = 0;
|
|
16
|
+
const arcs = segments.map((s, i) => {
|
|
17
|
+
const a0 = a, a1 = a + (s.value / total) * 360;
|
|
18
|
+
a = a1;
|
|
19
|
+
return { s, i, a0, a1, mid: (a0 + a1) / 2 };
|
|
20
|
+
});
|
|
21
|
+
const colorOf = (s, i) => s.color || seriesColor(i);
|
|
22
|
+
const h = hover != null ? arcs[hover] : null;
|
|
23
|
+
/* A pinned centerValue is a stat, not a readout: swapping it on hover made the tile
|
|
24
|
+
look broken, so the tooltip carries per-segment detail and the center stays put. */
|
|
25
|
+
const swapCenter = centerValue == null;
|
|
26
|
+
const tipAt = h ? polar(cx, cy, pie ? rO * 0.62 : (rO + rI) / 2, h.mid) : null;
|
|
27
|
+
return (
|
|
28
|
+
<div style={{ ...wrapStyle, ...style }}>
|
|
29
|
+
<div style={{ display: "flex", alignItems: "center", gap: 18 }}>
|
|
30
|
+
<div style={{ position: "relative", width: size, height: size, flexShrink: 0 }}>
|
|
31
|
+
<svg width={size} height={size} role="img">
|
|
32
|
+
{arcs.map(({ s, i, a0, a1, mid }) => {
|
|
33
|
+
const lift = interactive && hover === i ? 3 : 0;
|
|
34
|
+
const [lx, ly] = polar(0, 0, lift, mid);
|
|
35
|
+
return (
|
|
36
|
+
<path key={i} d={arcPath(cx + lx, cy + ly, rO, rI, a0 + 0.6, Math.max(a0 + 0.8, a1 - 0.6))}
|
|
37
|
+
fill={colorOf(s, i)} opacity={hover == null || hover === i ? 1 : 0.35}
|
|
38
|
+
onMouseEnter={interactive ? () => setHover(i) : undefined} onMouseLeave={interactive ? () => setHover(null) : undefined}
|
|
39
|
+
style={{ transition: "opacity var(--dur-fast) var(--ease-out)", cursor: interactive ? "pointer" : "default" }} />
|
|
40
|
+
);
|
|
41
|
+
})}
|
|
42
|
+
</svg>
|
|
43
|
+
{!pie ? (
|
|
44
|
+
<div style={{ position: "absolute", inset: 0, display: "flex", flexDirection: "column", alignItems: "center", justifyContent: "center", pointerEvents: "none", lineHeight: "normal", gap: 3 }}>
|
|
45
|
+
<span style={{ fontFamily: "var(--font-sans)", fontWeight: "var(--weight-light)", fontSize: Math.round(size / 6.4), lineHeight: 1, color: "var(--text-heading)" }}>
|
|
46
|
+
{h && swapCenter ? valueFormat(h.s.value) : centerValue != null ? centerValue : valueFormat(total)}
|
|
47
|
+
</span>
|
|
48
|
+
<span style={{ fontFamily: "var(--font-sans)", fontSize: "var(--text-2xs)", lineHeight: 1.2, color: "var(--text-muted)", maxWidth: rI * 1.8, textAlign: "center", whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }}>
|
|
49
|
+
{h && swapCenter ? h.s.label : centerLabel || "Total"}
|
|
50
|
+
</span>
|
|
51
|
+
</div>
|
|
52
|
+
) : null}
|
|
53
|
+
{h && interactive ? (
|
|
54
|
+
<ChartTooltip x={tipAt[0]} y={tipAt[1]} flip={tipAt[0] > size * 0.55} title={h.s.label}
|
|
55
|
+
rows={[{ label: "value", value: valueFormat(h.s.value), color: colorOf(h.s, h.i) }, { label: "share", value: ((h.s.value / total) * 100).toFixed(1) + "%" }]} />
|
|
56
|
+
) : null}
|
|
57
|
+
</div>
|
|
58
|
+
{legend ? (
|
|
59
|
+
<div style={{ display: "flex", flexDirection: "column", gap: 6, lineHeight: "normal" }}>
|
|
60
|
+
{segments.map((s, i) => (
|
|
61
|
+
<span key={s.label} onMouseEnter={interactive ? () => setHover(i) : undefined} onMouseLeave={interactive ? () => setHover(null) : undefined}
|
|
62
|
+
style={{ display: "inline-flex", alignItems: "center", gap: 7, cursor: interactive ? "default" : undefined, opacity: hover == null || hover === i ? 1 : 0.45, transition: "var(--t-hover)" }}>
|
|
63
|
+
<span style={{ width: 8, height: 8, borderRadius: 2, background: colorOf(s, i), flexShrink: 0 }} />
|
|
64
|
+
<span style={{ fontFamily: "var(--font-sans)", fontSize: "var(--text-xs)", color: "var(--text-secondary)" }}>{s.label}</span>
|
|
65
|
+
<span style={{ fontFamily: "var(--font-mono)", fontSize: "var(--text-xs)", color: "var(--text-heading)", marginLeft: 6 }}>{((s.value / total) * 100).toFixed(0)}%</span>
|
|
66
|
+
</span>
|
|
67
|
+
))}
|
|
68
|
+
</div>
|
|
69
|
+
) : null}
|
|
70
|
+
</div>
|
|
71
|
+
</div>
|
|
72
|
+
);
|
|
73
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { ref, computed } from 'vue'
|
|
3
|
+
import { fmtNum, arcPath, polar, SERIES } from './chart-geometry.js'
|
|
4
|
+
// Donut with a KPI center (or pie). Hovering a segment or its legend row lifts it.
|
|
5
|
+
const props = defineProps({ segments:{type:Array,default:()=>[]}, size:{type:Number,default:180}, thickness:{type:Number,default:22}, pie:Boolean, centerLabel:String, centerValue:[String,Number], legend:{type:Boolean,default:true}, interactive:{type:Boolean,default:true}, valueFormat:{type:Function,default:fmtNum}, style:Object })
|
|
6
|
+
const hover = ref(null)
|
|
7
|
+
const total = computed(() => props.segments.reduce((s,x)=>s+x.value,0) || 1)
|
|
8
|
+
const arcs = computed(() => {
|
|
9
|
+
const cx=props.size/2, cy=props.size/2, rO=props.size/2-4, rI=props.pie?0:rO-props.thickness
|
|
10
|
+
let a=0
|
|
11
|
+
return props.segments.map((s,i)=>{
|
|
12
|
+
const a0=a, a1=a+(s.value/total.value)*360; a=a1
|
|
13
|
+
const mid=(a0+a1)/2, rad=((mid-90)*Math.PI)/180
|
|
14
|
+
const lift = props.interactive && hover.value===i ? 3 : 0
|
|
15
|
+
return { s, i, mid, color: s.color||SERIES[i%SERIES.length],
|
|
16
|
+
d: arcPath(cx+lift*Math.cos(rad), cy+lift*Math.sin(rad), rO, rI, a0+0.6, Math.max(a0+0.8,a1-0.6)) }
|
|
17
|
+
})
|
|
18
|
+
})
|
|
19
|
+
const rI = computed(() => props.size/2-4-props.thickness)
|
|
20
|
+
const tipStyle = computed(() => {
|
|
21
|
+
const a = arcs.value[hover.value]
|
|
22
|
+
const r = props.pie ? (props.size/2-4)*0.62 : (props.size/2-4+rI.value)/2
|
|
23
|
+
const p = polar(props.size/2, props.size/2, r, a.mid)
|
|
24
|
+
return { position:'absolute', left:p[0]+'px', top:p[1]+'px',
|
|
25
|
+
transform: p[0] > props.size*0.55 ? 'translate(calc(-100% - 10px),-50%)' : 'translate(10px,-50%)',
|
|
26
|
+
pointerEvents:'none', zIndex:5, background:'var(--bg-surface)', border:'1px solid var(--border-card)',
|
|
27
|
+
borderRadius:'var(--radius-md)', boxShadow:'var(--shadow-menu)', padding:'7px 10px', lineHeight:'normal' }
|
|
28
|
+
})
|
|
29
|
+
</script>
|
|
30
|
+
<template>
|
|
31
|
+
<div :style="{display:'inline-block',...style}">
|
|
32
|
+
<div style="display:flex;align-items:center;gap:18px">
|
|
33
|
+
<div :style="{position:'relative',width:size+'px',height:size+'px',flexShrink:0}">
|
|
34
|
+
<svg :width="size" :height="size" role="img">
|
|
35
|
+
<path v-for="a in arcs" :key="a.i" :d="a.d" :fill="a.color" :opacity="hover==null||hover===a.i?1:.35"
|
|
36
|
+
@mouseenter="interactive && (hover=a.i)" @mouseleave="interactive && (hover=null)"
|
|
37
|
+
:style="{transition:'opacity var(--dur-fast) var(--ease-out)',cursor:interactive?'pointer':'default'}"/>
|
|
38
|
+
</svg>
|
|
39
|
+
<!-- A pinned centerValue is a stat, not a readout: the tooltip carries per-segment detail. -->
|
|
40
|
+
<div v-if="!pie" style="position:absolute;inset:0;display:flex;flex-direction:column;align-items:center;justify-content:center;pointer-events:none;line-height:normal;gap:3px">
|
|
41
|
+
<span :style="{fontFamily:'var(--font-sans)',fontWeight:'var(--weight-light)',fontSize:Math.round(size/6.4)+'px',lineHeight:1,color:'var(--text-heading)'}">
|
|
42
|
+
{{ hover!=null && centerValue==null ? valueFormat(segments[hover].value) : (centerValue!=null ? centerValue : valueFormat(total)) }}</span>
|
|
43
|
+
<span :style="{fontFamily:'var(--font-sans)',fontSize:'var(--text-2xs)',lineHeight:1.2,color:'var(--text-muted)',maxWidth:rI*1.8+'px',textAlign:'center',whiteSpace:'nowrap',overflow:'hidden',textOverflow:'ellipsis'}">
|
|
44
|
+
{{ hover!=null && centerValue==null ? segments[hover].label : (centerLabel||'Total') }}</span>
|
|
45
|
+
</div>
|
|
46
|
+
<div v-if="hover!=null && interactive" :style="tipStyle" >
|
|
47
|
+
<div style="font-family:var(--font-sans);font-size:var(--text-2xs);color:var(--text-muted);padding-bottom:3px;white-space:nowrap">{{ segments[hover].label }}</div>
|
|
48
|
+
<div style="font-family:var(--font-mono);font-size:var(--text-xs);color:var(--text-heading);white-space:nowrap">{{ valueFormat(segments[hover].value) }} · {{ ((segments[hover].value/total)*100).toFixed(1) }}%</div>
|
|
49
|
+
</div>
|
|
50
|
+
</div>
|
|
51
|
+
<div v-if="legend" style="display:flex;flex-direction:column;gap:6px">
|
|
52
|
+
<span v-for="a in arcs" :key="a.s.label" @mouseenter="interactive && (hover=a.i)" @mouseleave="interactive && (hover=null)"
|
|
53
|
+
:style="{display:'inline-flex',alignItems:'center',gap:'7px',opacity:hover==null||hover===a.i?1:.45,transition:'var(--t-hover)'}">
|
|
54
|
+
<span :style="{width:'8px',height:'8px',borderRadius:'2px',background:a.color,flexShrink:0}"/>
|
|
55
|
+
<span style="font-family:var(--font-sans);font-size:var(--text-xs);color:var(--text-secondary)">{{ a.s.label }}</span>
|
|
56
|
+
<span style="font-family:var(--font-mono);font-size:var(--text-xs);color:var(--text-heading);margin-left:6px">{{ ((a.s.value/total)*100).toFixed(0) }}%</span>
|
|
57
|
+
</span>
|
|
58
|
+
</div>
|
|
59
|
+
</div>
|
|
60
|
+
</div>
|
|
61
|
+
</template>
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
export interface FunnelStage { label: string; value: number; }
|
|
3
|
+
/** Centered conversion funnel with step-to-step % underneath each band. */
|
|
4
|
+
export interface FunnelChartProps {
|
|
5
|
+
stages?: FunnelStage[];
|
|
6
|
+
width?: number; bandHeight?: number; gap?: number;
|
|
7
|
+
valueFormat?: (v: number) => string;
|
|
8
|
+
interactive?: boolean;
|
|
9
|
+
style?: React.CSSProperties;
|
|
10
|
+
}
|
|
11
|
+
export declare function FunnelChart(props: FunnelChartProps): JSX.Element;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { fmtNum } from "./chart-geometry.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Conversion funnel — onboarding, approval chains, migration stages.
|
|
6
|
+
* stages: [{label, value}]; each band shows its share of stage 1 and the
|
|
7
|
+
* step-to-step conversion underneath.
|
|
8
|
+
*/
|
|
9
|
+
export function FunnelChart({ stages = [], width = 420, bandHeight = 34, gap = 14, valueFormat = fmtNum, interactive = true, style }) {
|
|
10
|
+
const [hover, setHover] = React.useState(null);
|
|
11
|
+
const top = stages[0] ? stages[0].value : 1;
|
|
12
|
+
return (
|
|
13
|
+
<div style={{ width, ...style }}>
|
|
14
|
+
{stages.map((s, i) => {
|
|
15
|
+
const share = s.value / (top || 1);
|
|
16
|
+
const prev = i > 0 ? s.value / (stages[i - 1].value || 1) : null;
|
|
17
|
+
return (
|
|
18
|
+
<div key={s.label} onMouseEnter={interactive ? () => setHover(i) : undefined} onMouseLeave={interactive ? () => setHover(null) : undefined}
|
|
19
|
+
style={{ paddingBottom: i < stages.length - 1 ? gap : 0, opacity: hover == null || hover === i ? 1 : 0.55, transition: "var(--t-hover)" }}>
|
|
20
|
+
<div style={{ display: "flex", justifyContent: "center" }}>
|
|
21
|
+
<div style={{ width: Math.max(12, share * 100) + "%", height: bandHeight, borderRadius: "var(--radius-sm)", background: "var(--series-1)", opacity: 1 - i * (0.55 / Math.max(1, stages.length - 1)), display: "flex", alignItems: "center", justifyContent: "center", gap: 8, minWidth: 150, transition: "width var(--dur) var(--ease-out)" }}>
|
|
22
|
+
<span style={{ fontFamily: "var(--font-sans)", fontSize: "var(--text-xs)", color: "var(--on-accent)", whiteSpace: "nowrap" }}>{s.label}</span>
|
|
23
|
+
<span style={{ fontFamily: "var(--font-mono)", fontSize: "var(--text-xs)", color: "var(--on-accent)" }}>{valueFormat(s.value)}</span>
|
|
24
|
+
{hover === i ? <span style={{ fontFamily: "var(--font-mono)", fontSize: "var(--text-2xs)", color: "var(--on-accent)", opacity: 0.8 }}>{((share) * 100).toFixed(0)}% of top</span> : null}
|
|
25
|
+
</div>
|
|
26
|
+
</div>
|
|
27
|
+
{prev != null ? (
|
|
28
|
+
<div style={{ textAlign: "center", paddingTop: 3, fontFamily: "var(--font-mono)", fontSize: "var(--text-2xs)", color: prev < 0.5 ? "var(--chart-neg)" : "var(--text-muted)" }}>
|
|
29
|
+
↓ {(prev * 100).toFixed(0)}%
|
|
30
|
+
</div>
|
|
31
|
+
) : null}
|
|
32
|
+
</div>
|
|
33
|
+
);
|
|
34
|
+
})}
|
|
35
|
+
</div>
|
|
36
|
+
);
|
|
37
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { computed, ref } from 'vue'
|
|
3
|
+
import { fmtNum } from './chart-geometry.js'
|
|
4
|
+
// Centered conversion funnel with step-to-step % under each band.
|
|
5
|
+
const props = defineProps({ stages:{type:Array,default:()=>[]}, width:{type:Number,default:420}, bandHeight:{type:Number,default:34}, gap:{type:Number,default:14}, valueFormat:{type:Function,default:fmtNum}, interactive:{type:Boolean,default:true}, style:Object })
|
|
6
|
+
const hover = ref(null)
|
|
7
|
+
const rows = computed(() => {
|
|
8
|
+
const top = props.stages[0] ? props.stages[0].value : 1
|
|
9
|
+
return props.stages.map((s,i)=>({ ...s,
|
|
10
|
+
share: Math.max(12, (s.value/(top||1))*100),
|
|
11
|
+
conv: i>0 ? s.value/(props.stages[i-1].value||1) : null,
|
|
12
|
+
opacity: 1 - i*(0.55/Math.max(1,props.stages.length-1)) }))
|
|
13
|
+
})
|
|
14
|
+
</script>
|
|
15
|
+
<template>
|
|
16
|
+
<div :style="{width:width+'px',...style}">
|
|
17
|
+
<div v-for="(s,i) in rows" :key="s.label" @mouseenter="interactive && (hover=i)" @mouseleave="interactive && (hover=null)"
|
|
18
|
+
:style="{paddingBottom:i<rows.length-1?gap+'px':0,opacity:hover==null||hover===i?1:0.55,transition:'var(--t-hover)'}">
|
|
19
|
+
<div style="display:flex;justify-content:center">
|
|
20
|
+
<div :style="{width:s.share+'%',height:bandHeight+'px',borderRadius:'var(--radius-sm)',background:'var(--series-1)',opacity:s.opacity,display:'flex',alignItems:'center',justifyContent:'center',gap:'8px',minWidth:'150px'}">
|
|
21
|
+
<span style="font-family:var(--font-sans);font-size:var(--text-xs);color:var(--on-accent);white-space:nowrap">{{ s.label }}</span>
|
|
22
|
+
<span style="font-family:var(--font-mono);font-size:var(--text-xs);color:var(--on-accent)">{{ valueFormat(s.value) }}</span>
|
|
23
|
+
<span v-if="hover===i" style="font-family:var(--font-mono);font-size:var(--text-2xs);color:var(--on-accent);opacity:.8">{{ s.share.toFixed(0) }}% of top</span>
|
|
24
|
+
</div>
|
|
25
|
+
</div>
|
|
26
|
+
<div v-if="s.conv!=null" :style="{textAlign:'center',paddingTop:'3px',fontFamily:'var(--font-mono)',fontSize:'var(--text-2xs)',color:s.conv<0.5?'var(--chart-neg)':'var(--text-muted)'}">↓ {{ (s.conv*100).toFixed(0) }}%</div>
|
|
27
|
+
</div>
|
|
28
|
+
</div>
|
|
29
|
+
</template>
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
export interface GanttTask {
|
|
3
|
+
label: string;
|
|
4
|
+
/** ISO date or Date */ start: string | Date;
|
|
5
|
+
end?: string | Date;
|
|
6
|
+
/** 0–1 completion fill */ progress?: number;
|
|
7
|
+
/** Color group index */ lane?: number;
|
|
8
|
+
color?: string;
|
|
9
|
+
/** Render as a diamond instead of a bar */ milestone?: boolean;
|
|
10
|
+
}
|
|
11
|
+
/** Timeline of tasks with weekend tinting and a today marker. */
|
|
12
|
+
export interface GanttChartProps {
|
|
13
|
+
tasks?: GanttTask[];
|
|
14
|
+
width?: number; rowHeight?: number;
|
|
15
|
+
/** ISO date or Date for the live marker */ today?: string | Date;
|
|
16
|
+
unit?: "day" | "week";
|
|
17
|
+
interactive?: boolean;
|
|
18
|
+
style?: React.CSSProperties;
|
|
19
|
+
}
|
|
20
|
+
export declare function GanttChart(props: GanttChartProps): JSX.Element;
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { seriesColor } from "./chart-geometry.js";
|
|
3
|
+
import { ChartTooltip, wrapStyle } from "./ChartKit.jsx";
|
|
4
|
+
|
|
5
|
+
const DAY = 86400000;
|
|
6
|
+
const parse = (d) => (d instanceof Date ? d : new Date(d));
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Gantt timeline — close calendars, migration waves, release plans.
|
|
10
|
+
* tasks: [{label, start, end, progress?, lane?, color?, milestone?}] with ISO dates.
|
|
11
|
+
* Weekend columns are tinted, today draws a live marker, and hovering a bar reports
|
|
12
|
+
* its window, duration and progress.
|
|
13
|
+
*/
|
|
14
|
+
export function GanttChart({ tasks = [], width = 660, rowHeight = 26, today, unit = "day", interactive = true, style }) {
|
|
15
|
+
const [hover, setHover] = React.useState(null);
|
|
16
|
+
const labelW = 150;
|
|
17
|
+
const t0 = new Date(Math.min(...tasks.map((t) => parse(t.start))));
|
|
18
|
+
const t1 = new Date(Math.max(...tasks.map((t) => parse(t.end || t.start))));
|
|
19
|
+
const days = Math.max(1, Math.round((t1 - t0) / DAY) + 1);
|
|
20
|
+
const plotW = width - labelW - 8;
|
|
21
|
+
const x = (d) => labelW + ((parse(d) - t0) / (days * DAY)) * plotW;
|
|
22
|
+
const height = tasks.length * rowHeight + 26;
|
|
23
|
+
/* One tick per ~52px of plot: enough room for "Mar 12" without labels touching. */
|
|
24
|
+
const ticks = [];
|
|
25
|
+
const perLabel = 52;
|
|
26
|
+
const stepDays = Math.max(unit === "week" ? 7 : 1, Math.ceil(days / Math.max(2, Math.floor(plotW / perLabel))));
|
|
27
|
+
for (let i = 0; i <= days; i += stepDays) ticks.push(new Date(+t0 + i * DAY));
|
|
28
|
+
const fmt = (d) => d.toLocaleDateString("en-US", { month: "short", day: "numeric" });
|
|
29
|
+
const h = hover != null ? tasks[hover] : null;
|
|
30
|
+
return (
|
|
31
|
+
<div style={{ ...wrapStyle, ...style }}>
|
|
32
|
+
<svg width={width} height={height} role="img">
|
|
33
|
+
{ticks.map((d, i) => (
|
|
34
|
+
<g key={i}>
|
|
35
|
+
<line x1={x(d)} x2={x(d)} y1={20} y2={height - 4} stroke="var(--chart-grid)" strokeWidth="1" />
|
|
36
|
+
<text x={x(d)} y={12} textAnchor="middle" style={{ fontFamily: "var(--font-sans)", fontSize: 9.5, fill: "var(--chart-label)" }}>{fmt(d)}</text>
|
|
37
|
+
</g>
|
|
38
|
+
))}
|
|
39
|
+
{[...Array(days)].map((_, i) => {
|
|
40
|
+
const d = new Date(+t0 + i * DAY);
|
|
41
|
+
const wk = d.getDay() === 0 || d.getDay() === 6;
|
|
42
|
+
return wk ? <rect key={i} x={x(d)} y={20} width={plotW / days} height={height - 24} fill="var(--bg-inset)" opacity="0.6" /> : null;
|
|
43
|
+
})}
|
|
44
|
+
{tasks.map((t, i) => {
|
|
45
|
+
const y = 24 + i * rowHeight;
|
|
46
|
+
const c = t.color || seriesColor(t.lane != null ? t.lane : i);
|
|
47
|
+
const bx = x(t.start), bw = Math.max(4, x(t.end || t.start) - bx + plotW / days);
|
|
48
|
+
const on = hover === i;
|
|
49
|
+
return (
|
|
50
|
+
<g key={t.label + i} onMouseEnter={interactive ? () => setHover(i) : undefined} onMouseLeave={interactive ? () => setHover(null) : undefined}
|
|
51
|
+
opacity={hover == null || on ? 1 : 0.55} style={{ transition: "opacity var(--dur-fast) var(--ease-out)", cursor: interactive ? "default" : undefined }}>
|
|
52
|
+
<text x={0} y={y + rowHeight / 2 + 3} style={{ fontFamily: "var(--font-sans)", fontSize: 11, fill: on ? "var(--text-heading)" : "var(--text-secondary)" }}>
|
|
53
|
+
{t.label.length > 22 ? t.label.slice(0, 21) + "…" : t.label}
|
|
54
|
+
</text>
|
|
55
|
+
{t.milestone ? (
|
|
56
|
+
<path d={"M" + bx + " " + (y + rowHeight / 2 - 6) + " L" + (bx + 6) + " " + (y + rowHeight / 2) + " L" + bx + " " + (y + rowHeight / 2 + 6) + " L" + (bx - 6) + " " + (y + rowHeight / 2) + " Z"} fill={c} />
|
|
57
|
+
) : (
|
|
58
|
+
<>
|
|
59
|
+
<rect x={bx} y={y + 4} width={bw} height={rowHeight - 12} rx="3" fill={c} opacity="0.32" />
|
|
60
|
+
{t.progress != null ? <rect x={bx} y={y + 4} width={Math.max(2, bw * Math.min(1, t.progress))} height={rowHeight - 12} rx="3" fill={c} /> : null}
|
|
61
|
+
</>
|
|
62
|
+
)}
|
|
63
|
+
</g>
|
|
64
|
+
);
|
|
65
|
+
})}
|
|
66
|
+
{today ? <g>
|
|
67
|
+
<line x1={x(today)} x2={x(today)} y1={18} y2={height - 4} stroke="var(--action-primary)" strokeWidth="1.2" />
|
|
68
|
+
<circle cx={x(today)} cy={18} r="2.6" fill="var(--action-primary)" />
|
|
69
|
+
</g> : null}
|
|
70
|
+
</svg>
|
|
71
|
+
{h && interactive ? (
|
|
72
|
+
<ChartTooltip x={x(h.start)} y={24 + hover * rowHeight + rowHeight / 2} flip={x(h.start) > width * 0.6} title={h.label}
|
|
73
|
+
rows={[{ label: "start", value: fmt(parse(h.start)), color: h.color || seriesColor(h.lane != null ? h.lane : hover) },
|
|
74
|
+
{ label: "end", value: fmt(parse(h.end || h.start)) },
|
|
75
|
+
{ label: "duration", value: Math.max(1, Math.round((parse(h.end || h.start) - parse(h.start)) / DAY) + 1) + "d" }]
|
|
76
|
+
.concat(h.progress != null ? [{ label: "progress", value: Math.round(h.progress * 100) + "%" }] : [])} />
|
|
77
|
+
) : null}
|
|
78
|
+
</div>
|
|
79
|
+
);
|
|
80
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { ref, computed } from 'vue'
|
|
3
|
+
import { SERIES } from './chart-geometry.js'
|
|
4
|
+
// Timeline of tasks with weekend tinting, progress fill, today marker and milestones.
|
|
5
|
+
const DAY = 86400000
|
|
6
|
+
const props = defineProps({ tasks:{type:Array,default:()=>[]}, width:{type:Number,default:660}, rowHeight:{type:Number,default:26}, today:[String,Object], unit:{type:String,default:'day'}, interactive:{type:Boolean,default:true}, style:Object })
|
|
7
|
+
const hover = ref(null)
|
|
8
|
+
const parse = d => d instanceof Date ? d : new Date(d)
|
|
9
|
+
const fmt = d => parse(d).toLocaleDateString('en-US',{month:'short',day:'numeric'})
|
|
10
|
+
const g = computed(() => {
|
|
11
|
+
const labelW = 150
|
|
12
|
+
const t0 = new Date(Math.min(...props.tasks.map(t=>parse(t.start))))
|
|
13
|
+
const t1 = new Date(Math.max(...props.tasks.map(t=>parse(t.end||t.start))))
|
|
14
|
+
const days = Math.max(1, Math.round((t1-t0)/DAY)+1)
|
|
15
|
+
const plotW = props.width-labelW-8
|
|
16
|
+
const x = d => labelW + ((parse(d)-t0)/(days*DAY))*plotW
|
|
17
|
+
const height = props.tasks.length*props.rowHeight+26
|
|
18
|
+
// One tick per ~52px of plot: enough room for "Mar 12" without labels touching.
|
|
19
|
+
const stepDays = Math.max(props.unit==='week'?7:1, Math.ceil(days/Math.max(2, Math.floor(plotW/52))))
|
|
20
|
+
const ticks = []
|
|
21
|
+
for (let i=0;i<=days;i+=stepDays) ticks.push(new Date(+t0+i*DAY))
|
|
22
|
+
const weekends = [...Array(days)].map((_,i)=>new Date(+t0+i*DAY)).filter(d=>d.getDay()===0||d.getDay()===6)
|
|
23
|
+
return { labelW, t0, days, plotW, x, height, ticks, weekends, colW: plotW/days,
|
|
24
|
+
rows: props.tasks.map((t,i)=>{
|
|
25
|
+
const bx = x(t.start)
|
|
26
|
+
return { t, i, y: 24+i*props.rowHeight, color: t.color||SERIES[(t.lane!=null?t.lane:i)%SERIES.length],
|
|
27
|
+
bx, bw: Math.max(4, x(t.end||t.start)-bx+plotW/days),
|
|
28
|
+
label: t.label.length>22 ? t.label.slice(0,21)+'…' : t.label,
|
|
29
|
+
dur: Math.max(1, Math.round((parse(t.end||t.start)-parse(t.start))/DAY)+1) }
|
|
30
|
+
}) }
|
|
31
|
+
})
|
|
32
|
+
</script>
|
|
33
|
+
<template>
|
|
34
|
+
<div :style="{position:'relative',display:'inline-block',lineHeight:0,...style}">
|
|
35
|
+
<svg :width="width" :height="g.height" role="img">
|
|
36
|
+
<g v-for="(d,i) in g.ticks" :key="'t'+i">
|
|
37
|
+
<line :x1="g.x(d)" :x2="g.x(d)" :y1="20" :y2="g.height-4" stroke="var(--chart-grid)" stroke-width="1"/>
|
|
38
|
+
<text :x="g.x(d)" :y="12" text-anchor="middle" style="font-family:var(--font-sans);font-size:9.5px;fill:var(--chart-label)">{{ fmt(d) }}</text>
|
|
39
|
+
</g>
|
|
40
|
+
<rect v-for="(d,i) in g.weekends" :key="'w'+i" :x="g.x(d)" :y="20" :width="g.colW" :height="g.height-24" fill="var(--bg-inset)" opacity="0.6"/>
|
|
41
|
+
<g v-for="r in g.rows" :key="r.t.label+r.i" @mouseenter="interactive && (hover=r.i)" @mouseleave="interactive && (hover=null)"
|
|
42
|
+
:opacity="hover==null||hover===r.i?1:0.55" style="transition:opacity var(--dur-fast) var(--ease-out)">
|
|
43
|
+
<text :x="0" :y="r.y+rowHeight/2+3" :style="{fontFamily:'var(--font-sans)',fontSize:'11px',fill:hover===r.i?'var(--text-heading)':'var(--text-secondary)'}">{{ r.label }}</text>
|
|
44
|
+
<path v-if="r.t.milestone" :d="'M'+r.bx+' '+(r.y+rowHeight/2-6)+' L'+(r.bx+6)+' '+(r.y+rowHeight/2)+' L'+r.bx+' '+(r.y+rowHeight/2+6)+' L'+(r.bx-6)+' '+(r.y+rowHeight/2)+' Z'" :fill="r.color"/>
|
|
45
|
+
<template v-else>
|
|
46
|
+
<rect :x="r.bx" :y="r.y+4" :width="r.bw" :height="rowHeight-12" rx="3" :fill="r.color" opacity="0.32"/>
|
|
47
|
+
<rect v-if="r.t.progress!=null" :x="r.bx" :y="r.y+4" :width="Math.max(2,r.bw*Math.min(1,r.t.progress))" :height="rowHeight-12" rx="3" :fill="r.color"/>
|
|
48
|
+
</template>
|
|
49
|
+
</g>
|
|
50
|
+
<g v-if="today">
|
|
51
|
+
<line :x1="g.x(today)" :x2="g.x(today)" :y1="18" :y2="g.height-4" stroke="var(--action-primary)" stroke-width="1.2"/>
|
|
52
|
+
<circle :cx="g.x(today)" :cy="18" r="2.6" fill="var(--action-primary)"/>
|
|
53
|
+
</g>
|
|
54
|
+
</svg>
|
|
55
|
+
<div v-if="hover!=null && interactive" :style="{position:'absolute',left:g.rows[hover].bx+'px',top:(g.rows[hover].y+rowHeight/2)+'px',transform:g.rows[hover].bx>width*0.6?'translate(calc(-100% - 10px),-50%)':'translate(10px,-50%)',pointerEvents:'none',zIndex:5,background:'var(--bg-surface)',border:'1px solid var(--border-card)',borderRadius:'var(--radius-md)',boxShadow:'var(--shadow-menu)',padding:'7px 10px',lineHeight:'normal',whiteSpace:'nowrap'}">
|
|
56
|
+
<div style="font-family:var(--font-sans);font-size:var(--text-2xs);color:var(--text-muted);padding-bottom:3px">{{ tasks[hover].label }}</div>
|
|
57
|
+
<div style="font-family:var(--font-mono);font-size:var(--text-xs);color:var(--text-heading)">{{ fmt(tasks[hover].start) }} → {{ fmt(tasks[hover].end||tasks[hover].start) }} · {{ g.rows[hover].dur }}d<span v-if="tasks[hover].progress!=null"> · {{ Math.round(tasks[hover].progress*100) }}%</span></div>
|
|
58
|
+
</div>
|
|
59
|
+
</div>
|
|
60
|
+
</template>
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
export interface GaugeBand { upTo: number; color: string; }
|
|
3
|
+
/** Semi-circular KPI gauge — flat arc fill, no needle. bands recolor by threshold. */
|
|
4
|
+
export interface GaugeChartProps {
|
|
5
|
+
value?: number; max?: number;
|
|
6
|
+
size?: number; thickness?: number;
|
|
7
|
+
label?: string;
|
|
8
|
+
valueFormat?: (v: number) => string;
|
|
9
|
+
bands?: GaugeBand[];
|
|
10
|
+
interactive?: boolean;
|
|
11
|
+
style?: React.CSSProperties;
|
|
12
|
+
}
|
|
13
|
+
export declare function GaugeChart(props: GaugeChartProps): JSX.Element;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { fmtNum, arcPath } from "./chart-geometry.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Semi-circular gauge KPI — utilization, data quality, SLA. Flat arc fill over a track
|
|
6
|
+
* (no needle). bands: [{upTo, color}] recolor the fill by threshold; big light numeral.
|
|
7
|
+
*/
|
|
8
|
+
export function GaugeChart({ value = 0, max = 100, size = 180, thickness = 14, label, valueFormat = (v) => fmtNum(v, { pct: true }), bands, interactive = true, style }) {
|
|
9
|
+
const [hover, setHover] = React.useState(false);
|
|
10
|
+
const rO = size / 2 - 4, rI = rO - thickness;
|
|
11
|
+
const cx = size / 2, cy = rO + 4;
|
|
12
|
+
const H = cy + 18;
|
|
13
|
+
const t = Math.max(0, Math.min(1, value / max));
|
|
14
|
+
const band = bands && bands.find((b) => value <= b.upTo);
|
|
15
|
+
const color = (band && band.color) || "var(--series-1)";
|
|
16
|
+
const A0 = -90;
|
|
17
|
+
return (
|
|
18
|
+
<div style={{ position: "relative", width: size, height: H, display: "inline-block", ...style }}>
|
|
19
|
+
<svg width={size} height={H} role="img">
|
|
20
|
+
<path d={arcPath(cx, cy, rO, rI, A0, 90)} fill="var(--chart-track)" />
|
|
21
|
+
{t > 0 ? <path d={arcPath(cx, cy, rO, rI, A0, A0 + t * 180)} fill={color}
|
|
22
|
+
onMouseEnter={interactive ? () => setHover(true) : undefined} onMouseLeave={interactive ? () => setHover(false) : undefined}
|
|
23
|
+
style={{ filter: hover ? "brightness(1.1)" : "none", transition: "filter var(--dur-fast) var(--ease-out)" }} /> : null}
|
|
24
|
+
<text x={cx - rO + thickness / 2} y={cy + 13} textAnchor="middle" style={{ fontFamily: "var(--font-mono)", fontSize: 9, fill: "var(--chart-label)" }}>0</text>
|
|
25
|
+
<text x={cx + rO - thickness / 2} y={cy + 13} textAnchor="middle" style={{ fontFamily: "var(--font-mono)", fontSize: 9, fill: "var(--chart-label)" }}>{fmtNum(max)}</text>
|
|
26
|
+
</svg>
|
|
27
|
+
<div style={{ position: "absolute", left: 0, right: 0, top: cy - size / 7, textAlign: "center", pointerEvents: "none" }}>
|
|
28
|
+
<div style={{ fontFamily: "var(--font-sans)", fontWeight: "var(--weight-light)", fontSize: size / 7, lineHeight: 1, color: "var(--text-heading)" }}>{valueFormat(value)}</div>
|
|
29
|
+
{label ? <div style={{ fontFamily: "var(--font-sans)", fontSize: "var(--text-2xs)", color: "var(--text-muted)", paddingTop: 3 }}>{hover ? Math.round(t * 100) + "% of " + fmtNum(max) : label}</div> : null}
|
|
30
|
+
</div>
|
|
31
|
+
</div>
|
|
32
|
+
);
|
|
33
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { computed, ref } from 'vue'
|
|
3
|
+
import { fmtNum, arcPath } from './chart-geometry.js'
|
|
4
|
+
// Semi-circular KPI gauge — flat arc fill, no needle. bands recolor by threshold.
|
|
5
|
+
const props = defineProps({ value:{type:Number,default:0}, max:{type:Number,default:100}, size:{type:Number,default:180}, thickness:{type:Number,default:14}, label:String, valueFormat:{type:Function,default:(v)=>fmtNum(v,{pct:true})}, bands:Array, interactive:{type:Boolean,default:true}, style:Object })
|
|
6
|
+
const hover = ref(false)
|
|
7
|
+
const g = computed(() => {
|
|
8
|
+
const rO = props.size/2-4, rI = rO-props.thickness, cx = props.size/2, cy = rO+4, H = cy+18
|
|
9
|
+
const t = Math.max(0, Math.min(1, props.value/props.max))
|
|
10
|
+
const band = props.bands && props.bands.find(b=>props.value<=b.upTo)
|
|
11
|
+
return { rO, rI, cx, cy, H, t, color:(band&&band.color)||'var(--series-1)',
|
|
12
|
+
track: arcPath(cx,cy,rO,rI,-90,90), fill: t>0 ? arcPath(cx,cy,rO,rI,-90,-90+t*180) : null }
|
|
13
|
+
})
|
|
14
|
+
</script>
|
|
15
|
+
<template>
|
|
16
|
+
<div :style="{position:'relative',width:size+'px',height:g.H+'px',display:'inline-block',...style}">
|
|
17
|
+
<svg :width="size" :height="g.H" role="img">
|
|
18
|
+
<path :d="g.track" fill="var(--chart-track)"/>
|
|
19
|
+
<path v-if="g.fill" :d="g.fill" :fill="g.color" @mouseenter="interactive && (hover=true)" @mouseleave="interactive && (hover=false)"
|
|
20
|
+
:style="{filter:hover?'brightness(1.1)':'none',transition:'filter var(--dur-fast) var(--ease-out)'}"/>
|
|
21
|
+
<text :x="g.cx-g.rO+thickness/2" :y="g.cy+13" text-anchor="middle" style="font-family:var(--font-mono);font-size:9px;fill:var(--chart-label)">0</text>
|
|
22
|
+
<text :x="g.cx+g.rO-thickness/2" :y="g.cy+13" text-anchor="middle" style="font-family:var(--font-mono);font-size:9px;fill:var(--chart-label)">{{ fmtNum(max) }}</text>
|
|
23
|
+
</svg>
|
|
24
|
+
<div :style="{position:'absolute',left:0,right:0,top:(g.cy-size/7)+'px',textAlign:'center',pointerEvents:'none'}">
|
|
25
|
+
<div :style="{fontFamily:'var(--font-sans)',fontWeight:'var(--weight-light)',fontSize:size/7+'px',lineHeight:1,color:'var(--text-heading)'}">{{ valueFormat(value) }}</div>
|
|
26
|
+
<div v-if="label" style="font-family:var(--font-sans);font-size:var(--text-2xs);color:var(--text-muted);padding-top:3px">{{ hover ? Math.round(g.t*100)+'% of '+max : label }}</div>
|
|
27
|
+
</div>
|
|
28
|
+
</div>
|
|
29
|
+
</template>
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
/** Matrix heatmap. values[row][col]; scale "blue" (sequential) or "diverging" (red/green variance). */
|
|
3
|
+
export interface HeatmapChartProps {
|
|
4
|
+
xLabels?: string[]; yLabels?: string[];
|
|
5
|
+
values?: (number | null)[][];
|
|
6
|
+
width?: number; cellHeight?: number; gap?: number;
|
|
7
|
+
scale?: "blue" | "diverging";
|
|
8
|
+
/** Print the number in each cell */ showValues?: boolean;
|
|
9
|
+
valueFormat?: (v: number) => string;
|
|
10
|
+
interactive?: boolean;
|
|
11
|
+
style?: React.CSSProperties;
|
|
12
|
+
}
|
|
13
|
+
export declare function HeatmapChart(props: HeatmapChartProps): JSX.Element;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { fmtNum, extent, heatStep } from "./chart-geometry.js";
|
|
3
|
+
import { ChartTooltip, wrapStyle } from "./ChartKit.jsx";
|
|
4
|
+
|
|
5
|
+
const HEAT = ["var(--chart-heat-1)","var(--chart-heat-2)","var(--chart-heat-3)","var(--chart-heat-4)","var(--chart-heat-5)"];
|
|
6
|
+
/**
|
|
7
|
+
* Matrix heatmap — entity × month, hour × weekday. values[row][col].
|
|
8
|
+
* scale "blue" (sequential) or "diverging" (negative red / positive green, for variance).
|
|
9
|
+
* showValues prints the number in each cell (md+ sizes).
|
|
10
|
+
*/
|
|
11
|
+
export function HeatmapChart({ xLabels = [], yLabels = [], values = [], width = 560, cellHeight = 26, gap = 2, scale = "blue", showValues = false, valueFormat = fmtNum, interactive = true, style }) {
|
|
12
|
+
const [hover, setHover] = React.useState(null);
|
|
13
|
+
const labelW = 86;
|
|
14
|
+
const cols = xLabels.length, rows = yLabels.length;
|
|
15
|
+
const cw = (width - labelW) / cols - gap;
|
|
16
|
+
const [lo, hi] = extent(values, false);
|
|
17
|
+
const amax = Math.max(Math.abs(lo), Math.abs(hi)) || 1;
|
|
18
|
+
const cellFill = (v) => {
|
|
19
|
+
if (v == null) return "var(--chart-track)";
|
|
20
|
+
if (scale === "diverging") {
|
|
21
|
+
const t = Math.min(1, Math.abs(v) / amax);
|
|
22
|
+
const a = (0.15 + t * 0.85).toFixed(2);
|
|
23
|
+
return v >= 0 ? `color-mix(in srgb, var(--chart-pos) ${Math.round(a * 100)}%, var(--bg-surface))` : `color-mix(in srgb, var(--chart-neg) ${Math.round(a * 100)}%, var(--bg-surface))`;
|
|
24
|
+
}
|
|
25
|
+
return HEAT[heatStep(v, lo, hi)];
|
|
26
|
+
};
|
|
27
|
+
const textOn = (v) => {
|
|
28
|
+
if (v == null) return "var(--text-muted)";
|
|
29
|
+
if (scale === "diverging") return Math.abs(v) / amax > 0.55 ? "var(--on-accent)" : "var(--text-heading)";
|
|
30
|
+
return heatStep(v, lo, hi) >= 3 ? "var(--chart-heat-ink)" : "var(--text-heading)";
|
|
31
|
+
};
|
|
32
|
+
const h = hover ? { v: values[hover.r] && values[hover.r][hover.c] } : null;
|
|
33
|
+
return (
|
|
34
|
+
<div style={{ ...wrapStyle, width, ...style }}>
|
|
35
|
+
<div style={{ display: "grid", gridTemplateColumns: `${labelW}px repeat(${cols}, 1fr)`, gap: gap + "px", lineHeight: "normal" }}>
|
|
36
|
+
<span />
|
|
37
|
+
{xLabels.map((x) => <span key={x} style={{ fontFamily: "var(--font-sans)", fontSize: 10, color: "var(--chart-label)", textAlign: "center", paddingBottom: 2 }}>{x}</span>)}
|
|
38
|
+
{yLabels.map((y, r) => (
|
|
39
|
+
<React.Fragment key={y}>
|
|
40
|
+
<span style={{ fontFamily: "var(--font-sans)", fontSize: "var(--text-xs)", color: "var(--text-secondary)", display: "flex", alignItems: "center", whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis", paddingRight: 6 }}>{y}</span>
|
|
41
|
+
{xLabels.map((x, c) => {
|
|
42
|
+
const v = values[r] ? values[r][c] : null;
|
|
43
|
+
const on = hover && hover.r === r && hover.c === c;
|
|
44
|
+
return (
|
|
45
|
+
<span key={c}
|
|
46
|
+
onMouseEnter={interactive ? () => setHover({ r, c }) : undefined} onMouseLeave={interactive ? () => setHover(null) : undefined}
|
|
47
|
+
style={{ height: cellHeight, borderRadius: 4, background: cellFill(v), display: "flex", alignItems: "center", justifyContent: "center",
|
|
48
|
+
outline: on ? "2px solid var(--focus-border)" : "none", outlineOffset: -1, transition: "var(--t-hover)", cursor: interactive ? "default" : undefined }}>
|
|
49
|
+
{showValues && v != null ? <span style={{ fontFamily: "var(--font-mono)", fontSize: 10, color: textOn(v) }}>{valueFormat(v)}</span> : null}
|
|
50
|
+
</span>
|
|
51
|
+
);
|
|
52
|
+
})}
|
|
53
|
+
</React.Fragment>
|
|
54
|
+
))}
|
|
55
|
+
</div>
|
|
56
|
+
{hover && interactive ? (
|
|
57
|
+
<ChartTooltip x={labelW + (hover.c + 0.5) * (cw + gap)} y={20 + (hover.r + 0.5) * (cellHeight + gap)} flip={hover.c > cols * 0.62}
|
|
58
|
+
title={yLabels[hover.r] + " · " + xLabels[hover.c]} rows={[{ label: "value", value: valueFormat(h.v) }]} />
|
|
59
|
+
) : null}
|
|
60
|
+
</div>
|
|
61
|
+
);
|
|
62
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { ref, computed } from 'vue'
|
|
3
|
+
import { fmtNum, extent, heatStep } from './chart-geometry.js'
|
|
4
|
+
// Matrix heatmap. scale "blue" (sequential) or "diverging" (red/green variance).
|
|
5
|
+
const HEAT = ['var(--chart-heat-1)','var(--chart-heat-2)','var(--chart-heat-3)','var(--chart-heat-4)','var(--chart-heat-5)']
|
|
6
|
+
const props = defineProps({ xLabels:{type:Array,default:()=>[]}, yLabels:{type:Array,default:()=>[]}, values:{type:Array,default:()=>[]}, width:{type:Number,default:560}, cellHeight:{type:Number,default:26}, gap:{type:Number,default:2}, scale:{type:String,default:'blue'}, showValues:Boolean, valueFormat:{type:Function,default:fmtNum}, interactive:{type:Boolean,default:true}, style:Object })
|
|
7
|
+
const hover = ref(null)
|
|
8
|
+
const dom = computed(() => { const [lo,hi] = extent(props.values,false); return { lo, hi, amax: Math.max(Math.abs(lo),Math.abs(hi))||1 } })
|
|
9
|
+
function fill(v){
|
|
10
|
+
if (v==null) return 'var(--chart-track)'
|
|
11
|
+
if (props.scale==='diverging'){
|
|
12
|
+
const a = Math.round((0.15+Math.min(1,Math.abs(v)/dom.value.amax)*0.85)*100)
|
|
13
|
+
return (v>=0?`color-mix(in srgb, var(--chart-pos) ${a}%, var(--bg-surface))`:`color-mix(in srgb, var(--chart-neg) ${a}%, var(--bg-surface))`)
|
|
14
|
+
}
|
|
15
|
+
return HEAT[heatStep(v, dom.value.lo, dom.value.hi)]
|
|
16
|
+
}
|
|
17
|
+
function ink(v){
|
|
18
|
+
if (v==null) return 'var(--text-muted)'
|
|
19
|
+
if (props.scale==='diverging') return Math.abs(v)/dom.value.amax>0.55 ? 'var(--on-accent)' : 'var(--text-heading)'
|
|
20
|
+
return heatStep(v, dom.value.lo, dom.value.hi)>=3 ? 'var(--chart-heat-ink)' : 'var(--text-heading)'
|
|
21
|
+
}
|
|
22
|
+
</script>
|
|
23
|
+
<template>
|
|
24
|
+
<div :style="{width:width+'px',...style}">
|
|
25
|
+
<div :style="{display:'grid',gridTemplateColumns:'86px repeat('+xLabels.length+', 1fr)',gap:gap+'px'}">
|
|
26
|
+
<span/>
|
|
27
|
+
<span v-for="x in xLabels" :key="x" style="font-family:var(--font-sans);font-size:10px;color:var(--chart-label);text-align:center;padding-bottom:2px">{{ x }}</span>
|
|
28
|
+
<template v-for="(y,r) in yLabels" :key="y">
|
|
29
|
+
<span style="font-family:var(--font-sans);font-size:var(--text-xs);color:var(--text-secondary);display:flex;align-items:center;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;padding-right:6px">{{ y }}</span>
|
|
30
|
+
<span v-for="(x,c) in xLabels" :key="c" :title="y+' · '+x+(values[r]&&values[r][c]!=null?' = '+valueFormat(values[r][c]):'')"
|
|
31
|
+
@mouseenter="interactive && (hover={r,c})" @mouseleave="interactive && (hover=null)"
|
|
32
|
+
:style="{height:cellHeight+'px',borderRadius:'4px',background:fill(values[r]?values[r][c]:null),display:'flex',alignItems:'center',justifyContent:'center',outline:hover&&hover.r===r&&hover.c===c?'2px solid var(--focus-border)':'none',outlineOffset:'-1px',transition:'var(--t-hover)'}">
|
|
33
|
+
<span v-if="showValues && values[r] && values[r][c]!=null" :style="{fontFamily:'var(--font-mono)',fontSize:'10px',color:ink(values[r][c])}">{{ valueFormat(values[r][c]) }}</span>
|
|
34
|
+
</span>
|
|
35
|
+
</template>
|
|
36
|
+
</div>
|
|
37
|
+
</div>
|
|
38
|
+
</template>
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
export interface HistogramBucket { from: number; to: number; n: number; }
|
|
3
|
+
/** Distribution of one measure — pass raw values (auto-bucketed) or pre-computed buckets. */
|
|
4
|
+
export interface HistogramProps {
|
|
5
|
+
values?: number[];
|
|
6
|
+
/** Pre-computed buckets; wins over values */ buckets?: HistogramBucket[];
|
|
7
|
+
bins?: number;
|
|
8
|
+
width?: number; height?: number;
|
|
9
|
+
axes?: boolean;
|
|
10
|
+
/** Dashed mean guide */ showMean?: boolean;
|
|
11
|
+
valueFormat?: (v: number) => string;
|
|
12
|
+
countFormat?: (v: number) => string;
|
|
13
|
+
interactive?: boolean; color?: string;
|
|
14
|
+
style?: React.CSSProperties;
|
|
15
|
+
}
|
|
16
|
+
export declare function Histogram(props: HistogramProps): JSX.Element;
|