@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,93 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { fmtNum, seriesColor, stack, niceTicks } from "./chart-geometry.js";
|
|
3
|
+
import { makeProj, cuboidFaces, shade, dragRotate, facePath } from "./chart-3d.js";
|
|
4
|
+
import { ChartTooltip, wrapStyle } from "./ChartKit.jsx";
|
|
5
|
+
|
|
6
|
+
const FACE_SHADE = { top: 0.22, front: 0, right: -0.22, left: -0.3, back: -0.1, bottom: -0.4 };
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* 3D bars — categories along x, series as depth rows (or stacked in place). Drag to orbit.
|
|
10
|
+
* mode "grouped" (one depth row per series) | "stacked" (segments pile up in one row).
|
|
11
|
+
*/
|
|
12
|
+
export function Bar3D({ series = [], categories = [], width = 460, height = 300, yaw: yaw0 = 30, pitch: pitch0 = 26, mode = "grouped", gap = 0.35, interactive = true, valueFormat = fmtNum, style }) {
|
|
13
|
+
const [rot, setRot] = React.useState({ yaw: yaw0, pitch: pitch0 });
|
|
14
|
+
const [hover, setHover] = React.useState(null);
|
|
15
|
+
const n = categories.length || Math.max(...series.map((s) => s.values.length));
|
|
16
|
+
const m = mode === "grouped" ? series.length : 1;
|
|
17
|
+
const hi = mode === "stacked"
|
|
18
|
+
? Math.max(...[...Array(n)].map((_, i) => series.reduce((t, s) => t + (s.values[i] || 0), 0)))
|
|
19
|
+
: Math.max(...series.flatMap((s) => s.values));
|
|
20
|
+
const bands = mode === "stacked" ? stack(series.map((s) => s.values)) : null;
|
|
21
|
+
const scale = Math.min(width, height) * 0.42;
|
|
22
|
+
const P = makeProj(width / 2, height / 2 + height * 0.06, scale, rot.yaw, rot.pitch);
|
|
23
|
+
const bw = (1 - gap) / n, bd = (1 - gap) / Math.max(m, 2);
|
|
24
|
+
const bars = [];
|
|
25
|
+
series.forEach((s, si) => s.values.forEach((v, i) => {
|
|
26
|
+
if (v == null) return;
|
|
27
|
+
const x0 = i / n - 0.5 + (gap / n) / 2, x1 = x0 + bw / (n / (n)) * (1 / 1) * (bw > 0 ? 1 : 1) * 1;
|
|
28
|
+
const cx0 = i / n - 0.5 + (1 / n) * (gap / 2), cx1 = cx0 + (1 / n) * (1 - gap);
|
|
29
|
+
const zi = mode === "grouped" ? si : 0;
|
|
30
|
+
const cz0 = zi / Math.max(m, 1) - 0.5 + (1 / Math.max(m, 2)) * (gap / 2), cz1 = cz0 + (1 / Math.max(m, 2)) * (1 - gap);
|
|
31
|
+
const y0 = mode === "stacked" ? (bands[si][i][0] / hi) : 0;
|
|
32
|
+
const y1 = mode === "stacked" ? (bands[si][i][1] / hi) : v / hi;
|
|
33
|
+
bars.push({ s, si, i, v, x0: cx0, x1: cx1, z0: cz0, z1: cz1, y0: (y0 - 0.5) * 0.9, y1: (y1 - 0.5) * 0.9 });
|
|
34
|
+
}));
|
|
35
|
+
// draw far-to-near: sort by projected center depth
|
|
36
|
+
const drawn = bars.map((b, key) => {
|
|
37
|
+
const faces = cuboidFaces(P, b.x0, b.x1, b.y0, b.y1, b.z0, b.z1);
|
|
38
|
+
const depth = faces.reduce((s2, f) => s2 + f.depth, 0) / faces.length;
|
|
39
|
+
const top = P((b.x0 + b.x1) / 2, b.y1, (b.z0 + b.z1) / 2);
|
|
40
|
+
return { ...b, key, faces, depth, top, color: b.s.color || seriesColor(b.si) };
|
|
41
|
+
}).sort((a, b2) => b2.depth - a.depth);
|
|
42
|
+
const base = [P(-0.5, -0.46, -0.5), P(0.5, -0.46, -0.5), P(0.5, -0.46, 0.5), P(-0.5, -0.46, 0.5)];
|
|
43
|
+
/* Vertical value axis with real ticks at the front-left corner. */
|
|
44
|
+
const vt = niceTicks(0, hi, 3);
|
|
45
|
+
const vTicks = vt.ticks.filter((t) => t <= hi * 1.001).map((t) => ({ p: P(-0.5, (t / hi - 0.5) * 0.9, 0.5), v: valueFormat(t) }));
|
|
46
|
+
const vAxis = [P(-0.5, -0.45, 0.5), P(-0.5, 0.45, 0.5)];
|
|
47
|
+
const handlers = interactive ? dragRotate(() => rot, setRot) : {};
|
|
48
|
+
return (
|
|
49
|
+
<div style={{ ...wrapStyle, ...style }}>
|
|
50
|
+
<svg width={width} height={height} {...handlers} role="img" style={{ cursor: interactive ? "grab" : "default", touchAction: "none" }}>
|
|
51
|
+
<path d={facePath(base)} fill="var(--bg-inset)" stroke="var(--chart-axis)" strokeWidth="1" />
|
|
52
|
+
<line x1={vAxis[0].x} y1={vAxis[0].y} x2={vAxis[1].x} y2={vAxis[1].y} stroke="var(--chart-guide)" strokeWidth="1" />
|
|
53
|
+
{vTicks.map((t, i) => (
|
|
54
|
+
<g key={"vt" + i}>
|
|
55
|
+
<line x1={t.p.x - 4} y1={t.p.y} x2={t.p.x} y2={t.p.y} stroke="var(--chart-guide)" strokeWidth="1" />
|
|
56
|
+
<text x={t.p.x - 7} y={t.p.y + 3} textAnchor="end" style={{ fontFamily: "var(--font-mono)", fontSize: 9, fill: "var(--chart-label)" }}>{t.v}</text>
|
|
57
|
+
</g>
|
|
58
|
+
))}
|
|
59
|
+
{categories.map((c, i) => {
|
|
60
|
+
/* Labels follow whichever base edge currently faces the viewer (larger screen y),
|
|
61
|
+
sit outside it, and paint BEFORE the bars, so they never cover a column. */
|
|
62
|
+
const zE = P(0, -0.46, 0.5).y >= P(0, -0.46, -0.5).y ? 0.5 : -0.5;
|
|
63
|
+
const p0 = P(i / n - 0.5 + 0.5 / n, -0.46, zE);
|
|
64
|
+
const ctr = P(0, -0.46, 0);
|
|
65
|
+
const dx = p0.x - ctr.x, dy = p0.y - ctr.y, len = Math.hypot(dx, dy) || 1;
|
|
66
|
+
return <text key={"c" + i} x={p0.x + (dx / len) * 18} y={p0.y + (dy / len) * 18 + 3} textAnchor="middle" style={{ fontFamily: "var(--font-sans)", fontSize: 10, fill: "var(--chart-label)" }}>{c}</text>;
|
|
67
|
+
})}
|
|
68
|
+
{drawn.map((b) => (
|
|
69
|
+
<g key={b.key} opacity={hover == null || hover === b.key ? 1 : 0.45}
|
|
70
|
+
onMouseEnter={interactive ? () => setHover(b.key) : undefined} onMouseLeave={interactive ? () => setHover(null) : undefined}
|
|
71
|
+
style={{ transition: "opacity var(--dur-fast) var(--ease-out)" }}>
|
|
72
|
+
{b.faces.map((f, fi) => (
|
|
73
|
+
<path key={fi} d={f.d} fill={shade(b.color, FACE_SHADE[f.kind])} stroke="var(--bg-surface)" strokeWidth="0.4" />
|
|
74
|
+
))}
|
|
75
|
+
</g>
|
|
76
|
+
))}
|
|
77
|
+
</svg>
|
|
78
|
+
{hover != null && interactive ? (() => {
|
|
79
|
+
const b = drawn.find((x) => x.key === hover);
|
|
80
|
+
return <ChartTooltip x={b.top.x} y={b.top.y} flip={b.top.x > width * 0.6} title={categories[b.i]}
|
|
81
|
+
rows={[{ label: b.s.name, value: valueFormat(b.v), color: b.color }]} />;
|
|
82
|
+
})() : null}
|
|
83
|
+
<div style={{ display: "flex", gap: 14, paddingTop: 6, lineHeight: "normal" }}>
|
|
84
|
+
{series.map((s, i) => (
|
|
85
|
+
<span key={s.name} style={{ display: "inline-flex", alignItems: "center", gap: 6 }}>
|
|
86
|
+
<span style={{ width: 8, height: 8, borderRadius: 2, background: s.color || seriesColor(i) }} />
|
|
87
|
+
<span style={{ fontFamily: "var(--font-sans)", fontSize: "var(--text-xs)", color: "var(--text-secondary)" }}>{s.name}</span>
|
|
88
|
+
</span>
|
|
89
|
+
))}
|
|
90
|
+
</div>
|
|
91
|
+
</div>
|
|
92
|
+
);
|
|
93
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { ref, computed } from 'vue'
|
|
3
|
+
import { fmtNum, SERIES, stack, niceTicks } from './chart-geometry.js'
|
|
4
|
+
import { makeProj, cuboidFaces, shade, facePath } from './chart-3d.js'
|
|
5
|
+
// 3D cuboid bars — categories on x, series as depth rows or stacked; drag to orbit.
|
|
6
|
+
const FACE_SHADE = { top:0.22, front:0, right:-0.22, left:-0.3, back:-0.1, bottom:-0.4 }
|
|
7
|
+
const props = defineProps({ series:{type:Array,default:()=>[]}, categories:{type:Array,default:()=>[]}, mode:{type:String,default:'grouped'}, width:{type:Number,default:460}, height:{type:Number,default:300}, yaw:{type:Number,default:30}, pitch:{type:Number,default:26}, gap:{type:Number,default:0.35}, interactive:{type:Boolean,default:true}, valueFormat:{type:Function,default:fmtNum}, style:Object })
|
|
8
|
+
const rot = ref({ yaw: props.yaw, pitch: props.pitch })
|
|
9
|
+
const hover = ref(null)
|
|
10
|
+
function down(e){ e.currentTarget.__kbDrag = { x:e.clientX, y:e.clientY, ...rot.value }; e.currentTarget.setPointerCapture && e.currentTarget.setPointerCapture(e.pointerId) }
|
|
11
|
+
function move(e){ const s = e.currentTarget.__kbDrag; if(!s) return; rot.value = { yaw: s.yaw+(e.clientX-s.x)*0.45, pitch: Math.max(8, Math.min(80, s.pitch+(e.clientY-s.y)*0.35)) } }
|
|
12
|
+
function up(e){ e.currentTarget.__kbDrag = null }
|
|
13
|
+
const g = computed(() => {
|
|
14
|
+
const n = props.categories.length || Math.max(...props.series.map(s=>s.values.length))
|
|
15
|
+
const m = props.mode==='grouped' ? props.series.length : 1
|
|
16
|
+
const hi = props.mode==='stacked'
|
|
17
|
+
? Math.max(...[...Array(n)].map((_,i)=>props.series.reduce((t,s)=>t+(s.values[i]||0),0)))
|
|
18
|
+
: Math.max(...props.series.flatMap(s=>s.values))
|
|
19
|
+
const bands = props.mode==='stacked' ? stack(props.series.map(s=>s.values)) : null
|
|
20
|
+
const scale = Math.min(props.width, props.height)*0.42
|
|
21
|
+
const P = makeProj(props.width/2, props.height/2 + props.height*0.06, scale, rot.value.yaw, rot.value.pitch)
|
|
22
|
+
const bars = []
|
|
23
|
+
props.series.forEach((s,si)=>s.values.forEach((v,i)=>{
|
|
24
|
+
if (v==null) return
|
|
25
|
+
const cx0 = i/n-0.5+(1/n)*(props.gap/2), cx1 = cx0+(1/n)*(1-props.gap)
|
|
26
|
+
const zi = props.mode==='grouped' ? si : 0
|
|
27
|
+
const cz0 = zi/Math.max(m,1)-0.5+(1/Math.max(m,2))*(props.gap/2), cz1 = cz0+(1/Math.max(m,2))*(1-props.gap)
|
|
28
|
+
const y0 = props.mode==='stacked' ? (bands[si][i][0]/hi) : 0
|
|
29
|
+
const y1 = props.mode==='stacked' ? (bands[si][i][1]/hi) : v/hi
|
|
30
|
+
bars.push({ s, si, i, v, x0:cx0, x1:cx1, z0:cz0, z1:cz1, y0:(y0-0.5)*0.9, y1:(y1-0.5)*0.9 })
|
|
31
|
+
}))
|
|
32
|
+
const drawn = bars.map((b,key)=>{
|
|
33
|
+
const faces = cuboidFaces(P, b.x0, b.x1, b.y0, b.y1, b.z0, b.z1)
|
|
34
|
+
return { ...b, key, faces, depth: faces.reduce((s2,f)=>s2+f.depth,0)/faces.length,
|
|
35
|
+
top: P((b.x0+b.x1)/2, b.y1, (b.z0+b.z1)/2), color: b.s.color || SERIES[b.si%SERIES.length] }
|
|
36
|
+
}).sort((a,b2)=>b2.depth-a.depth)
|
|
37
|
+
const base = [P(-0.5,-0.46,-0.5), P(0.5,-0.46,-0.5), P(0.5,-0.46,0.5), P(-0.5,-0.46,0.5)]
|
|
38
|
+
const vt = niceTicks(0, hi, 3)
|
|
39
|
+
const vTicks = vt.ticks.filter(t=>t<=hi*1.001).map(t=>({ p:P(-0.5,(t/hi-0.5)*0.9,0.5), v:props.valueFormat(t) }))
|
|
40
|
+
// Labels follow whichever base edge faces the viewer, outside it.
|
|
41
|
+
const zE = P(0,-0.46,0.5).y >= P(0,-0.46,-0.5).y ? 0.5 : -0.5
|
|
42
|
+
const ctr = P(0,-0.46,0)
|
|
43
|
+
const labels = props.categories.map((c,i)=>{
|
|
44
|
+
const p0 = P(i/n-0.5+0.5/n, -0.46, zE)
|
|
45
|
+
const dx = p0.x-ctr.x, dy = p0.y-ctr.y, len = Math.hypot(dx,dy)||1
|
|
46
|
+
return { c, x: p0.x+(dx/len)*18, y: p0.y+(dy/len)*18+3 }
|
|
47
|
+
})
|
|
48
|
+
return { drawn, basePath: facePath(base), vTicks, vAxis:[P(-0.5,-0.45,0.5), P(-0.5,0.45,0.5)], labels, FACE_SHADE }
|
|
49
|
+
})
|
|
50
|
+
</script>
|
|
51
|
+
<template>
|
|
52
|
+
<div :style="{position:'relative',display:'inline-block',lineHeight:0,...style}">
|
|
53
|
+
<svg :width="width" :height="height" @pointerdown="down" @pointermove="move" @pointerup="up" @pointerleave="up"
|
|
54
|
+
role="img" :style="{cursor:interactive?'grab':'default',touchAction:'none'}">
|
|
55
|
+
<path :d="g.basePath" fill="var(--bg-inset)" stroke="var(--chart-axis)" stroke-width="1"/>
|
|
56
|
+
<line :x1="g.vAxis[0].x" :y1="g.vAxis[0].y" :x2="g.vAxis[1].x" :y2="g.vAxis[1].y" stroke="var(--chart-guide)" stroke-width="1"/>
|
|
57
|
+
<g v-for="(t,i) in g.vTicks" :key="'vt'+i">
|
|
58
|
+
<line :x1="t.p.x-4" :y1="t.p.y" :x2="t.p.x" :y2="t.p.y" stroke="var(--chart-guide)" stroke-width="1"/>
|
|
59
|
+
<text :x="t.p.x-7" :y="t.p.y+3" text-anchor="end" style="font-family:var(--font-mono);font-size:9px;fill:var(--chart-label)">{{ t.v }}</text>
|
|
60
|
+
</g>
|
|
61
|
+
<text v-for="(l,i) in g.labels" :key="'c'+i" :x="l.x" :y="l.y" text-anchor="middle" style="font-family:var(--font-sans);font-size:10px;fill:var(--chart-label)">{{ l.c }}</text>
|
|
62
|
+
<g v-for="b in g.drawn" :key="b.key" :opacity="hover==null||hover===b.key?1:0.45"
|
|
63
|
+
@mouseenter="interactive && (hover=b.key)" @mouseleave="interactive && (hover=null)"
|
|
64
|
+
style="transition:opacity var(--dur-fast) var(--ease-out)">
|
|
65
|
+
<path v-for="(f,fi) in b.faces" :key="fi" :d="f.d" :fill="shade(b.color, g.FACE_SHADE[f.kind])" stroke="var(--bg-surface)" stroke-width="0.4"/>
|
|
66
|
+
</g>
|
|
67
|
+
</svg>
|
|
68
|
+
<div v-if="hover!=null && interactive" :style="{position:'absolute',left:g.drawn.find(b=>b.key===hover).top.x+'px',top:g.drawn.find(b=>b.key===hover).top.y+'px',transform:g.drawn.find(b=>b.key===hover).top.x>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'}">
|
|
69
|
+
<div style="font-family:var(--font-sans);font-size:var(--text-2xs);color:var(--text-muted);padding-bottom:3px">{{ categories[g.drawn.find(b=>b.key===hover).i] }}</div>
|
|
70
|
+
<div style="font-family:var(--font-mono);font-size:var(--text-xs);color:var(--text-heading)">{{ g.drawn.find(b=>b.key===hover).s.name }} · {{ valueFormat(g.drawn.find(b=>b.key===hover).v) }}</div>
|
|
71
|
+
</div>
|
|
72
|
+
<div style="display:flex;gap:14px;padding-top:6px;line-height:normal">
|
|
73
|
+
<span v-for="(s,i) in series" :key="s.name" style="display:inline-flex;align-items:center;gap:6px">
|
|
74
|
+
<span :style="{width:'8px',height:'8px',borderRadius:'2px',background:s.color||SERIES[i%SERIES.length]}"/>
|
|
75
|
+
<span style="font-family:var(--font-sans);font-size:var(--text-xs);color:var(--text-secondary)">{{ s.name }}</span>
|
|
76
|
+
</span>
|
|
77
|
+
</div>
|
|
78
|
+
</div>
|
|
79
|
+
</template>
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
export interface BarSeries { name: string; values: number[]; color?: string; }
|
|
3
|
+
export interface BarOverlay { name: string; values: number[]; color?: string; }
|
|
4
|
+
/** Vertical bars: grouped, stacked, or diverging (variance ±). overlay adds a line on a right-hand axis (combo). */
|
|
5
|
+
export interface BarChartProps {
|
|
6
|
+
series?: BarSeries[];
|
|
7
|
+
categories?: string[];
|
|
8
|
+
mode?: "grouped" | "stacked" | "stacked100" | "diverging";
|
|
9
|
+
/** Swap the axes — long category names, ranked comparisons */
|
|
10
|
+
orientation?: "vertical" | "horizontal";
|
|
11
|
+
/** Print the value inside each bar (skipped where it wouldn't fit) */
|
|
12
|
+
showValues?: boolean;
|
|
13
|
+
/** Corner radius on the outer end of a bar. Default 2 */
|
|
14
|
+
radius?: number;
|
|
15
|
+
/** Line on its own right axis, e.g. margin % over revenue bars */
|
|
16
|
+
overlay?: BarOverlay;
|
|
17
|
+
width?: number; height?: number;
|
|
18
|
+
axes?: boolean; legend?: boolean; interactive?: boolean;
|
|
19
|
+
yFormat?: (v: number) => string;
|
|
20
|
+
overlayFormat?: (v: number) => string;
|
|
21
|
+
style?: React.CSSProperties;
|
|
22
|
+
}
|
|
23
|
+
export declare function BarChart(props: BarChartProps): JSX.Element;
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { fmtNum, extent, scaleLin, stack, niceTicks, seriesColor, linePath } from "./chart-geometry.js";
|
|
3
|
+
import { ChartLegend, ChartTooltip, PlotGrid, thinLabels, wrapStyle } from "./ChartKit.jsx";
|
|
4
|
+
|
|
5
|
+
/** Bar with only its outer end rounded — a stack must read as one bar, not stacked pills. */
|
|
6
|
+
function barPath(x, y, w, h, r, side) {
|
|
7
|
+
const rr = Math.max(0, Math.min(r, w / 2, h / 2));
|
|
8
|
+
if (!rr) return `M${x} ${y} h${w} v${h} h${-w} Z`;
|
|
9
|
+
if (side === "top") return `M${x} ${y + h} V${y + rr} a${rr} ${rr} 0 0 1 ${rr} ${-rr} h${w - rr * 2} a${rr} ${rr} 0 0 1 ${rr} ${rr} V${y + h} Z`;
|
|
10
|
+
if (side === "bottom") return `M${x} ${y} V${y + h - rr} a${rr} ${rr} 0 0 0 ${rr} ${rr} h${w - rr * 2} a${rr} ${rr} 0 0 0 ${rr} ${-rr} V${y} Z`;
|
|
11
|
+
if (side === "right") return `M${x} ${y} h${w - rr} a${rr} ${rr} 0 0 1 ${rr} ${rr} v${h - rr * 2} a${rr} ${rr} 0 0 1 ${-rr} ${rr} h${-(w - rr)} Z`;
|
|
12
|
+
if (side === "left") return `M${x + w} ${y} h${-(w - rr)} a${rr} ${rr} 0 0 0 ${-rr} ${rr} v${h - rr * 2} a${rr} ${rr} 0 0 0 ${rr} ${rr} h${w - rr} Z`;
|
|
13
|
+
return `M${x} ${y} h${w} v${h} h${-w} Z`;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Bar chart. series over shared categories; mode "grouped" | "stacked" | "stacked100"
|
|
18
|
+
* (share of each category) | "diverging" (single series, positive --chart-pos / negative
|
|
19
|
+
* --chart-neg — variance vs. plan). orientation="horizontal" swaps the axes for long
|
|
20
|
+
* category names and ranked comparisons. overlay draws a line on its own secondary axis
|
|
21
|
+
* (vertical only). Inside a stack only the outermost segment is rounded, so joins are flush.
|
|
22
|
+
*/
|
|
23
|
+
export function BarChart({ series = [], categories = [], mode = "grouped", orientation = "vertical", overlay, width = 560, height = 240, axes = true, legend = true, interactive = true, showValues = false, radius = 2, yFormat = fmtNum, overlayFormat = (v) => fmtNum(v, { pct: true }), style }) {
|
|
24
|
+
const [hidden, setHidden] = React.useState([]);
|
|
25
|
+
const [hover, setHover] = React.useState(null);
|
|
26
|
+
const horiz = orientation === "horizontal";
|
|
27
|
+
const shown = series.filter((s) => !hidden.includes(s.name));
|
|
28
|
+
const n = categories.length || Math.max(...series.map((s) => s.values.length));
|
|
29
|
+
const pct = mode === "stacked100";
|
|
30
|
+
const stacked = mode === "stacked" || pct;
|
|
31
|
+
|
|
32
|
+
const norm = pct
|
|
33
|
+
? shown.map((s) => s.values.map((v, i) => {
|
|
34
|
+
const tot = shown.reduce((t, o) => t + (o.values[i] || 0), 0) || 1;
|
|
35
|
+
return ((v || 0) / tot) * 100;
|
|
36
|
+
}))
|
|
37
|
+
: shown.map((s) => s.values);
|
|
38
|
+
const bands = stacked && shown.length ? stack(norm) : null;
|
|
39
|
+
const [lo0, hi0] = pct ? [0, 100] : stacked ? extent([(bands[bands.length - 1] || []).map((b) => b[1])]) : extent(norm);
|
|
40
|
+
const { ticks, lo, hi } = niceTicks(lo0, hi0, 4);
|
|
41
|
+
const labelPad = horiz ? Math.min(140, Math.max(...categories.map((c) => String(c).length)) * 6.4 + 12) : 44;
|
|
42
|
+
const pad = horiz
|
|
43
|
+
? { l: axes ? labelPad : 4, r: 12, t: 6, b: axes ? 22 : 4 }
|
|
44
|
+
: { l: axes ? 44 : 4, r: overlay ? 40 : 8, t: 10, b: axes ? 24 : 4 };
|
|
45
|
+
|
|
46
|
+
const sv = horiz ? scaleLin(lo, hi, pad.l, width - pad.r) : scaleLin(lo, hi, height - pad.b, pad.t);
|
|
47
|
+
const band = ((horiz ? height - pad.t - pad.b : width - pad.l - pad.r) / n);
|
|
48
|
+
const mid = (i) => (horiz ? pad.t : pad.l) + band * i + band / 2;
|
|
49
|
+
const groupW = Math.min(band * 0.66, mode === "grouped" ? 20 * Math.max(1, shown.length) : 30);
|
|
50
|
+
const bw = mode === "grouped" ? groupW / Math.max(1, shown.length) : groupW;
|
|
51
|
+
const colorOf = (s) => s.color || seriesColor(series.indexOf(s));
|
|
52
|
+
const zero = sv(Math.max(lo, 0));
|
|
53
|
+
|
|
54
|
+
const oScale = overlay && !horiz ? (() => { const [ol, oh] = extent([overlay.values]); const t = niceTicks(ol, oh, 4); return { sy: scaleLin(t.lo, t.hi, height - pad.b, pad.t), ticks: t.ticks }; })() : null;
|
|
55
|
+
const move = (e) => {
|
|
56
|
+
if (!interactive) return;
|
|
57
|
+
const r = e.currentTarget.getBoundingClientRect();
|
|
58
|
+
const off = horiz ? e.clientY - r.top - pad.t : e.clientX - r.left - pad.l;
|
|
59
|
+
const i = Math.floor(off / band);
|
|
60
|
+
setHover(i >= 0 && i < n ? i : null);
|
|
61
|
+
};
|
|
62
|
+
const fmtVal = (s, i) => (pct ? norm[shown.indexOf(s)][i].toFixed(1) + "%" : yFormat(s.values[i]));
|
|
63
|
+
|
|
64
|
+
const rects = [];
|
|
65
|
+
shown.forEach((s, si) => (pct ? norm[si] : s.values).forEach((v, i) => {
|
|
66
|
+
if (v == null) return;
|
|
67
|
+
let fill = mode === "diverging" ? (v >= 0 ? "var(--chart-pos)" : "var(--chart-neg)") : colorOf(s);
|
|
68
|
+
const outer = !shown.slice(si + 1).some((o) => ((pct ? norm[shown.indexOf(o)][i] : o.values[i]) || 0) > 0);
|
|
69
|
+
let x, y, w, h, side;
|
|
70
|
+
if (stacked) {
|
|
71
|
+
const a = sv(bands[si][i][0]), b = sv(bands[si][i][1]);
|
|
72
|
+
if (horiz) { x = Math.min(a, b); w = Math.max(1, Math.abs(b - a)); y = mid(i) - bw / 2; h = bw; side = outer ? "right" : null; }
|
|
73
|
+
else { y = Math.min(a, b); h = Math.max(1, Math.abs(a - b)); x = mid(i) - bw / 2; w = bw; side = outer ? "top" : null; }
|
|
74
|
+
} else if (horiz) {
|
|
75
|
+
const e = sv(v);
|
|
76
|
+
x = Math.min(zero, e); w = Math.max(1, Math.abs(e - zero)); y = mid(i) - groupW / 2 + si * bw + 0.5; h = Math.max(1, bw - 1);
|
|
77
|
+
side = v >= 0 ? "right" : "left";
|
|
78
|
+
} else {
|
|
79
|
+
const e = sv(v);
|
|
80
|
+
y = Math.min(zero, e); h = Math.max(1, Math.abs(e - zero)); x = mid(i) - groupW / 2 + si * bw + 0.5; w = Math.max(1, bw - 1);
|
|
81
|
+
side = v >= 0 ? "top" : "bottom";
|
|
82
|
+
}
|
|
83
|
+
rects.push({ key: s.name + i, d: barPath(x, y, w, h, radius, side), fill, x, y, w, h, v, s, i });
|
|
84
|
+
}));
|
|
85
|
+
|
|
86
|
+
return (
|
|
87
|
+
<div style={{ ...wrapStyle, ...style }}>
|
|
88
|
+
<svg width={width} height={height} onMouseMove={move} onMouseLeave={() => setHover(null)} role="img">
|
|
89
|
+
{axes && !horiz ? <PlotGrid w={width} h={height} pad={pad} ticks={ticks} sy={sv} xLabels={thinLabels(categories)} sxLabel={mid} yFormat={pct ? (v) => v + "%" : yFormat} /> : null}
|
|
90
|
+
{axes && horiz ? (
|
|
91
|
+
<g>
|
|
92
|
+
{ticks.map((t) => (
|
|
93
|
+
<g key={t}>
|
|
94
|
+
<line x1={sv(t)} x2={sv(t)} y1={pad.t} y2={height - pad.b} stroke={t === 0 ? "var(--chart-axis)" : "var(--chart-grid)"} strokeWidth="1" />
|
|
95
|
+
<text x={sv(t)} y={height - pad.b + 15} textAnchor="middle" style={{ fontFamily: "var(--font-mono)", fontSize: 10, fill: "var(--chart-label)" }}>{pct ? t + "%" : yFormat(t)}</text>
|
|
96
|
+
</g>
|
|
97
|
+
))}
|
|
98
|
+
{categories.map((c, i) => (
|
|
99
|
+
<text key={i} x={pad.l - 8} y={mid(i) + 3.5} textAnchor="end" style={{ fontFamily: "var(--font-sans)", fontSize: 10.5, fill: hover === i ? "var(--text-heading)" : "var(--chart-label)" }}>{c}</text>
|
|
100
|
+
))}
|
|
101
|
+
</g>
|
|
102
|
+
) : null}
|
|
103
|
+
{hover != null ? (horiz
|
|
104
|
+
? <rect x={pad.l} y={pad.t + band * hover} width={width - pad.l - pad.r} height={band} fill="var(--bg-hover)" />
|
|
105
|
+
: <rect x={pad.l + band * hover} y={pad.t} width={band} height={height - pad.t - pad.b} fill="var(--bg-hover)" />) : null}
|
|
106
|
+
{rects.map((r) => <path key={r.key} d={r.d} fill={r.fill} opacity={hover == null || hover === r.i ? 1 : 0.55} style={{ transition: "opacity var(--dur-fast) var(--ease-out)" }} />)}
|
|
107
|
+
{showValues ? rects.map((r) => (
|
|
108
|
+
(horiz ? r.w : r.h) < 22 ? null : (
|
|
109
|
+
<text key={"v" + r.key} x={horiz ? r.x + r.w - 5 : r.x + r.w / 2} y={horiz ? r.y + r.h / 2 + 3.5 : r.y + 12}
|
|
110
|
+
textAnchor={horiz ? "end" : "middle"} style={{ fontFamily: "var(--font-mono)", fontSize: 10, fill: "var(--on-accent)" }}>{fmtVal(r.s, r.i)}</text>
|
|
111
|
+
)
|
|
112
|
+
)) : null}
|
|
113
|
+
{oScale ? <g>
|
|
114
|
+
<path d={linePath(overlay.values.map((v, i) => [mid(i), oScale.sy(v)]))} fill="none" stroke={overlay.color || "var(--chart-total)"} strokeWidth="1.5" />
|
|
115
|
+
{overlay.values.map((v, i) => <circle key={i} cx={mid(i)} cy={oScale.sy(v)} r="2.5" fill={overlay.color || "var(--chart-total)"} />)}
|
|
116
|
+
{axes ? oScale.ticks.map((t) => <text key={t} x={width - pad.r + 6} y={oScale.sy(t) + 3} style={{ fontFamily: "var(--font-mono)", fontSize: 10, fill: "var(--chart-label)" }}>{overlayFormat(t)}</text>) : null}
|
|
117
|
+
</g> : null}
|
|
118
|
+
</svg>
|
|
119
|
+
{hover != null && interactive ? (
|
|
120
|
+
<ChartTooltip
|
|
121
|
+
x={horiz ? Math.min(width - 20, pad.l + (width - pad.l - pad.r) * 0.5) : mid(hover)}
|
|
122
|
+
y={horiz ? pad.t + band * hover + band / 2 : height / 2 - 10}
|
|
123
|
+
flip={horiz ? true : mid(hover) > width * 0.62} title={categories[hover]}
|
|
124
|
+
rows={shown.map((s) => ({ label: s.name, value: fmtVal(s, hover), color: mode === "diverging" ? (s.values[hover] >= 0 ? "var(--chart-pos)" : "var(--chart-neg)") : colorOf(s) }))
|
|
125
|
+
.concat(overlay ? [{ label: overlay.name, value: overlayFormat(overlay.values[hover]), color: overlay.color || "var(--chart-total)" }] : [])} />
|
|
126
|
+
) : null}
|
|
127
|
+
{legend && (series.length > 1 || overlay) ? (
|
|
128
|
+
<ChartLegend style={{ paddingTop: 8 }}
|
|
129
|
+
items={series.map((s) => ({ label: s.name, color: colorOf(s) })).concat(overlay ? [{ label: overlay.name, color: overlay.color || "var(--chart-total)" }] : [])}
|
|
130
|
+
hidden={hidden}
|
|
131
|
+
onToggle={(l) => series.some((s) => s.name === l) && setHidden(hidden.includes(l) ? hidden.filter((x) => x !== l) : hidden.length + 1 < series.length ? [...hidden, l] : hidden)} />
|
|
132
|
+
) : null}
|
|
133
|
+
</div>
|
|
134
|
+
);
|
|
135
|
+
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { ref, computed } from 'vue'
|
|
3
|
+
import { fmtNum, extent, scaleLin, stack, niceTicks, SERIES, linePath } from './chart-geometry.js'
|
|
4
|
+
/**
|
|
5
|
+
* Bar chart. mode "grouped" | "stacked" | "stacked100" | "diverging";
|
|
6
|
+
* orientation="horizontal" swaps the axes. Only the outermost stack segment is rounded,
|
|
7
|
+
* so colour joins inside a stack stay flush. overlay = line on a secondary axis (vertical).
|
|
8
|
+
*/
|
|
9
|
+
const props = defineProps({ series:{type:Array,default:()=>[]}, categories:{type:Array,default:()=>[]}, mode:{type:String,default:'grouped'}, orientation:{type:String,default:'vertical'}, overlay:Object, width:{type:Number,default:560}, height:{type:Number,default:240}, axes:{type:Boolean,default:true}, legend:{type:Boolean,default:true}, interactive:{type:Boolean,default:true}, showValues:Boolean, radius:{type:Number,default:2}, yFormat:{type:Function,default:fmtNum}, overlayFormat:{type:Function,default:(v)=>fmtNum(v,{pct:true})}, style:Object })
|
|
10
|
+
const hidden = ref([]); const hover = ref(null)
|
|
11
|
+
function barPath(x,y,w,h,r,side){
|
|
12
|
+
const rr = Math.max(0, Math.min(r, w/2, h/2))
|
|
13
|
+
if (!rr) return `M${x} ${y} h${w} v${h} h${-w} Z`
|
|
14
|
+
if (side==='top') return `M${x} ${y+h} V${y+rr} a${rr} ${rr} 0 0 1 ${rr} ${-rr} h${w-rr*2} a${rr} ${rr} 0 0 1 ${rr} ${rr} V${y+h} Z`
|
|
15
|
+
if (side==='bottom') return `M${x} ${y} V${y+h-rr} a${rr} ${rr} 0 0 0 ${rr} ${rr} h${w-rr*2} a${rr} ${rr} 0 0 0 ${rr} ${-rr} V${y} Z`
|
|
16
|
+
if (side==='right') return `M${x} ${y} h${w-rr} a${rr} ${rr} 0 0 1 ${rr} ${rr} v${h-rr*2} a${rr} ${rr} 0 0 1 ${-rr} ${rr} h${-(w-rr)} Z`
|
|
17
|
+
if (side==='left') return `M${x+w} ${y} h${-(w-rr)} a${rr} ${rr} 0 0 0 ${-rr} ${rr} v${h-rr*2} a${rr} ${rr} 0 0 0 ${rr} ${rr} h${w-rr} Z`
|
|
18
|
+
return `M${x} ${y} h${w} v${h} h${-w} Z`
|
|
19
|
+
}
|
|
20
|
+
const g = computed(() => {
|
|
21
|
+
const horiz = props.orientation === 'horizontal'
|
|
22
|
+
const shown = props.series.filter(s=>!hidden.value.includes(s.name))
|
|
23
|
+
const n = props.categories.length || Math.max(...props.series.map(s=>s.values.length))
|
|
24
|
+
const pct = props.mode === 'stacked100'
|
|
25
|
+
const stacked = props.mode === 'stacked' || pct
|
|
26
|
+
const norm = pct
|
|
27
|
+
? shown.map(s=>s.values.map((v,i)=>{ const tot = shown.reduce((t,o)=>t+(o.values[i]||0),0)||1; return ((v||0)/tot)*100 }))
|
|
28
|
+
: shown.map(s=>s.values)
|
|
29
|
+
const bands = stacked && shown.length ? stack(norm) : null
|
|
30
|
+
const ext = pct ? [0,100] : (stacked ? extent([(bands[bands.length-1]||[]).map(b=>b[1])]) : extent(norm))
|
|
31
|
+
const { ticks, lo, hi } = niceTicks(ext[0], ext[1], 4)
|
|
32
|
+
const labelPad = horiz ? Math.min(140, Math.max(...props.categories.map(c=>String(c).length))*6.4+12) : 44
|
|
33
|
+
const pad = horiz ? { l: props.axes?labelPad:4, r:12, t:6, b: props.axes?22:4 }
|
|
34
|
+
: { l: props.axes?44:4, r: props.overlay?40:8, t:10, b: props.axes?24:4 }
|
|
35
|
+
const sv = horiz ? scaleLin(lo,hi,pad.l,props.width-pad.r) : scaleLin(lo,hi,props.height-pad.b,pad.t)
|
|
36
|
+
const band = (horiz ? props.height-pad.t-pad.b : props.width-pad.l-pad.r)/n
|
|
37
|
+
const mid = i => (horiz?pad.t:pad.l) + band*i + band/2
|
|
38
|
+
const groupW = Math.min(band*0.66, props.mode==='grouped' ? 20*Math.max(1,shown.length) : 30)
|
|
39
|
+
const bw = props.mode==='grouped' ? groupW/Math.max(1,shown.length) : groupW
|
|
40
|
+
const colorOf = s => s.color || SERIES[props.series.indexOf(s)%SERIES.length]
|
|
41
|
+
const zero = sv(Math.max(lo,0))
|
|
42
|
+
const rects = []
|
|
43
|
+
shown.forEach((s,si)=>(pct?norm[si]:s.values).forEach((v,i)=>{
|
|
44
|
+
if (v==null) return
|
|
45
|
+
const fill = props.mode==='diverging' ? (v>=0?'var(--chart-pos)':'var(--chart-neg)') : colorOf(s)
|
|
46
|
+
const outer = !shown.slice(si+1).some(o=>((pct?norm[shown.indexOf(o)][i]:o.values[i])||0)>0)
|
|
47
|
+
let x,y,w,h,side
|
|
48
|
+
if (stacked) {
|
|
49
|
+
const a = sv(bands[si][i][0]), b = sv(bands[si][i][1])
|
|
50
|
+
if (horiz) { x=Math.min(a,b); w=Math.max(1,Math.abs(b-a)); y=mid(i)-bw/2; h=bw; side=outer?'right':null }
|
|
51
|
+
else { y=Math.min(a,b); h=Math.max(1,Math.abs(a-b)); x=mid(i)-bw/2; w=bw; side=outer?'top':null }
|
|
52
|
+
} else if (horiz) {
|
|
53
|
+
const e = sv(v); x=Math.min(zero,e); w=Math.max(1,Math.abs(e-zero)); y=mid(i)-groupW/2+si*bw+0.5; h=Math.max(1,bw-1); side=v>=0?'right':'left'
|
|
54
|
+
} else {
|
|
55
|
+
const e = sv(v); y=Math.min(zero,e); h=Math.max(1,Math.abs(e-zero)); x=mid(i)-groupW/2+si*bw+0.5; w=Math.max(1,bw-1); side=v>=0?'top':'bottom'
|
|
56
|
+
}
|
|
57
|
+
rects.push({ key:s.name+i, d:barPath(x,y,w,h,props.radius,side), fill, x, y, w, h, s, i,
|
|
58
|
+
text: pct ? norm[si][i].toFixed(1)+'%' : props.yFormat(s.values[i]) })
|
|
59
|
+
}))
|
|
60
|
+
let ov = null
|
|
61
|
+
if (props.overlay && !horiz) {
|
|
62
|
+
const e2 = extent([props.overlay.values]); const t = niceTicks(e2[0],e2[1],4)
|
|
63
|
+
const osy = scaleLin(t.lo,t.hi,props.height-pad.b,pad.t)
|
|
64
|
+
ov = { path: linePath(props.overlay.values.map((v,i)=>[mid(i),osy(v)])), dots: props.overlay.values.map((v,i)=>[mid(i),osy(v)]), ticks:t.ticks, osy, color: props.overlay.color||'var(--chart-total)' }
|
|
65
|
+
}
|
|
66
|
+
return { horiz, shown, pct, stacked, ticks, sv, band, mid, pad, rects, ov, n, colorOf,
|
|
67
|
+
fmtAxis: pct ? (v=>v+'%') : props.yFormat }
|
|
68
|
+
})
|
|
69
|
+
function move(e){
|
|
70
|
+
if(!props.interactive) return
|
|
71
|
+
const r = e.currentTarget.getBoundingClientRect()
|
|
72
|
+
const off = g.value.horiz ? e.clientY-r.top-g.value.pad.t : e.clientX-r.left-g.value.pad.l
|
|
73
|
+
const i = Math.floor(off/g.value.band)
|
|
74
|
+
hover.value = i>=0 && i<g.value.n ? i : null
|
|
75
|
+
}
|
|
76
|
+
function toggle(name){
|
|
77
|
+
if (hidden.value.includes(name)) hidden.value = hidden.value.filter(x=>x!==name)
|
|
78
|
+
else if (hidden.value.length+1 < props.series.length) hidden.value = [...hidden.value, name]
|
|
79
|
+
}
|
|
80
|
+
function rowVal(s){
|
|
81
|
+
const i = hover.value
|
|
82
|
+
return g.value.pct ? (g.value.rects.find(r=>r.s===s && r.i===i)||{}).text : props.yFormat(s.values[i])
|
|
83
|
+
}
|
|
84
|
+
</script>
|
|
85
|
+
<template>
|
|
86
|
+
<div :style="{position:'relative',display:'inline-block',lineHeight:0,...style}">
|
|
87
|
+
<svg :width="width" :height="height" @mousemove="move" @mouseleave="hover=null" role="img">
|
|
88
|
+
<template v-if="axes">
|
|
89
|
+
<g v-for="t in g.ticks" :key="t">
|
|
90
|
+
<line v-if="g.horiz" :x1="g.sv(t)" :x2="g.sv(t)" :y1="g.pad.t" :y2="height-g.pad.b" :stroke="t===0?'var(--chart-axis)':'var(--chart-grid)'" stroke-width="1"/>
|
|
91
|
+
<line v-else :x1="g.pad.l" :x2="width-g.pad.r" :y1="g.sv(t)" :y2="g.sv(t)" :stroke="t===0?'var(--chart-axis)':'var(--chart-grid)'" stroke-width="1"/>
|
|
92
|
+
<text v-if="g.horiz" :x="g.sv(t)" :y="height-g.pad.b+15" text-anchor="middle" style="font-family:var(--font-mono);font-size:10px;fill:var(--chart-label)">{{ g.fmtAxis(t) }}</text>
|
|
93
|
+
<text v-else :x="g.pad.l-8" :y="g.sv(t)+3" text-anchor="end" style="font-family:var(--font-mono);font-size:10px;fill:var(--chart-label)">{{ g.fmtAxis(t) }}</text>
|
|
94
|
+
</g>
|
|
95
|
+
<text v-for="(c,i) in categories" :key="'c'+i"
|
|
96
|
+
:x="g.horiz ? g.pad.l-8 : g.mid(i)" :y="g.horiz ? g.mid(i)+3.5 : height-g.pad.b+16"
|
|
97
|
+
:text-anchor="g.horiz ? 'end' : 'middle'"
|
|
98
|
+
:style="{fontFamily:'var(--font-sans)',fontSize:g.horiz?'10.5px':'10px',fill:hover===i?'var(--text-heading)':'var(--chart-label)'}">{{ c }}</text>
|
|
99
|
+
</template>
|
|
100
|
+
<rect v-if="hover!=null && g.horiz" :x="g.pad.l" :y="g.pad.t+g.band*hover" :width="width-g.pad.l-g.pad.r" :height="g.band" fill="var(--bg-hover)"/>
|
|
101
|
+
<rect v-else-if="hover!=null" :x="g.pad.l+g.band*hover" :y="g.pad.t" :width="g.band" :height="height-g.pad.t-g.pad.b" fill="var(--bg-hover)"/>
|
|
102
|
+
<path v-for="r in g.rects" :key="r.key" :d="r.d" :fill="r.fill" :opacity="hover==null||hover===r.i?1:0.55" style="transition:opacity var(--dur-fast) var(--ease-out)"/>
|
|
103
|
+
<template v-if="showValues">
|
|
104
|
+
<text v-for="r in g.rects.filter(r=>(g.horiz?r.w:r.h)>=22)" :key="'v'+r.key"
|
|
105
|
+
:x="g.horiz ? r.x+r.w-5 : r.x+r.w/2" :y="g.horiz ? r.y+r.h/2+3.5 : r.y+12"
|
|
106
|
+
:text-anchor="g.horiz ? 'end' : 'middle'" style="font-family:var(--font-mono);font-size:10px;fill:var(--on-accent)">{{ r.text }}</text>
|
|
107
|
+
</template>
|
|
108
|
+
<template v-if="g.ov">
|
|
109
|
+
<path :d="g.ov.path" fill="none" :stroke="g.ov.color" stroke-width="1.5"/>
|
|
110
|
+
<circle v-for="(d,i) in g.ov.dots" :key="'o'+i" :cx="d[0]" :cy="d[1]" r="2.5" :fill="g.ov.color"/>
|
|
111
|
+
<text v-if="axes" v-for="t in g.ov.ticks" :key="'ot'+t" :x="width-g.pad.r+6" :y="g.ov.osy(t)+3" style="font-family:var(--font-mono);font-size:10px;fill:var(--chart-label)">{{ overlayFormat(t) }}</text>
|
|
112
|
+
</template>
|
|
113
|
+
</svg>
|
|
114
|
+
<div v-if="hover!=null && interactive" :style="{position:'absolute',left:(g.horiz ? Math.min(width-20, g.pad.l+(width-g.pad.l-g.pad.r)*0.5) : g.mid(hover))+'px',top:(g.horiz ? g.pad.t+g.band*hover+g.band/2 : height/2-10)+'px',transform:(g.horiz || g.mid(hover)>width*0.62)?'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'}">
|
|
115
|
+
<div style="font-family:var(--font-sans);font-size:var(--text-2xs);color:var(--text-muted);padding-bottom:4px">{{ categories[hover] }}</div>
|
|
116
|
+
<div v-for="s in g.shown" :key="s.name" style="display:flex;align-items:center;gap:6px;padding:1px 0">
|
|
117
|
+
<span :style="{width:'7px',height:'7px',borderRadius:'2px',background:mode==='diverging'?(s.values[hover]>=0?'var(--chart-pos)':'var(--chart-neg)'):g.colorOf(s),flexShrink:0}"/>
|
|
118
|
+
<span style="font-family:var(--font-sans);font-size:var(--text-xs);color:var(--text-secondary)">{{ s.name }}</span>
|
|
119
|
+
<span style="font-family:var(--font-mono);font-size:var(--text-xs);color:var(--text-heading);margin-left:auto;padding-left:10px">{{ rowVal(s) }}</span>
|
|
120
|
+
</div>
|
|
121
|
+
<div v-if="overlay" style="display:flex;align-items:center;gap:6px;padding:1px 0">
|
|
122
|
+
<span :style="{width:'7px',height:'7px',borderRadius:'2px',background:overlay.color||'var(--chart-total)'}"/>
|
|
123
|
+
<span style="font-family:var(--font-sans);font-size:var(--text-xs);color:var(--text-secondary)">{{ overlay.name }}</span>
|
|
124
|
+
<span style="font-family:var(--font-mono);font-size:var(--text-xs);color:var(--text-heading);margin-left:auto;padding-left:10px">{{ overlayFormat(overlay.values[hover]) }}</span>
|
|
125
|
+
</div>
|
|
126
|
+
</div>
|
|
127
|
+
<div v-if="legend && (series.length>1 || overlay)" style="display:flex;flex-wrap:wrap;gap:4px 14px;padding-top:8px;line-height:normal">
|
|
128
|
+
<span v-for="(s,i) in series" :key="s.name" role="button" tabindex="0" @click="toggle(s.name)"
|
|
129
|
+
:style="{display:'inline-flex',alignItems:'center',gap:'6px',cursor:'pointer',opacity:hidden.includes(s.name)?0.4:1}">
|
|
130
|
+
<span :style="{width:'8px',height:'8px',borderRadius:'2px',background:s.color||SERIES[i%SERIES.length]}"/>
|
|
131
|
+
<span :style="{fontFamily:'var(--font-sans)',fontSize:'var(--text-xs)',color:'var(--text-secondary)',textDecoration:hidden.includes(s.name)?'line-through':'none'}">{{ s.name }}</span>
|
|
132
|
+
</span>
|
|
133
|
+
<span v-if="overlay" style="display:inline-flex;align-items:center;gap:6px">
|
|
134
|
+
<span :style="{width:'8px',height:'8px',borderRadius:'2px',background:overlay.color||'var(--chart-total)'}"/>
|
|
135
|
+
<span style="font-family:var(--font-sans);font-size:var(--text-xs);color:var(--text-secondary)">{{ overlay.name }}</span>
|
|
136
|
+
</span>
|
|
137
|
+
</div>
|
|
138
|
+
</div>
|
|
139
|
+
</template>
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
export interface BarListRow { label: string; value: number; color?: string; /** Signed % shown at the right */ delta?: number; }
|
|
3
|
+
/** Horizontal labeled bars — top-N rankings, budget consumption. */
|
|
4
|
+
export interface BarListProps {
|
|
5
|
+
rows?: BarListRow[];
|
|
6
|
+
/** Scale cap; defaults to the largest row */ max?: number;
|
|
7
|
+
width?: number; barHeight?: number; showTrack?: boolean;
|
|
8
|
+
valueFormat?: (v: number) => string;
|
|
9
|
+
interactive?: boolean;
|
|
10
|
+
onSelect?: (row: BarListRow) => void;
|
|
11
|
+
style?: React.CSSProperties;
|
|
12
|
+
}
|
|
13
|
+
export declare function BarList(props: BarListProps): JSX.Element;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { fmtNum } from "./chart-geometry.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Horizontal labeled bars — top-N rankings, budget consumption, category comparison.
|
|
6
|
+
* rows: [{label, value, color?, delta?}] — delta renders a signed % at the right.
|
|
7
|
+
*/
|
|
8
|
+
export function BarList({ rows = [], max, width = 360, valueFormat = fmtNum, showTrack = true, barHeight = 8, interactive = true, onSelect, style }) {
|
|
9
|
+
const [hover, setHover] = React.useState(null);
|
|
10
|
+
const top = max != null ? max : Math.max(1, ...rows.map((r) => r.value));
|
|
11
|
+
const total = rows.reduce((s, r) => s + r.value, 0) || 1;
|
|
12
|
+
return (
|
|
13
|
+
<div style={{ display: "flex", flexDirection: "column", gap: 10, width, ...style }}>
|
|
14
|
+
{rows.map((r) => (
|
|
15
|
+
<div key={r.label} onMouseEnter={interactive ? () => setHover(r.label) : undefined} onMouseLeave={interactive ? () => setHover(null) : undefined}
|
|
16
|
+
onClick={onSelect ? () => onSelect(r) : undefined}
|
|
17
|
+
style={{ opacity: hover == null || hover === r.label ? 1 : 0.5, cursor: onSelect ? "pointer" : "default", transition: "var(--t-hover)" }}>
|
|
18
|
+
<div style={{ display: "flex", alignItems: "baseline", gap: 8, paddingBottom: 4 }}>
|
|
19
|
+
<span style={{ fontFamily: "var(--font-sans)", fontSize: "var(--text-sm)", color: "var(--text-body)", whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }}>{r.label}</span>
|
|
20
|
+
<span style={{ marginLeft: "auto", fontFamily: "var(--font-mono)", fontSize: "var(--text-xs)", color: "var(--text-heading)" }}>{valueFormat(r.value)}</span>
|
|
21
|
+
{hover === r.label && r.delta == null ? <span style={{ fontFamily: "var(--font-mono)", fontSize: "var(--text-2xs)", color: "var(--text-muted)", paddingLeft: 6 }}>{((r.value / total) * 100).toFixed(1)}%</span> : null}
|
|
22
|
+
{r.delta != null ? <span style={{ fontFamily: "var(--font-mono)", fontSize: "var(--text-2xs)", color: r.delta >= 0 ? "var(--chart-pos)" : "var(--chart-neg)", minWidth: 38, textAlign: "right" }}>{(r.delta >= 0 ? "+" : "") + r.delta.toFixed(1)}%</span> : null}
|
|
23
|
+
</div>
|
|
24
|
+
<div style={{ height: barHeight, borderRadius: "var(--radius-full)", background: showTrack ? "var(--chart-track)" : "transparent", overflow: "hidden" }}>
|
|
25
|
+
<div style={{ width: Math.max(1, (r.value / top) * 100) + "%", height: "100%", borderRadius: "var(--radius-full)", background: r.color || "var(--series-1)", filter: hover === r.label ? "brightness(1.08)" : "none", transition: "width var(--dur) var(--ease-out), filter var(--dur-fast) var(--ease-out)" }} />
|
|
26
|
+
</div>
|
|
27
|
+
</div>
|
|
28
|
+
))}
|
|
29
|
+
</div>
|
|
30
|
+
);
|
|
31
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { computed, ref } from 'vue'
|
|
3
|
+
import { fmtNum } from './chart-geometry.js'
|
|
4
|
+
// Horizontal labeled bars — top-N rankings, budget consumption.
|
|
5
|
+
const props = defineProps({ rows:{type:Array,default:()=>[]}, max:Number, width:{type:Number,default:360}, barHeight:{type:Number,default:8}, showTrack:{type:Boolean,default:true}, valueFormat:{type:Function,default:fmtNum}, interactive:{type:Boolean,default:true}, style:Object })
|
|
6
|
+
const top = computed(() => props.max!=null ? props.max : Math.max(1,...props.rows.map(r=>r.value)))
|
|
7
|
+
const total = computed(() => props.rows.reduce((s,r)=>s+r.value,0)||1)
|
|
8
|
+
const hover = ref(null)
|
|
9
|
+
</script>
|
|
10
|
+
<template>
|
|
11
|
+
<div :style="{display:'flex',flexDirection:'column',gap:'10px',width:width+'px',...style}">
|
|
12
|
+
<div v-for="r in rows" :key="r.label" @mouseenter="interactive && (hover=r.label)" @mouseleave="interactive && (hover=null)"
|
|
13
|
+
:style="{opacity:hover==null||hover===r.label?1:0.5,transition:'var(--t-hover)'}">
|
|
14
|
+
<div style="display:flex;align-items:baseline;gap:8px;padding-bottom:4px">
|
|
15
|
+
<span style="font-family:var(--font-sans);font-size:var(--text-sm);color:var(--text-body);white-space:nowrap;overflow:hidden;text-overflow:ellipsis">{{ r.label }}</span>
|
|
16
|
+
<span style="margin-left:auto;font-family:var(--font-mono);font-size:var(--text-xs);color:var(--text-heading)">{{ valueFormat(r.value) }}</span>
|
|
17
|
+
<span v-if="hover===r.label && r.delta==null" style="font-family:var(--font-mono);font-size:var(--text-2xs);color:var(--text-muted);padding-left:6px">{{ ((r.value/total)*100).toFixed(1) }}%</span>
|
|
18
|
+
<span v-if="r.delta!=null" :style="{fontFamily:'var(--font-mono)',fontSize:'var(--text-2xs)',color:r.delta>=0?'var(--chart-pos)':'var(--chart-neg)',minWidth:'38px',textAlign:'right'}">{{ (r.delta>=0?'+':'')+r.delta.toFixed(1) }}%</span>
|
|
19
|
+
</div>
|
|
20
|
+
<div :style="{height:barHeight+'px',borderRadius:'var(--radius-full)',background:showTrack?'var(--chart-track)':'transparent',overflow:'hidden'}">
|
|
21
|
+
<div :style="{width:Math.max(1,(r.value/top)*100)+'%',height:'100%',borderRadius:'var(--radius-full)',background:r.color||'var(--series-1)',filter:hover===r.label?'brightness(1.08)':'none',transition:'width var(--dur) var(--ease-out), filter var(--dur-fast) var(--ease-out)'}"/>
|
|
22
|
+
</div>
|
|
23
|
+
</div>
|
|
24
|
+
</div>
|
|
25
|
+
</template>
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
export interface BoxGroup {
|
|
3
|
+
label: string;
|
|
4
|
+
/** Raw values — summarized for you */ values?: number[];
|
|
5
|
+
q1?: number; med?: number; q3?: number; lo?: number; hi?: number; outliers?: number[];
|
|
6
|
+
color?: string;
|
|
7
|
+
}
|
|
8
|
+
/** Box & whisker with Tukey-fence outliers. */
|
|
9
|
+
export interface BoxPlotProps {
|
|
10
|
+
groups?: BoxGroup[];
|
|
11
|
+
width?: number; height?: number;
|
|
12
|
+
axes?: boolean; interactive?: boolean;
|
|
13
|
+
valueFormat?: (v: number) => string;
|
|
14
|
+
style?: React.CSSProperties;
|
|
15
|
+
}
|
|
16
|
+
export declare function BoxPlot(props: BoxPlotProps): JSX.Element;
|
|
17
|
+
/** Five-number summary helper (q1, med, q3, fences, outliers). */
|
|
18
|
+
export declare function summarize(values: number[]): { q1: number; med: number; q3: number; lo: number; hi: number; outliers: number[] };
|