@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,69 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { fmtNum } from "./chart-geometry.js";
|
|
3
|
+
import { makeProj, dragRotate } from "./chart-3d.js";
|
|
4
|
+
import { ChartTooltip, wrapStyle } from "./ChartKit.jsx";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* 3D scatter — points in x/y/z with group coloring; drag to orbit. Depth-sorted circles
|
|
8
|
+
* with radius attenuated by distance so nearer points read closer.
|
|
9
|
+
*/
|
|
10
|
+
export function Scatter3D({ points = [], width = 420, height = 300, yaw: yaw0 = 35, pitch: pitch0 = 28, r = 4, xLabel = "x", yLabel = "y", zLabel = "z", legend = true, interactive = true, valueFormat = fmtNum, style }) {
|
|
11
|
+
const [rot, setRot] = React.useState({ yaw: yaw0, pitch: pitch0 });
|
|
12
|
+
const [hover, setHover] = React.useState(null);
|
|
13
|
+
const names = [...new Set(points.map((p) => p.series).filter(Boolean))];
|
|
14
|
+
const norm = (arr) => { const l = Math.min(...arr), h = Math.max(...arr), s = h - l || 1; return (v) => (v - l) / s - 0.5; };
|
|
15
|
+
const nx = norm(points.map((p) => p.x)), ny = norm(points.map((p) => p.y)), nz = norm(points.map((p) => p.z));
|
|
16
|
+
const scale = Math.min(width, height) * 0.44;
|
|
17
|
+
const P = makeProj(width / 2, height / 2, scale, rot.yaw, rot.pitch);
|
|
18
|
+
const dots = points.map((p, i) => ({ p, i, ...P(nx(p.x), ny(p.y), nz(p.z)) })).sort((a, b) => b.depth - a.depth);
|
|
19
|
+
// Cube edges for orientation
|
|
20
|
+
const C = [-0.5, 0.5];
|
|
21
|
+
const edges = [];
|
|
22
|
+
C.forEach((a) => C.forEach((b) => { edges.push([P(a, b, -0.5), P(a, b, 0.5)], [P(a, -0.5, b), P(a, 0.5, b)], [P(-0.5, a, b), P(0.5, a, b)]); }));
|
|
23
|
+
const colorOf = (p) => p.color || `var(--series-${(names.indexOf(p.series) % 10) + 1})`;
|
|
24
|
+
/* Three axis edges meet at visible cube corners; names sit outside the cube along the
|
|
25
|
+
screen-space outward direction, so they never land inside the plot at any rotation. */
|
|
26
|
+
const axes3 = [
|
|
27
|
+
{ a: P(-0.5, -0.5, 0.5), b: P(0.5, -0.5, 0.5), name: xLabel },
|
|
28
|
+
{ a: P(-0.5, -0.5, 0.5), b: P(-0.5, 0.5, 0.5), name: yLabel },
|
|
29
|
+
{ a: P(0.5, -0.5, 0.5), b: P(0.5, -0.5, -0.5), name: zLabel },
|
|
30
|
+
].map((ax) => {
|
|
31
|
+
const mx = (ax.a.x + ax.b.x) / 2, my = (ax.a.y + ax.b.y) / 2;
|
|
32
|
+
const dx = mx - width / 2, dy = my - height / 2;
|
|
33
|
+
const len = Math.hypot(dx, dy) || 1;
|
|
34
|
+
return { ...ax, nx: mx + (dx / len) * 18, ny: my + (dy / len) * 18 };
|
|
35
|
+
});
|
|
36
|
+
const handlers = interactive ? dragRotate(() => rot, setRot) : {};
|
|
37
|
+
return (
|
|
38
|
+
<div style={{ ...wrapStyle, ...style }}>
|
|
39
|
+
<svg width={width} height={height} {...handlers} role="img" style={{ cursor: interactive ? "grab" : "default", touchAction: "none" }}>
|
|
40
|
+
{edges.map(([a, b], i) => <line key={i} x1={a.x} y1={a.y} x2={b.x} y2={b.y} stroke="var(--chart-axis)" strokeWidth="1" />)}
|
|
41
|
+
{axes3.map((ax, i) => <line key={"ae" + i} x1={ax.a.x} y1={ax.a.y} x2={ax.b.x} y2={ax.b.y} stroke="var(--chart-guide)" strokeWidth="1" />)}
|
|
42
|
+
{axes3.map((ax, i) => (
|
|
43
|
+
<text key={"at" + i} x={ax.nx} y={ax.ny + 3} textAnchor="middle" style={{ fontFamily: "var(--font-sans)", fontSize: 10, fill: "var(--text-secondary)" }}>{ax.name}</text>
|
|
44
|
+
))}
|
|
45
|
+
{dots.map((d) => (
|
|
46
|
+
<circle key={d.i} cx={d.x} cy={d.y} r={r * (1.18 - (d.depth + 0.75) * 0.35) * (hover === d.i ? 1.5 : 1)}
|
|
47
|
+
fill={colorOf(d.p)} opacity={hover === d.i ? 1 : (0.85 - (d.depth + 0.75) * 0.28) * (hover == null ? 1 : 0.45)}
|
|
48
|
+
stroke={hover === d.i ? "var(--bg-surface)" : "none"} strokeWidth="1.5"
|
|
49
|
+
onMouseEnter={interactive ? () => setHover(d.i) : undefined} onMouseLeave={interactive ? () => setHover(null) : undefined} />
|
|
50
|
+
))}
|
|
51
|
+
</svg>
|
|
52
|
+
{hover != null && interactive ? (() => {
|
|
53
|
+
const d = dots.find((x) => x.i === hover), p = d.p;
|
|
54
|
+
return <ChartTooltip x={d.x} y={d.y} flip={d.x > width * 0.6} title={p.label || p.series}
|
|
55
|
+
rows={[{ label: "x", value: valueFormat(p.x) }, { label: "y", value: valueFormat(p.y) }, { label: "z", value: valueFormat(p.z) }]} />;
|
|
56
|
+
})() : null}
|
|
57
|
+
{legend && names.length > 1 ? (
|
|
58
|
+
<div style={{ display: "flex", gap: 14, paddingTop: 6, lineHeight: "normal" }}>
|
|
59
|
+
{names.map((nm, i) => (
|
|
60
|
+
<span key={nm} style={{ display: "inline-flex", alignItems: "center", gap: 6 }}>
|
|
61
|
+
<span style={{ width: 8, height: 8, borderRadius: 4, background: `var(--series-${(i % 10) + 1})` }} />
|
|
62
|
+
<span style={{ fontFamily: "var(--font-sans)", fontSize: "var(--text-xs)", color: "var(--text-secondary)" }}>{nm}</span>
|
|
63
|
+
</span>
|
|
64
|
+
))}
|
|
65
|
+
</div>
|
|
66
|
+
) : null}
|
|
67
|
+
</div>
|
|
68
|
+
);
|
|
69
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { ref, computed } from 'vue'
|
|
3
|
+
import { fmtNum, SERIES } from './chart-geometry.js'
|
|
4
|
+
import { makeProj } from './chart-3d.js'
|
|
5
|
+
// 3D scatter in an orientation cube; drag to orbit. Depth attenuates radius and opacity.
|
|
6
|
+
const props = defineProps({ points:{type:Array,default:()=>[]}, width:{type:Number,default:420}, height:{type:Number,default:300}, yaw:{type:Number,default:35}, pitch:{type:Number,default:28}, r:{type:Number,default:4}, xLabel:{type:String,default:'x'}, yLabel:{type:String,default:'y'}, zLabel:{type:String,default:'z'}, legend:{type:Boolean,default:true}, interactive:{type:Boolean,default:true}, valueFormat:{type:Function,default:fmtNum}, style:Object })
|
|
7
|
+
const rot = ref({ yaw: props.yaw, pitch: props.pitch })
|
|
8
|
+
const hover = ref(null)
|
|
9
|
+
function down(e){ e.currentTarget.__kbDrag = { x:e.clientX, y:e.clientY, ...rot.value }; e.currentTarget.setPointerCapture && e.currentTarget.setPointerCapture(e.pointerId) }
|
|
10
|
+
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)) } }
|
|
11
|
+
function up(e){ e.currentTarget.__kbDrag = null }
|
|
12
|
+
const g = computed(() => {
|
|
13
|
+
const names = [...new Set(props.points.map(p=>p.series).filter(Boolean))]
|
|
14
|
+
const norm = arr => { const l=Math.min(...arr), h=Math.max(...arr), s=h-l||1; return v=>(v-l)/s-0.5 }
|
|
15
|
+
const nx = norm(props.points.map(p=>p.x)), ny = norm(props.points.map(p=>p.y)), nz = norm(props.points.map(p=>p.z))
|
|
16
|
+
const scale = Math.min(props.width, props.height)*0.44
|
|
17
|
+
const P = makeProj(props.width/2, props.height/2, scale, rot.value.yaw, rot.value.pitch)
|
|
18
|
+
const dots = props.points.map((p,i)=>({ p, i, ...P(nx(p.x), ny(p.y), nz(p.z)),
|
|
19
|
+
color: p.color || SERIES[(p.series?names.indexOf(p.series):0)%SERIES.length] })).sort((a,b)=>b.depth-a.depth)
|
|
20
|
+
const C = [-0.5,0.5]; const edges = []
|
|
21
|
+
C.forEach(a=>C.forEach(b=>{ edges.push([P(a,b,-0.5),P(a,b,0.5)],[P(a,-0.5,b),P(a,0.5,b)],[P(-0.5,a,b),P(0.5,a,b)]) }))
|
|
22
|
+
// Axis names sit outside the cube along the screen-space outward direction.
|
|
23
|
+
const axes3 = [
|
|
24
|
+
{ a:P(-0.5,-0.5,0.5), b:P(0.5,-0.5,0.5), name:props.xLabel },
|
|
25
|
+
{ a:P(-0.5,-0.5,0.5), b:P(-0.5,0.5,0.5), name:props.yLabel },
|
|
26
|
+
{ a:P(0.5,-0.5,0.5), b:P(0.5,-0.5,-0.5), name:props.zLabel },
|
|
27
|
+
].map(ax=>{
|
|
28
|
+
const mx=(ax.a.x+ax.b.x)/2, my=(ax.a.y+ax.b.y)/2
|
|
29
|
+
const dx=mx-props.width/2, dy=my-props.height/2, len=Math.hypot(dx,dy)||1
|
|
30
|
+
return { ...ax, nx: mx+(dx/len)*18, ny: my+(dy/len)*18 }
|
|
31
|
+
})
|
|
32
|
+
return { names, dots, edges, axes3 }
|
|
33
|
+
})
|
|
34
|
+
</script>
|
|
35
|
+
<template>
|
|
36
|
+
<div :style="{position:'relative',display:'inline-block',lineHeight:0,...style}">
|
|
37
|
+
<svg :width="width" :height="height" @pointerdown="down" @pointermove="move" @pointerup="up" @pointerleave="up"
|
|
38
|
+
role="img" :style="{cursor:interactive?'grab':'default',touchAction:'none'}">
|
|
39
|
+
<line v-for="(e,i) in g.edges" :key="'e'+i" :x1="e[0].x" :y1="e[0].y" :x2="e[1].x" :y2="e[1].y" stroke="var(--chart-axis)" stroke-width="1"/>
|
|
40
|
+
<line v-for="(ax,i) in g.axes3" :key="'ae'+i" :x1="ax.a.x" :y1="ax.a.y" :x2="ax.b.x" :y2="ax.b.y" stroke="var(--chart-guide)" stroke-width="1"/>
|
|
41
|
+
<text v-for="(ax,i) in g.axes3" :key="'at'+i" :x="ax.nx" :y="ax.ny+3" text-anchor="middle" style="font-family:var(--font-sans);font-size:10px;fill:var(--text-secondary)">{{ ax.name }}</text>
|
|
42
|
+
<circle v-for="d in g.dots" :key="d.i" :cx="d.x" :cy="d.y"
|
|
43
|
+
:r="r*(1.18-(d.depth+0.75)*0.35)*(hover===d.i?1.5:1)" :fill="d.color"
|
|
44
|
+
:opacity="hover===d.i ? 1 : (0.85-(d.depth+0.75)*0.28)*(hover==null?1:0.45)"
|
|
45
|
+
:stroke="hover===d.i?'var(--bg-surface)':'none'" stroke-width="1.5"
|
|
46
|
+
@mouseenter="interactive && (hover=d.i)" @mouseleave="interactive && (hover=null)"/>
|
|
47
|
+
</svg>
|
|
48
|
+
<div v-if="hover!=null && interactive" :style="{position:'absolute',left:g.dots.find(d=>d.i===hover).x+'px',top:g.dots.find(d=>d.i===hover).y+'px',transform:g.dots.find(d=>d.i===hover).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'}">
|
|
49
|
+
<div style="font-family:var(--font-sans);font-size:var(--text-2xs);color:var(--text-muted);padding-bottom:3px">{{ points[hover].label || points[hover].series }}</div>
|
|
50
|
+
<div style="font-family:var(--font-mono);font-size:var(--text-xs);color:var(--text-heading)">{{ xLabel }} {{ valueFormat(points[hover].x) }} · {{ yLabel }} {{ valueFormat(points[hover].y) }} · {{ zLabel }} {{ valueFormat(points[hover].z) }}</div>
|
|
51
|
+
</div>
|
|
52
|
+
<div v-if="legend && g.names.length>1" style="display:flex;gap:14px;padding-top:6px;line-height:normal">
|
|
53
|
+
<span v-for="(nm,i) in g.names" :key="nm" style="display:inline-flex;align-items:center;gap:6px">
|
|
54
|
+
<span :style="{width:'8px',height:'8px',borderRadius:'4px',background:SERIES[i%SERIES.length]}"/>
|
|
55
|
+
<span style="font-family:var(--font-sans);font-size:var(--text-xs);color:var(--text-secondary)">{{ nm }}</span>
|
|
56
|
+
</span>
|
|
57
|
+
</div>
|
|
58
|
+
</div>
|
|
59
|
+
</template>
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
export interface ScatterPoint { x: number; y: number; /** Bubble size in data units */ r?: number; label?: string; series?: string; color?: string; }
|
|
3
|
+
/** Scatter / bubble with nearest-point tooltip. */
|
|
4
|
+
export interface ScatterChartProps {
|
|
5
|
+
points?: ScatterPoint[];
|
|
6
|
+
width?: number; height?: number;
|
|
7
|
+
xFormat?: (v: number) => string; yFormat?: (v: number) => string;
|
|
8
|
+
xLabel?: string; yLabel?: string;
|
|
9
|
+
axes?: boolean; legend?: boolean; interactive?: boolean;
|
|
10
|
+
style?: React.CSSProperties;
|
|
11
|
+
}
|
|
12
|
+
export declare function ScatterChart(props: ScatterChartProps): JSX.Element;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { fmtNum, extent, scaleLin, niceTicks, seriesColor } from "./chart-geometry.js";
|
|
3
|
+
import { ChartLegend, ChartTooltip, PlotGrid, wrapStyle } from "./ChartKit.jsx";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Scatter / bubble. points: [{x, y, r?, label?, series?}] — r in data units scaled to
|
|
7
|
+
* 3–14px bubbles. Hover pins a tooltip to the nearest point.
|
|
8
|
+
*/
|
|
9
|
+
export function ScatterChart({ points = [], width = 560, height = 260, xFormat = fmtNum, yFormat = fmtNum, xLabel, yLabel, axes = true, legend = true, interactive = true, style }) {
|
|
10
|
+
const [hover, setHover] = React.useState(null);
|
|
11
|
+
const names = [...new Set(points.map((p) => p.series).filter(Boolean))];
|
|
12
|
+
const pad = { l: axes ? 44 : 4, r: 10, t: 10, b: axes ? 26 : 4 };
|
|
13
|
+
const [xl, xh] = extent([points.map((p) => p.x)], false);
|
|
14
|
+
const [yl, yh] = extent([points.map((p) => p.y)], false);
|
|
15
|
+
const tx = niceTicks(xl, xh, 5), ty = niceTicks(yl, yh, 4);
|
|
16
|
+
const sx = scaleLin(tx.lo, tx.hi, pad.l + 6, width - pad.r - 6);
|
|
17
|
+
const sy = scaleLin(ty.lo, ty.hi, height - pad.b - 6, pad.t + 6);
|
|
18
|
+
const rs = points.map((p) => p.r != null ? p.r : 1);
|
|
19
|
+
const [rl, rh] = extent([rs], false);
|
|
20
|
+
const sr = (r) => 3 + ((r - rl) / (rh - rl || 1)) * 11;
|
|
21
|
+
const colorOf = (p) => p.color || seriesColor(p.series ? names.indexOf(p.series) : 0);
|
|
22
|
+
const move = (e) => {
|
|
23
|
+
if (!interactive) return;
|
|
24
|
+
const rc = e.currentTarget.getBoundingClientRect();
|
|
25
|
+
const mx = e.clientX - rc.left, my = e.clientY - rc.top;
|
|
26
|
+
let best = null, bd = 24 * 24;
|
|
27
|
+
points.forEach((p, i) => { const d = (sx(p.x) - mx) ** 2 + (sy(p.y) - my) ** 2; if (d < bd) { bd = d; best = i; } });
|
|
28
|
+
setHover(best);
|
|
29
|
+
};
|
|
30
|
+
const h = hover != null ? points[hover] : null;
|
|
31
|
+
return (
|
|
32
|
+
<div style={{ ...wrapStyle, ...style }}>
|
|
33
|
+
<svg width={width} height={height} onMouseMove={move} onMouseLeave={() => setHover(null)} role="img">
|
|
34
|
+
{axes ? <g>
|
|
35
|
+
<PlotGrid w={width} h={height} pad={pad} ticks={ty.ticks} sy={sy} xLabels={[]} sxLabel={() => 0} yFormat={yFormat} />
|
|
36
|
+
{tx.ticks.map((t) => <text key={t} x={sx(t)} y={height - pad.b + 16} textAnchor="middle" style={{ fontFamily: "var(--font-mono)", fontSize: 10, fill: "var(--chart-label)" }}>{xFormat(t)}</text>)}
|
|
37
|
+
{xLabel ? <text x={width - pad.r} y={height - 2} textAnchor="end" style={{ fontFamily: "var(--font-sans)", fontSize: 10, fill: "var(--chart-label)" }}>{xLabel}</text> : null}
|
|
38
|
+
{yLabel ? <text x={pad.l} y={pad.t - 1} style={{ fontFamily: "var(--font-sans)", fontSize: 10, fill: "var(--chart-label)" }}>{yLabel}</text> : null}
|
|
39
|
+
</g> : null}
|
|
40
|
+
{points.map((p, i) => (
|
|
41
|
+
<circle key={i} cx={sx(p.x)} cy={sy(p.y)} r={sr(p.r != null ? p.r : 1)} fill={colorOf(p)}
|
|
42
|
+
opacity={hover == null ? 0.55 : hover === i ? 0.9 : 0.2} stroke={hover === i ? "var(--bg-surface)" : "none"} strokeWidth="1.5"
|
|
43
|
+
style={{ transition: "opacity var(--dur-fast) var(--ease-out)" }} />
|
|
44
|
+
))}
|
|
45
|
+
</svg>
|
|
46
|
+
{h && interactive ? (
|
|
47
|
+
<ChartTooltip x={sx(h.x)} y={sy(h.y)} flip={sx(h.x) > width * 0.62} title={h.label || h.series}
|
|
48
|
+
rows={[{ label: xLabel || "x", value: xFormat(h.x) }, { label: yLabel || "y", value: yFormat(h.y) }].concat(h.r != null ? [{ label: "size", value: fmtNum(h.r) }] : [])} />
|
|
49
|
+
) : null}
|
|
50
|
+
{legend && names.length > 1 ? <ChartLegend style={{ paddingTop: 8 }} items={names.map((nm, i) => ({ label: nm, color: seriesColor(i) }))} /> : null}
|
|
51
|
+
</div>
|
|
52
|
+
);
|
|
53
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { ref, computed } from 'vue'
|
|
3
|
+
import { fmtNum, extent, scaleLin, niceTicks, SERIES } from './chart-geometry.js'
|
|
4
|
+
// Scatter / bubble with nearest-point tooltip.
|
|
5
|
+
const props = defineProps({ points:{type:Array,default:()=>[]}, width:{type:Number,default:560}, height:{type:Number,default:260}, xFormat:{type:Function,default:fmtNum}, yFormat:{type:Function,default:fmtNum}, xLabel:String, yLabel:String, axes:{type:Boolean,default:true}, legend:{type:Boolean,default:true}, interactive:{type:Boolean,default:true}, style:Object })
|
|
6
|
+
const hover = ref(null)
|
|
7
|
+
const g = computed(() => {
|
|
8
|
+
const names = [...new Set(props.points.map(p=>p.series).filter(Boolean))]
|
|
9
|
+
const pad = { l: props.axes?44:4, r:10, t:10, b: props.axes?26:4 }
|
|
10
|
+
const [xl,xh] = extent([props.points.map(p=>p.x)], false)
|
|
11
|
+
const [yl,yh] = extent([props.points.map(p=>p.y)], false)
|
|
12
|
+
const tx = niceTicks(xl,xh,5), ty = niceTicks(yl,yh,4)
|
|
13
|
+
const sx = scaleLin(tx.lo,tx.hi,pad.l+6,props.width-pad.r-6)
|
|
14
|
+
const sy = scaleLin(ty.lo,ty.hi,props.height-pad.b-6,pad.t+6)
|
|
15
|
+
const rs = props.points.map(p=>p.r!=null?p.r:1)
|
|
16
|
+
const [rl,rh] = extent([rs], false)
|
|
17
|
+
return { names, pad, tx, ty, sx, sy,
|
|
18
|
+
dots: props.points.map((p,i)=>({ p, i, cx:sx(p.x), cy:sy(p.y), r: 3+(((p.r!=null?p.r:1)-rl)/(rh-rl||1))*11,
|
|
19
|
+
color: p.color || SERIES[(p.series?names.indexOf(p.series):0)%SERIES.length] })) }
|
|
20
|
+
})
|
|
21
|
+
function move(e){
|
|
22
|
+
if (!props.interactive) return
|
|
23
|
+
const rc = e.currentTarget.getBoundingClientRect()
|
|
24
|
+
const mx = e.clientX-rc.left, my = e.clientY-rc.top
|
|
25
|
+
let best=null, bd=576
|
|
26
|
+
g.value.dots.forEach(d=>{ const dd=(d.cx-mx)**2+(d.cy-my)**2; if(dd<bd){bd=dd;best=d.i} })
|
|
27
|
+
hover.value = best
|
|
28
|
+
}
|
|
29
|
+
</script>
|
|
30
|
+
<template>
|
|
31
|
+
<div :style="{position:'relative',display:'inline-block',lineHeight:0,...style}">
|
|
32
|
+
<svg :width="width" :height="height" @mousemove="move" @mouseleave="hover=null" role="img">
|
|
33
|
+
<template v-if="axes">
|
|
34
|
+
<g v-for="t in g.ty.ticks" :key="t">
|
|
35
|
+
<line :x1="g.pad.l" :x2="width-g.pad.r" :y1="g.sy(t)" :y2="g.sy(t)" :stroke="t===0?'var(--chart-axis)':'var(--chart-grid)'" stroke-width="1"/>
|
|
36
|
+
<text :x="g.pad.l-8" :y="g.sy(t)+3" text-anchor="end" style="font-family:var(--font-mono);font-size:10px;fill:var(--chart-label)">{{ yFormat(t) }}</text>
|
|
37
|
+
</g>
|
|
38
|
+
<text v-for="t in g.tx.ticks" :key="'x'+t" :x="g.sx(t)" :y="height-g.pad.b+16" text-anchor="middle" style="font-family:var(--font-mono);font-size:10px;fill:var(--chart-label)">{{ xFormat(t) }}</text>
|
|
39
|
+
<text v-if="xLabel" :x="width-g.pad.r" :y="height-2" text-anchor="end" style="font-family:var(--font-sans);font-size:10px;fill:var(--chart-label)">{{ xLabel }}</text>
|
|
40
|
+
<text v-if="yLabel" :x="g.pad.l" :y="g.pad.t-1" style="font-family:var(--font-sans);font-size:10px;fill:var(--chart-label)">{{ yLabel }}</text>
|
|
41
|
+
</template>
|
|
42
|
+
<circle v-for="d in g.dots" :key="d.i" :cx="d.cx" :cy="d.cy" :r="d.r" :fill="d.color"
|
|
43
|
+
:opacity="hover==null?.55:hover===d.i?.9:.2" :stroke="hover===d.i?'var(--bg-surface)':'none'" stroke-width="1.5"
|
|
44
|
+
style="transition:opacity var(--dur-fast) var(--ease-out)"/>
|
|
45
|
+
</svg>
|
|
46
|
+
<div v-if="hover!=null && interactive" :style="{position:'absolute',left:g.dots[hover].cx+'px',top:g.dots[hover].cy+'px',transform:g.dots[hover].cx>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'}">
|
|
47
|
+
<div style="font-family:var(--font-sans);font-size:var(--text-2xs);color:var(--text-muted);padding-bottom:4px;white-space:nowrap">{{ points[hover].label || points[hover].series }}</div>
|
|
48
|
+
<div style="font-family:var(--font-mono);font-size:var(--text-xs);color:var(--text-heading);white-space:nowrap">{{ (xLabel||'x')+' '+xFormat(points[hover].x) }} · {{ (yLabel||'y')+' '+yFormat(points[hover].y) }}</div>
|
|
49
|
+
</div>
|
|
50
|
+
<div v-if="legend && g.names.length>1" style="display:flex;gap:14px;padding-top:8px;line-height:normal">
|
|
51
|
+
<span v-for="(nm,i) in g.names" :key="nm" style="display:inline-flex;align-items:center;gap:6px">
|
|
52
|
+
<span :style="{width:'8px',height:'8px',borderRadius:'2px',background:SERIES[i%SERIES.length]}"/>
|
|
53
|
+
<span style="font-family:var(--font-sans);font-size:var(--text-xs);color:var(--text-secondary)">{{ nm }}</span>
|
|
54
|
+
</span>
|
|
55
|
+
</div>
|
|
56
|
+
</div>
|
|
57
|
+
</template>
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
/** Micro trend for stat tiles, table cells, list rows — no axes, no labels. */
|
|
3
|
+
export interface SparklineProps {
|
|
4
|
+
values?: number[];
|
|
5
|
+
mode?: "line" | "area" | "bars";
|
|
6
|
+
curve?: "linear" | "smooth" | "step";
|
|
7
|
+
/** "auto" colors by direction (green up / red down) */
|
|
8
|
+
tone?: "default" | "auto" | "pos" | "neg" | "muted";
|
|
9
|
+
width?: number; height?: number; strokeWidth?: number;
|
|
10
|
+
/** End-point dot (line/area) */
|
|
11
|
+
dot?: boolean;
|
|
12
|
+
/** Dashed reference value */
|
|
13
|
+
baseline?: number;
|
|
14
|
+
/** Hover readout (on by default even at tile size) */ interactive?: boolean;
|
|
15
|
+
/** Point labels shown in the hover chip */ labels?: string[];
|
|
16
|
+
valueFormat?: (v: number) => string;
|
|
17
|
+
style?: React.CSSProperties;
|
|
18
|
+
}
|
|
19
|
+
export declare function Sparkline(props: SparklineProps): JSX.Element;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { extent, scaleLin, shapePath, closeToBaseline, fmtNum } from "./chart-geometry.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Micro line/area/bar chart for stat tiles, table cells and list rows. No axes, no labels.
|
|
6
|
+
* mode: "line" | "area" | "bars". curve: "linear" | "smooth" | "step".
|
|
7
|
+
* tone: "default" (series-1) | "auto" (green up / red down) | "pos" | "neg" | "muted".
|
|
8
|
+
* Interactive by default: hovering snaps to the nearest point and shows a compact value chip.
|
|
9
|
+
*/
|
|
10
|
+
export function Sparkline({ values = [], mode = "line", curve = "linear", tone = "default", width = 96, height = 28, strokeWidth = 1.5, dot = true, baseline, interactive = true, labels, valueFormat = fmtNum, style }) {
|
|
11
|
+
const [hover, setHover] = React.useState(null);
|
|
12
|
+
const uid = React.useId ? React.useId().replace(/:/g, "") : "kbspark";
|
|
13
|
+
const [lo, hi] = extent([values], mode === "bars");
|
|
14
|
+
const pad = strokeWidth + 1.5;
|
|
15
|
+
const sx = scaleLin(0, values.length - 1 || 1, pad, width - pad);
|
|
16
|
+
const sy = scaleLin(lo, hi, height - pad, pad);
|
|
17
|
+
const up = values.length > 1 && values[values.length - 1] >= values[0];
|
|
18
|
+
const color = tone === "auto" ? (up ? "var(--chart-pos)" : "var(--chart-neg)")
|
|
19
|
+
: tone === "pos" ? "var(--chart-pos)" : tone === "neg" ? "var(--chart-neg)"
|
|
20
|
+
: tone === "muted" ? "var(--chart-guide)" : "var(--series-1)";
|
|
21
|
+
const pts = values.map((v, i) => [sx(i), sy(v)]);
|
|
22
|
+
const d = shapePath(pts, curve);
|
|
23
|
+
const bw = Math.max(2, (width - pad * 2) / (values.length || 1) - 2);
|
|
24
|
+
const move = (e) => {
|
|
25
|
+
if (!interactive) return;
|
|
26
|
+
const r = e.currentTarget.getBoundingClientRect();
|
|
27
|
+
const i = Math.round(((e.clientX - r.left - pad) / (width - pad * 2)) * (values.length - 1 || 1));
|
|
28
|
+
setHover(Math.max(0, Math.min(values.length - 1, i)));
|
|
29
|
+
};
|
|
30
|
+
const chip = hover != null && interactive ? (
|
|
31
|
+
<span style={{ position: "absolute", left: Math.min(Math.max(sx(hover), 18), width - 18), top: -4, transform: "translate(-50%,-100%)", pointerEvents: "none", whiteSpace: "nowrap", background: "var(--bg-surface)", border: "1px solid var(--border-card)", borderRadius: "var(--radius-sm)", boxShadow: "var(--shadow-menu)", padding: "2px 6px", fontFamily: "var(--font-mono)", fontSize: 10, lineHeight: "14px", color: "var(--text-heading)", zIndex: 5 }}>
|
|
32
|
+
{labels && labels[hover] ? <span style={{ color: "var(--text-muted)", paddingRight: 5 }}>{labels[hover]}</span> : null}
|
|
33
|
+
{valueFormat(values[hover])}
|
|
34
|
+
</span>
|
|
35
|
+
) : null;
|
|
36
|
+
return (
|
|
37
|
+
<span style={{ position: "relative", display: "inline-block", verticalAlign: "middle", lineHeight: 0, ...style }}>
|
|
38
|
+
<svg width={width} height={height} onMouseMove={move} onMouseLeave={() => setHover(null)} role="img" style={{ display: "block", overflow: "visible" }}>
|
|
39
|
+
{mode === "area" ? (
|
|
40
|
+
<defs>
|
|
41
|
+
<linearGradient id={uid} x1="0" y1="0" x2="0" y2="1">
|
|
42
|
+
<stop offset="0%" stopColor={color} stopOpacity="0.38" />
|
|
43
|
+
<stop offset="100%" stopColor={color} stopOpacity="0.02" />
|
|
44
|
+
</linearGradient>
|
|
45
|
+
</defs>
|
|
46
|
+
) : null}
|
|
47
|
+
{mode === "bars" ? values.map((v, i) => {
|
|
48
|
+
const y = sy(Math.max(0, v)), y0 = sy(Math.min(0, v));
|
|
49
|
+
return <rect key={i} x={sx(i) - bw / 2} y={y} width={bw} height={Math.max(1.5, y0 - y)} rx="1" fill={v < 0 ? "var(--chart-neg)" : color} opacity={hover === i ? 1 : hover != null ? 0.4 : i === values.length - 1 ? 1 : 0.55} />;
|
|
50
|
+
}) : (
|
|
51
|
+
<>
|
|
52
|
+
{baseline != null ? <line x1={pad} x2={width - pad} y1={sy(baseline)} y2={sy(baseline)} stroke="var(--chart-guide)" strokeWidth="1" strokeDasharray="2 3" /> : null}
|
|
53
|
+
{mode === "area" ? <path d={closeToBaseline(d, pts, height - pad)} fill={"url(#" + uid + ")"} /> : null}
|
|
54
|
+
<path d={d} fill="none" stroke={color} strokeWidth={strokeWidth} strokeLinejoin="round" strokeLinecap="round" />
|
|
55
|
+
{dot && pts.length ? <circle cx={pts[pts.length - 1][0]} cy={pts[pts.length - 1][1]} r={strokeWidth + 0.8} fill={color} /> : null}
|
|
56
|
+
{hover != null && interactive ? <circle cx={sx(hover)} cy={sy(values[hover])} r={strokeWidth + 1.4} fill={color} stroke="var(--bg-surface)" strokeWidth="1.5" /> : null}
|
|
57
|
+
</>
|
|
58
|
+
)}
|
|
59
|
+
</svg>
|
|
60
|
+
{chip}
|
|
61
|
+
</span>
|
|
62
|
+
);
|
|
63
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { computed, ref } from 'vue'
|
|
3
|
+
import { extent, scaleLin, shapePath, closeToBaseline, fmtNum } from './chart-geometry.js'
|
|
4
|
+
// Micro line/area/bar trend — no axes, no labels. tone "auto" = green up / red down.
|
|
5
|
+
const props = defineProps({ values:{type:Array,default:()=>[]}, mode:{type:String,default:'line'}, curve:{type:String,default:'linear'}, tone:{type:String,default:'default'}, width:{type:Number,default:96}, height:{type:Number,default:28}, strokeWidth:{type:Number,default:1.5}, dot:{type:Boolean,default:true}, baseline:Number, interactive:{type:Boolean,default:true}, labels:Array, valueFormat:{type:Function,default:fmtNum}, style:Object })
|
|
6
|
+
const hover = ref(null)
|
|
7
|
+
const uid = 'kbspark-' + Math.random().toString(36).slice(2, 8)
|
|
8
|
+
function move(e){
|
|
9
|
+
if (!props.interactive) return
|
|
10
|
+
const r = e.currentTarget.getBoundingClientRect()
|
|
11
|
+
const pad = props.strokeWidth + 1.5
|
|
12
|
+
const i = Math.round(((e.clientX-r.left-pad)/(props.width-pad*2))*(props.values.length-1||1))
|
|
13
|
+
hover.value = Math.max(0, Math.min(props.values.length-1, i))
|
|
14
|
+
}
|
|
15
|
+
const g = computed(() => {
|
|
16
|
+
const [lo,hi] = extent([props.values], props.mode==='bars')
|
|
17
|
+
const pad = props.strokeWidth + 1.5
|
|
18
|
+
const sx = scaleLin(0, props.values.length-1||1, pad, props.width-pad)
|
|
19
|
+
const sy = scaleLin(lo, hi, props.height-pad, pad)
|
|
20
|
+
const up = props.values.length>1 && props.values[props.values.length-1] >= props.values[0]
|
|
21
|
+
const color = props.tone==='auto' ? (up?'var(--chart-pos)':'var(--chart-neg)') : props.tone==='pos'?'var(--chart-pos)' : props.tone==='neg'?'var(--chart-neg)' : props.tone==='muted'?'var(--chart-guide)':'var(--series-1)'
|
|
22
|
+
const pts = props.values.map((v,i)=>[sx(i),sy(v)])
|
|
23
|
+
const bw = Math.max(2,(props.width-pad*2)/props.values.length-2)
|
|
24
|
+
return { pad, sx, sy, color, pts, bw,
|
|
25
|
+
line: shapePath(pts, props.curve), area: closeToBaseline(shapePath(pts, props.curve), pts, props.height-pad),
|
|
26
|
+
baseY: props.baseline!=null ? sy(props.baseline) : null,
|
|
27
|
+
bars: props.values.map((v,i)=>({x:sx(i)-bw/2,y:sy(Math.max(0,v)),h:Math.max(1.5,sy(Math.min(0,v))-sy(Math.max(0,v))),neg:v<0,last:i===props.values.length-1})) }
|
|
28
|
+
})
|
|
29
|
+
</script>
|
|
30
|
+
<template>
|
|
31
|
+
<span :style="{position:'relative',display:'inline-block',verticalAlign:'middle',lineHeight:0,...style}">
|
|
32
|
+
<svg :width="width" :height="height" @mousemove="move" @mouseleave="hover=null" role="img" style="display:block;overflow:visible">
|
|
33
|
+
<defs v-if="mode==='area'">
|
|
34
|
+
<linearGradient :id="uid" x1="0" y1="0" x2="0" y2="1">
|
|
35
|
+
<stop offset="0%" :stop-color="g.color" stop-opacity="0.38"/>
|
|
36
|
+
<stop offset="100%" :stop-color="g.color" stop-opacity="0.02"/>
|
|
37
|
+
</linearGradient>
|
|
38
|
+
</defs>
|
|
39
|
+
<template v-if="mode==='bars'">
|
|
40
|
+
<rect v-for="(b,i) in g.bars" :key="i" :x="b.x" :y="b.y" :width="g.bw" :height="b.h" rx="1" :fill="b.neg?'var(--chart-neg)':g.color" :opacity="hover===i?1:(hover!=null?.4:(b.last?1:.55))"/>
|
|
41
|
+
</template>
|
|
42
|
+
<template v-else>
|
|
43
|
+
<line v-if="g.baseY!=null" :x1="g.pad" :x2="width-g.pad" :y1="g.baseY" :y2="g.baseY" stroke="var(--chart-guide)" stroke-width="1" stroke-dasharray="2 3"/>
|
|
44
|
+
<path v-if="mode==='area'" :d="g.area" :fill="'url(#'+uid+')'"/>
|
|
45
|
+
<path :d="g.line" fill="none" :stroke="g.color" :stroke-width="strokeWidth" stroke-linejoin="round" stroke-linecap="round"/>
|
|
46
|
+
<circle v-if="dot && g.pts.length" :cx="g.pts[g.pts.length-1][0]" :cy="g.pts[g.pts.length-1][1]" :r="strokeWidth+0.8" :fill="g.color"/>
|
|
47
|
+
<circle v-if="hover!=null && interactive" :cx="g.sx(hover)" :cy="g.sy(values[hover])" :r="strokeWidth+1.4" :fill="g.color" stroke="var(--bg-surface)" stroke-width="1.5"/>
|
|
48
|
+
</template>
|
|
49
|
+
</svg>
|
|
50
|
+
<span v-if="hover!=null && interactive" :style="{position:'absolute',left:Math.min(Math.max(g.sx(hover),18),width-18)+'px',top:'-4px',transform:'translate(-50%,-100%)',pointerEvents:'none',whiteSpace:'nowrap',background:'var(--bg-surface)',border:'1px solid var(--border-card)',borderRadius:'var(--radius-sm)',boxShadow:'var(--shadow-menu)',padding:'2px 6px',fontFamily:'var(--font-mono)',fontSize:'10px',lineHeight:'14px',color:'var(--text-heading)',zIndex:5}">
|
|
51
|
+
<span v-if="labels && labels[hover]" style="color:var(--text-muted);padding-right:5px">{{ labels[hover] }}</span>{{ valueFormat(values[hover]) }}
|
|
52
|
+
</span>
|
|
53
|
+
</span>
|
|
54
|
+
</template>
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
export interface SunburstNode { label: string; value?: number; children?: SunburstNode[]; }
|
|
3
|
+
/** Hierarchy as concentric rings; click to zoom a branch, click the center to go back. */
|
|
4
|
+
export interface SunburstChartProps {
|
|
5
|
+
data?: SunburstNode;
|
|
6
|
+
size?: number;
|
|
7
|
+
/** Ring thickness; derived from depth when omitted */ ringWidth?: number;
|
|
8
|
+
valueFormat?: (v: number) => string;
|
|
9
|
+
interactive?: boolean;
|
|
10
|
+
style?: React.CSSProperties;
|
|
11
|
+
}
|
|
12
|
+
export declare function SunburstChart(props: SunburstChartProps): JSX.Element;
|
|
@@ -0,0 +1,61 @@
|
|
|
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
|
+
* Sunburst — a hierarchy as concentric rings (the radial treemap). data: a root
|
|
7
|
+
* {label, children:[{label, value, children?}]} tree; leaf values roll up. Clicking a
|
|
8
|
+
* ring zooms into that branch, and the center returns to the parent.
|
|
9
|
+
*/
|
|
10
|
+
export function SunburstChart({ data, size = 320, ringWidth, valueFormat = fmtNum, interactive = true, style }) {
|
|
11
|
+
const [zoom, setZoom] = React.useState([]);
|
|
12
|
+
const [hover, setHover] = React.useState(null);
|
|
13
|
+
const roll = (n) => (n.children && n.children.length ? { ...n, value: n.children.reduce((s, c) => s + roll(c).value, 0), children: n.children.map(roll) } : { ...n, value: n.value || 0 });
|
|
14
|
+
const rooted = data ? roll(data) : { label: "", value: 0, children: [] };
|
|
15
|
+
const focus = zoom.reduce((n, i) => (n.children ? n.children[i] : n), rooted);
|
|
16
|
+
const depthOf = (n) => (n.children && n.children.length ? 1 + Math.max(...n.children.map(depthOf)) : 0);
|
|
17
|
+
const rings = Math.max(1, depthOf(focus));
|
|
18
|
+
const rInner = size * 0.17;
|
|
19
|
+
const rw = ringWidth || (size / 2 - 6 - rInner) / rings;
|
|
20
|
+
const segs = [];
|
|
21
|
+
const walk = (node, depth, a0, a1, path, colorIdx) => {
|
|
22
|
+
(node.children || []).forEach((c, ci) => {
|
|
23
|
+
const span = (c.value / (node.value || 1)) * (a1 - a0);
|
|
24
|
+
const idx = depth === 0 ? ci : colorIdx;
|
|
25
|
+
segs.push({ node: c, depth, a0, a1: a0 + span, path: [...path, ci], colorIdx: idx });
|
|
26
|
+
walk(c, depth + 1, a0, a0 + span, [...path, ci], idx);
|
|
27
|
+
a0 += span;
|
|
28
|
+
});
|
|
29
|
+
};
|
|
30
|
+
walk(focus, 0, 0, 360, [], 0);
|
|
31
|
+
const h = hover != null ? segs[hover] : null;
|
|
32
|
+
const tipAt = h ? polar(size / 2, size / 2, rInner + rw * (h.depth + 0.5), (h.a0 + h.a1) / 2) : null;
|
|
33
|
+
return (
|
|
34
|
+
<div style={{ ...wrapStyle, ...style }}>
|
|
35
|
+
<div style={{ position: "relative", width: size, height: size }}>
|
|
36
|
+
<svg width={size} height={size} role="img">
|
|
37
|
+
{segs.map((s, i) => (
|
|
38
|
+
<path key={i} d={arcPath(size / 2, size / 2, rInner + rw * (s.depth + 1) - 1.5, rInner + rw * s.depth, s.a0 + 0.35, Math.max(s.a0 + 0.5, s.a1 - 0.35))}
|
|
39
|
+
fill={seriesColor(s.colorIdx)} opacity={hover == null ? 0.92 - s.depth * 0.16 : hover === i ? 1 : 0.28}
|
|
40
|
+
onMouseEnter={interactive ? () => setHover(i) : undefined} onMouseLeave={interactive ? () => setHover(null) : undefined}
|
|
41
|
+
onClick={interactive && s.node.children ? () => { setZoom([...zoom, ...s.path]); setHover(null); } : undefined}
|
|
42
|
+
style={{ transition: "opacity var(--dur-fast) var(--ease-out)", cursor: interactive && s.node.children ? "pointer" : "default" }} />
|
|
43
|
+
))}
|
|
44
|
+
<circle cx={size / 2} cy={size / 2} r={rInner - 2} fill="var(--bg-surface)" stroke="var(--border-subtle)" strokeWidth="1"
|
|
45
|
+
onClick={interactive && zoom.length ? () => setZoom(zoom.slice(0, -1)) : undefined} style={{ cursor: interactive && zoom.length ? "pointer" : "default" }} />
|
|
46
|
+
</svg>
|
|
47
|
+
<div style={{ position: "absolute", inset: 0, display: "flex", flexDirection: "column", alignItems: "center", justifyContent: "center", pointerEvents: "none", lineHeight: "normal", gap: 2 }}>
|
|
48
|
+
<span style={{ fontFamily: "var(--font-sans)", fontWeight: "var(--weight-light)", fontSize: Math.round(size / 13), color: "var(--text-heading)" }}>{valueFormat(focus.value)}</span>
|
|
49
|
+
<span style={{ fontFamily: "var(--font-sans)", fontSize: "var(--text-2xs)", color: "var(--text-muted)", maxWidth: rInner * 1.7, textAlign: "center", whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }}>
|
|
50
|
+
{zoom.length ? focus.label : focus.label || "Total"}
|
|
51
|
+
</span>
|
|
52
|
+
{zoom.length ? <span style={{ fontFamily: "var(--font-sans)", fontSize: 9, color: "var(--action-primary)" }}>↑ back</span> : null}
|
|
53
|
+
</div>
|
|
54
|
+
{h && interactive ? (
|
|
55
|
+
<ChartTooltip x={tipAt[0]} y={tipAt[1]} flip={tipAt[0] > size * 0.55} title={h.node.label}
|
|
56
|
+
rows={[{ label: "value", value: valueFormat(h.node.value), color: seriesColor(h.colorIdx) }, { label: "of parent", value: ((h.node.value / (focus.value || 1)) * 100).toFixed(1) + "%" }]} />
|
|
57
|
+
) : null}
|
|
58
|
+
</div>
|
|
59
|
+
</div>
|
|
60
|
+
);
|
|
61
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { ref, computed } from 'vue'
|
|
3
|
+
import { fmtNum, arcPath, polar, SERIES } from './chart-geometry.js'
|
|
4
|
+
// Hierarchy as concentric rings; click to zoom a branch, click the center to go back.
|
|
5
|
+
const props = defineProps({ data:Object, size:{type:Number,default:320}, ringWidth:Number, valueFormat:{type:Function,default:fmtNum}, interactive:{type:Boolean,default:true}, style:Object })
|
|
6
|
+
const zoom = ref([]); const hover = ref(null)
|
|
7
|
+
function roll(n){ return n.children && n.children.length ? Object.assign({}, n, { value: n.children.reduce((s,c)=>s+roll(c).value,0), children: n.children.map(roll) }) : Object.assign({}, n, { value: n.value||0 }) }
|
|
8
|
+
const g = computed(() => {
|
|
9
|
+
const rooted = props.data ? roll(props.data) : { label:'', value:0, children:[] }
|
|
10
|
+
const focus = zoom.value.reduce((n,i)=> n.children ? n.children[i] : n, rooted)
|
|
11
|
+
const depthOf = n => n.children && n.children.length ? 1+Math.max(...n.children.map(depthOf)) : 0
|
|
12
|
+
const rings = Math.max(1, depthOf(focus))
|
|
13
|
+
const rInner = props.size*0.17
|
|
14
|
+
const rw = props.ringWidth || (props.size/2-6-rInner)/rings
|
|
15
|
+
const segs = []
|
|
16
|
+
const walk = (node, depth, a0, a1, path, colorIdx) => {
|
|
17
|
+
(node.children||[]).forEach((c,ci)=>{
|
|
18
|
+
const span = (c.value/(node.value||1))*(a1-a0)
|
|
19
|
+
const idx = depth===0 ? ci : colorIdx
|
|
20
|
+
segs.push({ node:c, depth, a0, a1:a0+span, path:[...path,ci], colorIdx:idx,
|
|
21
|
+
d: arcPath(props.size/2, props.size/2, rInner+rw*(depth+1)-1.5, rInner+rw*depth, a0+0.35, Math.max(a0+0.5, a0+span-0.35)),
|
|
22
|
+
color: SERIES[idx%SERIES.length] })
|
|
23
|
+
walk(c, depth+1, a0, a0+span, [...path,ci], idx)
|
|
24
|
+
a0 += span
|
|
25
|
+
})
|
|
26
|
+
}
|
|
27
|
+
walk(focus, 0, 0, 360, [], 0)
|
|
28
|
+
return { focus, rInner, rw, segs }
|
|
29
|
+
})
|
|
30
|
+
const tip = computed(()=>{
|
|
31
|
+
if (hover.value==null) return null
|
|
32
|
+
const s = g.value.segs[hover.value]
|
|
33
|
+
const p = polar(props.size/2, props.size/2, g.value.rInner+g.value.rw*(s.depth+0.5), (s.a0+s.a1)/2)
|
|
34
|
+
return { x:p[0], y:p[1], title:s.node.label, value: props.valueFormat(s.node.value)+' · '+((s.node.value/(g.value.focus.value||1))*100).toFixed(1)+'%' }
|
|
35
|
+
})
|
|
36
|
+
</script>
|
|
37
|
+
<template>
|
|
38
|
+
<div :style="{position:'relative',display:'inline-block',lineHeight:0,...style}">
|
|
39
|
+
<div :style="{position:'relative',width:size+'px',height:size+'px'}">
|
|
40
|
+
<svg :width="size" :height="size" role="img">
|
|
41
|
+
<path v-for="(s,i) in g.segs" :key="i" :d="s.d" :fill="s.color"
|
|
42
|
+
:opacity="hover==null ? 0.92-s.depth*0.16 : (hover===i?1:0.28)"
|
|
43
|
+
@mouseenter="interactive && (hover=i)" @mouseleave="interactive && (hover=null)"
|
|
44
|
+
@click="interactive && s.node.children && (zoom=[...zoom,...s.path], hover=null)"
|
|
45
|
+
:style="{transition:'opacity var(--dur-fast) var(--ease-out)',cursor:interactive&&s.node.children?'pointer':'default'}"/>
|
|
46
|
+
<circle :cx="size/2" :cy="size/2" :r="g.rInner-2" fill="var(--bg-surface)" stroke="var(--border-subtle)" stroke-width="1"
|
|
47
|
+
@click="interactive && zoom.length && (zoom=zoom.slice(0,-1))" :style="{cursor:interactive&&zoom.length?'pointer':'default'}"/>
|
|
48
|
+
</svg>
|
|
49
|
+
<div style="position:absolute;inset:0;display:flex;flex-direction:column;align-items:center;justify-content:center;pointer-events:none;line-height:normal;gap:2px">
|
|
50
|
+
<span :style="{fontFamily:'var(--font-sans)',fontWeight:'var(--weight-light)',fontSize:Math.round(size/13)+'px',color:'var(--text-heading)'}">{{ valueFormat(g.focus.value) }}</span>
|
|
51
|
+
<span :style="{fontFamily:'var(--font-sans)',fontSize:'var(--text-2xs)',color:'var(--text-muted)',maxWidth:g.rInner*1.7+'px',textAlign:'center',whiteSpace:'nowrap',overflow:'hidden',textOverflow:'ellipsis'}">{{ g.focus.label || 'Total' }}</span>
|
|
52
|
+
<span v-if="zoom.length" style="font-family:var(--font-sans);font-size:9px;color:var(--action-primary)">↑ back</span>
|
|
53
|
+
</div>
|
|
54
|
+
<div v-if="tip && interactive" :style="{position:'absolute',left:tip.x+'px',top:tip.y+'px',transform:tip.x>size*0.55?'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'}">
|
|
55
|
+
<div style="font-family:var(--font-sans);font-size:var(--text-2xs);color:var(--text-muted);padding-bottom:3px">{{ tip.title }}</div>
|
|
56
|
+
<div style="font-family:var(--font-mono);font-size:var(--text-xs);color:var(--text-heading)">{{ tip.value }}</div>
|
|
57
|
+
</div>
|
|
58
|
+
</div>
|
|
59
|
+
</div>
|
|
60
|
+
</template>
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
/** 3D surface z = f(x,y) over a grid; drag to orbit. Cells shade on the heat ramp by height. */
|
|
3
|
+
export interface Surface3DProps {
|
|
4
|
+
/** values[row][col] */ values?: number[][];
|
|
5
|
+
width?: number; height?: number;
|
|
6
|
+
/** Initial orbit angles (degrees) */ yaw?: number; pitch?: number;
|
|
7
|
+
/** Vertical exaggeration, 0–1 */ zScale?: number;
|
|
8
|
+
/** Cell outlines */ wire?: boolean;
|
|
9
|
+
/** Drag-to-orbit */ interactive?: boolean;
|
|
10
|
+
valueFormat?: (v: number) => string;
|
|
11
|
+
style?: React.CSSProperties;
|
|
12
|
+
}
|
|
13
|
+
export declare function Surface3D(props: Surface3DProps): JSX.Element;
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { fmtNum, niceTicks } from "./chart-geometry.js";
|
|
3
|
+
import { makeProj, facePath, faceDepth, shade, lightOf, dragRotate, heatColor } from "./chart-3d.js";
|
|
4
|
+
import { ChartTooltip, wrapStyle } from "./ChartKit.jsx";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* 3D surface — z = f(x, y) over a grid. values[row][col]; drag to orbit. Cells shade on
|
|
8
|
+
* the sequential heat ramp by height with light from the upper left, so the shape reads
|
|
9
|
+
* in both themes without a legend.
|
|
10
|
+
*/
|
|
11
|
+
export function Surface3D({ values = [], width = 420, height = 300, yaw: yaw0 = 38, pitch: pitch0 = 32, zScale = 0.55, wire = true, xLabel = "x", yLabel = "y", interactive = true, valueFormat = fmtNum, style }) {
|
|
12
|
+
const [rot, setRot] = React.useState({ yaw: yaw0, pitch: pitch0 });
|
|
13
|
+
const [hover, setHover] = React.useState(null);
|
|
14
|
+
const rows = values.length, cols = values[0] ? values[0].length : 0;
|
|
15
|
+
let lo = Infinity, hi = -Infinity;
|
|
16
|
+
values.forEach((r) => r.forEach((v) => { if (v < lo) lo = v; if (v > hi) hi = v; }));
|
|
17
|
+
const span = hi - lo || 1;
|
|
18
|
+
const scale = Math.min(width, height) * 0.42;
|
|
19
|
+
const P = makeProj(width / 2, height / 2 + height * 0.05, scale, rot.yaw, rot.pitch);
|
|
20
|
+
const pt = (r, c) => P((c / (cols - 1)) - 0.5, (((values[r][c] - lo) / span) - 0.5) * zScale * 2, (r / (rows - 1)) - 0.5);
|
|
21
|
+
const quads = [];
|
|
22
|
+
for (let r = 0; r < rows - 1; r++) for (let c = 0; c < cols - 1; c++) {
|
|
23
|
+
const pts = [pt(r, c), pt(r, c + 1), pt(r + 1, c + 1), pt(r + 1, c)];
|
|
24
|
+
const avg = (values[r][c] + values[r][c + 1] + values[r + 1][c] + values[r + 1][c + 1]) / 4;
|
|
25
|
+
const t = (avg - lo) / span;
|
|
26
|
+
quads.push({ pts, t, avg, r, c, depth: faceDepth(pts), light: lightOf(pts[0], pts[1], pts[3]) });
|
|
27
|
+
}
|
|
28
|
+
quads.sort((a, b) => b.depth - a.depth);
|
|
29
|
+
/* Base frame + three axes meeting at the front-left corner: X along the columns,
|
|
30
|
+
Y along the rows (depth), Z vertical with real ticks. */
|
|
31
|
+
const yB = -zScale - 0.06;
|
|
32
|
+
const base = [P(-0.5, yB, -0.5), P(0.5, yB, -0.5), P(0.5, yB, 0.5), P(-0.5, yB, 0.5)];
|
|
33
|
+
const basePath = "M" + base.map((p) => p.x.toFixed(1) + " " + p.y.toFixed(1)).join(" L") + " Z";
|
|
34
|
+
const corner = P(-0.5, yB, 0.5);
|
|
35
|
+
const axes3 = [
|
|
36
|
+
{ a: corner, b: P(0.5, yB, 0.5), label: xLabel, lp: P(0, yB - 0.16, 0.5), anchor: "middle" },
|
|
37
|
+
{ a: corner, b: P(-0.5, yB, -0.5), label: yLabel, lp: P(-0.62, yB - 0.12, 0), anchor: "end" },
|
|
38
|
+
{ a: corner, b: P(-0.5, zScale, 0.5) },
|
|
39
|
+
];
|
|
40
|
+
const zt = niceTicks(lo, hi, 3);
|
|
41
|
+
const zTicks = zt.ticks.filter((t) => t >= lo && t <= hi).map((t) => ({
|
|
42
|
+
p: P(-0.5, ((t - lo) / span - 0.5) * zScale * 2, 0.5), v: valueFormat(t),
|
|
43
|
+
}));
|
|
44
|
+
const handlers = interactive ? dragRotate(() => rot, setRot) : {};
|
|
45
|
+
return (
|
|
46
|
+
<div style={{ ...wrapStyle, ...style }}>
|
|
47
|
+
<svg width={width} height={height} {...handlers} role="img" style={{ cursor: interactive ? "grab" : "default", touchAction: "none" }}>
|
|
48
|
+
<path d={basePath} fill="var(--bg-inset)" stroke="var(--chart-axis)" strokeWidth="1" />
|
|
49
|
+
{axes3.map((ax, i) => <line key={"ae" + i} x1={ax.a.x} y1={ax.a.y} x2={ax.b.x} y2={ax.b.y} stroke="var(--chart-guide)" strokeWidth="1" />)}
|
|
50
|
+
{axes3.map((ax, i) => ax.label ? <text key={"al" + i} x={ax.lp.x} y={ax.lp.y + 3} textAnchor={ax.anchor} style={{ fontFamily: "var(--font-sans)", fontSize: 10, fill: "var(--text-secondary)" }}>{ax.label}</text> : null)}
|
|
51
|
+
{zTicks.map((t, i) => (
|
|
52
|
+
<g key={"zt" + i}>
|
|
53
|
+
<line x1={t.p.x - 4} y1={t.p.y} x2={t.p.x} y2={t.p.y} stroke="var(--chart-guide)" strokeWidth="1" />
|
|
54
|
+
<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>
|
|
55
|
+
</g>
|
|
56
|
+
))}
|
|
57
|
+
{quads.map((q, i) => (
|
|
58
|
+
<path key={i} d={facePath(q.pts)} fill={shade(heatColor(q.t), Math.max(-0.35, Math.min(0.3, q.light * 0.4)) + (hover && hover.i === i ? 0.18 : 0))}
|
|
59
|
+
stroke={wire ? "var(--bg-surface)" : "none"} strokeWidth="0.5"
|
|
60
|
+
onMouseEnter={interactive ? () => setHover({ i, q, x: q.pts.reduce((s, p) => s + p.x, 0) / 4, y: q.pts.reduce((s, p) => s + p.y, 0) / 4 }) : undefined}
|
|
61
|
+
onMouseLeave={interactive ? () => setHover(null) : undefined} />
|
|
62
|
+
))}
|
|
63
|
+
</svg>
|
|
64
|
+
{hover && interactive ? (
|
|
65
|
+
<ChartTooltip x={hover.x} y={hover.y} flip={hover.x > width * 0.6} title={"row " + (hover.q.r + 1) + " · col " + (hover.q.c + 1)}
|
|
66
|
+
rows={[{ label: "value", value: valueFormat(hover.q.avg), color: heatColor(hover.q.t) }]} />
|
|
67
|
+
) : null}
|
|
68
|
+
</div>
|
|
69
|
+
);
|
|
70
|
+
}
|