@tetrascience-npm/tetrascience-react-ui 1.0.0-beta.148.1 → 1.0.0-beta.150.1
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/dist/components/charts/AreaPlot/AreaPlot.cjs +1 -1
- package/dist/components/charts/AreaPlot/AreaPlot.cjs.map +1 -1
- package/dist/components/charts/AreaPlot/AreaPlot.js +166 -148
- package/dist/components/charts/AreaPlot/AreaPlot.js.map +1 -1
- package/dist/components/charts/BarChart/BarChart.cjs +1 -1
- package/dist/components/charts/BarChart/BarChart.cjs.map +1 -1
- package/dist/components/charts/BarChart/BarChart.d.ts +7 -0
- package/dist/components/charts/BarChart/BarChart.js +151 -120
- package/dist/components/charts/BarChart/BarChart.js.map +1 -1
- package/dist/components/charts/BoxPlot/BoxPlot.cjs +1 -1
- package/dist/components/charts/BoxPlot/BoxPlot.cjs.map +1 -1
- package/dist/components/charts/BoxPlot/BoxPlot.d.ts +4 -0
- package/dist/components/charts/BoxPlot/BoxPlot.js +133 -102
- package/dist/components/charts/BoxPlot/BoxPlot.js.map +1 -1
- package/dist/components/charts/ChartTooltip/lines.cjs.map +1 -1
- package/dist/components/charts/ChartTooltip/lines.d.ts +2 -0
- package/dist/components/charts/ChartTooltip/lines.js.map +1 -1
- package/dist/components/charts/Chromatogram/Chromatogram.cjs +1 -1
- package/dist/components/charts/Chromatogram/Chromatogram.cjs.map +1 -1
- package/dist/components/charts/Chromatogram/Chromatogram.js +111 -114
- package/dist/components/charts/Chromatogram/Chromatogram.js.map +1 -1
- package/dist/components/charts/Chromatogram/plotBuilder.cjs +4 -1
- package/dist/components/charts/Chromatogram/plotBuilder.cjs.map +1 -1
- package/dist/components/charts/Chromatogram/plotBuilder.d.ts +32 -2
- package/dist/components/charts/Chromatogram/plotBuilder.js +170 -127
- package/dist/components/charts/Chromatogram/plotBuilder.js.map +1 -1
- package/dist/components/charts/Chromatogram/regionOverlays.cjs +1 -1
- package/dist/components/charts/Chromatogram/regionOverlays.cjs.map +1 -1
- package/dist/components/charts/Chromatogram/regionOverlays.js +11 -11
- package/dist/components/charts/Chromatogram/regionOverlays.js.map +1 -1
- package/dist/components/charts/Chromatogram/types.d.ts +4 -2
- package/dist/components/charts/Electropherogram/Electropherogram.cjs +1 -1
- package/dist/components/charts/Electropherogram/Electropherogram.cjs.map +1 -1
- package/dist/components/charts/Electropherogram/Electropherogram.js +89 -88
- package/dist/components/charts/Electropherogram/Electropherogram.js.map +1 -1
- package/dist/components/charts/ScatterPlotInteractive/ScatterPlotInteractive.cjs +1 -1
- package/dist/components/charts/ScatterPlotInteractive/ScatterPlotInteractive.cjs.map +1 -1
- package/dist/components/charts/ScatterPlotInteractive/ScatterPlotInteractive.js +4 -5
- package/dist/components/charts/ScatterPlotInteractive/ScatterPlotInteractive.js.map +1 -1
- package/dist/components/charts/chart-scale.cjs +2 -0
- package/dist/components/charts/chart-scale.cjs.map +1 -0
- package/dist/components/charts/chart-scale.d.ts +61 -0
- package/dist/components/charts/chart-scale.js +61 -0
- package/dist/components/charts/chart-scale.js.map +1 -0
- package/dist/index.css +1 -1
- package/dist/index.scoped.css +1 -1
- package/dist/index.tailwind.css +1 -1
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ScatterPlotInteractive.cjs","sources":["../../../../src/components/charts/ScatterPlotInteractive/ScatterPlotInteractive.tsx"],"sourcesContent":["import React, { useEffect, useMemo, useRef, useState } from \"react\";\n\nimport \"./ScatterPlotInteractive.scss\";\nimport { useChartTooltip } from \"../ChartTooltip\";\nimport { getLoadedPlotly, loadPlotly } from \"../plotly-loader\";\n\nimport { COLORS, DEFAULT_COLOR_SCALE, PLOT_CONSTANTS } from \"./constants\";\nimport {\n applySelection,\n calculateAxisRange,\n calculateRange,\n downsampleData,\n generateTooltipContent,\n getPlotlyLayoutConfig,\n getSelectionMode,\n mapColors,\n mapShapes,\n mapSizes,\n} from \"./utils\";\n\nimport type { AxisConfig, ScatterPlotInteractiveProps, SelectionMode, TooltipConfig } from \"./types\";\nimport type Plotly from \"plotly.js-dist\";\n\nimport { usePlotlyTheme } from \"@/hooks/use-plotly-theme\";\n\n// Stable default prop objects — inline defaults would create a new identity\n// on every render, retriggering the plot effect (and tearing down the\n// tooltip) whenever internal state changes\nconst DEFAULT_AXIS_CONFIG: AxisConfig = {};\nconst DEFAULT_TOOLTIP_CONFIG: TooltipConfig = { enabled: true };\n\n/**\n * ScatterPlotInteractive component for visualizing scatter plot data with advanced interactions.\n *\n * **Features:**\n * - Data-driven and static styling (color, shape, size)\n * - Interactive selection (click, box, lasso)\n * - Keyboard modifiers for click-selection modes (Shift/Ctrl)\n * - Customizable tooltips with rich content support\n * - Axis customization (ranges, log/linear scales)\n * - Performance optimizations for large datasets\n * - Selection propagation via callbacks\n *\n * **Selection Modes (click selection only):**\n * - Default click: Replace selection\n * - Shift + click: Add to selection\n * - Ctrl/Cmd + click: Remove from selection\n * - Shift + Ctrl + click: Toggle selection\n *\n * Box/lasso selection always replaces the current selection because\n * Plotly does not expose the original keyboard event for drag operations.\n */\nconst ScatterPlotInteractive: React.FC<ScatterPlotInteractiveProps> = ({\n data,\n title,\n xAxis = DEFAULT_AXIS_CONFIG,\n yAxis = DEFAULT_AXIS_CONFIG,\n colorMapping,\n shapeMapping,\n sizeMapping,\n tooltip = DEFAULT_TOOLTIP_CONFIG,\n enableClickSelection = true,\n enableBoxSelection = true,\n enableLassoSelection = true,\n selectedIds: controlledSelectedIds,\n onSelectionChange,\n onPointClick,\n downsampling,\n width = 800,\n height = 600,\n showColorBar = true,\n className,\n}) => {\n const plotRef = useRef<HTMLDivElement>(null);\n const theme = usePlotlyTheme();\n const onSelectionChangeRef = useRef(onSelectionChange);\n const onPointClickRef = useRef(onPointClick);\n const selectedIdsRef = useRef<Set<string | number>>(new Set());\n const isControlledRef = useRef(false);\n\n // Keep refs updated\n onSelectionChangeRef.current = onSelectionChange;\n onPointClickRef.current = onPointClick;\n\n // Internal selection state (only used in uncontrolled mode)\n const [internalSelectedIds, setInternalSelectedIds] = useState<Set<string | number>>(new Set());\n\n // Use controlled or uncontrolled selection\n const isControlled = controlledSelectedIds !== undefined;\n const selectedIds = isControlled ? controlledSelectedIds : internalSelectedIds;\n selectedIdsRef.current = selectedIds;\n isControlledRef.current = isControlled;\n\n // Normalize to strings so numeric IDs passed by consumers always match the\n // string IDs that Plotly stores (we always do String(p.id) when building the trace).\n const normalizedSelectedIds = useMemo(() => new Set([...selectedIds].map(String)), [selectedIds]);\n\n // Reverse-lookup: Plotly string ID → original (possibly numeric) ScatterPoint ID.\n // Used to restore original ID types when echoing selections back to consumers.\n const originalIdLookup = useMemo(() => new Map(data.map((p) => [String(p.id), p.id])), [data]);\n\n // Apply downsampling if configured\n const processedData = useMemo(() => {\n if (!downsampling) {\n return data;\n }\n return downsampleData(data, downsampling);\n }, [data, downsampling]);\n\n // Map colors, shapes, and sizes based on configuration\n const colors = useMemo(() => mapColors(processedData, colorMapping), [processedData, colorMapping]);\n\n const shapes = useMemo(() => mapShapes(processedData, shapeMapping), [processedData, shapeMapping]);\n\n const sizes = useMemo(() => mapSizes(processedData, sizeMapping), [processedData, sizeMapping]);\n\n // Calculate axis ranges\n const xRange = useMemo(() => {\n if (xAxis.range) return xAxis.range;\n if (xAxis.autoRange === false) return;\n return calculateAxisRange(processedData, \"x\", xAxis.autoRangePadding ?? PLOT_CONSTANTS.AUTO_RANGE_PADDING, xAxis.scale);\n }, [processedData, xAxis]);\n\n const yRange = useMemo(() => {\n if (yAxis.range) return yAxis.range;\n if (yAxis.autoRange === false) return;\n return calculateAxisRange(processedData, \"y\", yAxis.autoRangePadding ?? PLOT_CONSTANTS.AUTO_RANGE_PADDING, yAxis.scale);\n }, [processedData, yAxis]);\n\n const tooltipEnabled = tooltip.enabled !== false;\n const nativeTooltip = tooltip.native === true;\n\n // Build tooltip text\n const tooltipText = useMemo(() => {\n if (!tooltipEnabled) return [];\n\n return processedData.map((point) =>\n tooltip.content ? tooltip.content(point) : generateTooltipContent(point, tooltip.fields),\n );\n }, [processedData, tooltip, tooltipEnabled]);\n\n // Design-system tooltip driven by Plotly hover events\n const { bindTooltip, tooltipElement } = useChartTooltip({\n getLines: (points) => {\n const index = points[0]?.pointIndex;\n if (index === undefined) return [];\n return (tooltipText[index] ?? \"\").split(\"<br>\").filter(Boolean);\n },\n });\n\n // Prepare Plotly-compatible color data\n const plotlyColors = useMemo(() => {\n if (colorMapping?.type === \"continuous\" && colorMapping.field && processedData.length > 0) {\n // For continuous mapping, extract the numeric values\n return processedData.map((point) => {\n const value = point.metadata?.[colorMapping.field!];\n return typeof value === \"number\" && Number.isFinite(value) ? value : 0;\n });\n }\n return colors;\n }, [processedData, colorMapping, colors]);\n\n // Build colorscale for continuous mapping\n const plotlyColorscale = useMemo(() => {\n if (colorMapping?.type === \"continuous\") {\n const scale = colorMapping.colorScale || DEFAULT_COLOR_SCALE;\n return typeof scale === \"string\" ? scale : scale;\n }\n }, [colorMapping]);\n\n // Build marker configuration for Plotly\n const markerConfig = useMemo((): Partial<Plotly.PlotMarker> => {\n const config: Partial<Plotly.PlotMarker> = {\n size: sizes,\n symbol: shapes,\n // Points carry no outline; depth comes from a CSS drop-shadow\n // (see ScatterPlotInteractive.scss). Selected points re-add a line below.\n line: {\n width: 0,\n },\n };\n\n // Configure color mapping\n if (colorMapping?.type === \"continuous\" && plotlyColorscale) {\n config.color = plotlyColors as number[];\n config.colorscale = plotlyColorscale;\n config.showscale = showColorBar;\n\n if (colorMapping.field) {\n const range =\n colorMapping.min !== undefined && colorMapping.max !== undefined\n ? { min: colorMapping.min, max: colorMapping.max }\n : calculateRange(processedData, colorMapping.field);\n config.cmin = range.min;\n config.cmax = range.max;\n config.colorbar = {\n title: { text: colorMapping.field, side: \"right\" },\n thickness: 20,\n len: 0.7,\n };\n }\n } else {\n config.color = colors;\n }\n\n return config;\n }, [sizes, shapes, colorMapping, plotlyColorscale, plotlyColors, showColorBar, processedData, colors]);\n\n // Create Plotly plot\n useEffect(() => {\n const currentRef = plotRef.current;\n if (!currentRef || processedData.length === 0) return;\n\n const xValues = processedData.map((p) => p.x);\n const yValues = processedData.map((p) => p.y);\n const ids = processedData.map((p) => String(p.id));\n\n // Create base trace\n const trace: Partial<Plotly.PlotData> & {\n unselected?: { marker?: { opacity?: number } };\n selected?: { marker?: { opacity?: number; line?: { color?: string; width?: number } } };\n } = {\n x: xValues,\n y: yValues,\n ids,\n mode: \"markers\",\n type: \"scatter\",\n marker: markerConfig,\n // \"none\" keeps hover events firing for the HTML tooltip without\n // showing Plotly's hover label; \"skip\" disables hover entirely\n hoverinfo: tooltipEnabled ? (nativeTooltip ? \"text\" : \"none\") : \"skip\",\n text: tooltipText,\n hovertemplate: tooltipEnabled && nativeTooltip ? \"%{text}<extra></extra>\" : undefined,\n showlegend: false,\n unselected: {\n marker: {\n opacity: 0.3,\n },\n },\n selected: {\n marker: {\n opacity: 1,\n line: {\n color: COLORS.selected,\n width: 2,\n },\n },\n },\n };\n\n const plotData: Plotly.Data[] = [trace as Plotly.Data];\n\n // Configure layout\n const layout: Partial<Plotly.Layout> = getPlotlyLayoutConfig({\n title,\n xAxis,\n yAxis,\n width,\n height,\n xRange,\n yRange,\n enableLassoSelection,\n enableBoxSelection,\n theme,\n });\n\n const config: Partial<Plotly.Config> = {\n responsive: true,\n displayModeBar: true,\n displaylogo: false,\n modeBarButtonsToAdd: [],\n modeBarButtonsToRemove: [\"toImage\"],\n };\n\n // Add selection mode buttons if enabled\n if (enableBoxSelection) {\n config.modeBarButtonsToAdd?.push(\"select2d\" as never);\n }\n if (enableLassoSelection) {\n config.modeBarButtonsToAdd?.push(\"lasso2d\" as never);\n }\n\n // Plotly is loaded lazily so it stays out of the consumer's main chunk\n // (SW-2007); the draw is skipped if the effect re-runs or unmounts first.\n let cancelled = false;\n let drawnElement: HTMLDivElement | null = null;\n void loadPlotly().then((plotly) => {\n if (cancelled || !plotRef.current) return;\n plotly.newPlot(currentRef, plotData, layout, config);\n\n // Attach event handlers\n const plotElement = currentRef as unknown as Plotly.PlotlyHTMLElement;\n\n // Handle point click\n if (enableClickSelection) {\n plotElement.on(\"plotly_click\", (eventData: Plotly.PlotMouseEvent) => {\n const point = eventData.points[0];\n if (point && point.data.ids) {\n const clickedId = point.data.ids[point.pointIndex];\n const clickedPoint = processedData.find((p) => String(p.id) === clickedId);\n\n if (clickedPoint) {\n // Call point click handler if provided\n onPointClickRef.current?.(clickedPoint, eventData.event as MouseEvent);\n\n // Handle selection. Use the original-typed ID from the matched point so\n // numeric IDs (e.g., 123) are preserved instead of being coerced to \"123\".\n const mode = getSelectionMode(eventData.event as MouseEvent);\n const newSelection = applySelection(selectedIdsRef.current, new Set([clickedPoint.id]), mode);\n\n if (!isControlledRef.current) {\n setInternalSelectedIds(newSelection);\n }\n onSelectionChangeRef.current?.(newSelection, mode);\n }\n }\n });\n }\n\n // Handle box/lasso selection\n if (enableBoxSelection || enableLassoSelection) {\n plotElement.on(\"plotly_selected\", (eventData: Plotly.PlotSelectionEvent) => {\n if (eventData && eventData.points) {\n const selectedPointIds = eventData.points\n .map((p): string | number | null => {\n if (p.data.ids && p.pointIndex !== undefined) {\n const strId = p.data.ids[p.pointIndex] as string;\n // Restore the original ID type (string or number) via the lookup.\n return originalIdLookup.get(strId) ?? strId;\n }\n return null;\n })\n .filter((id: string | number | null): id is string | number => id !== null);\n\n // Get selection mode from keyboard state\n // Note: Plotly doesn't pass the original event, so we can't detect modifiers\n // For now, box/lasso selection always replaces\n const mode: SelectionMode = \"replace\";\n const newSelection = new Set<string | number>(selectedPointIds);\n\n if (!isControlledRef.current) {\n setInternalSelectedIds(newSelection);\n }\n onSelectionChangeRef.current?.(newSelection, mode);\n }\n });\n\n // Handle deselect (clicking on background)\n plotElement.on(\"plotly_deselect\", () => {\n const newSelection = new Set<string | number>();\n if (!isControlledRef.current) {\n setInternalSelectedIds(newSelection);\n }\n onSelectionChangeRef.current?.(newSelection, \"replace\");\n });\n }\n\n // Design-system tooltip driven by Plotly hover events\n if (tooltipEnabled && !nativeTooltip) {\n bindTooltip(currentRef);\n }\n\n // Capture ref value for cleanup\n drawnElement = currentRef;\n });\n\n return () => {\n cancelled = true;\n if (drawnElement) {\n getLoadedPlotly().purge(drawnElement);\n }\n };\n }, [\n processedData,\n markerConfig,\n xAxis,\n yAxis,\n xRange,\n yRange,\n title,\n width,\n height,\n tooltip,\n tooltipText,\n enableClickSelection,\n enableBoxSelection,\n enableLassoSelection,\n originalIdLookup,\n tooltipEnabled,\n nativeTooltip,\n bindTooltip,\n theme,\n ]);\n\n // Apply selection state to Plotly\n useEffect(() => {\n if (!plotRef.current || processedData.length === 0) return;\n\n // Wait for the lazily-loaded Plotly module: the draw effect registers its\n // loadPlotly().then first, so by the time this callback runs the plot has\n // been drawn — preserving the original \"restyle right after newPlot\"\n // ordering on initial mount.\n let cancelled = false;\n void loadPlotly().then((plotly) => {\n const currentRef = plotRef.current;\n if (cancelled || !currentRef) return;\n // Guard: skip if the chart hasn't been drawn (e.g. the draw was cancelled).\n if (!(currentRef as { _fullLayout?: unknown })._fullLayout) return;\n\n const plotElement = currentRef as unknown as Plotly.PlotlyHTMLElement;\n\n // Find indices of selected points. Use the string-normalized set to match the string IDs Plotly stores\n const selectedIndices = processedData\n .map((point, index) => (normalizedSelectedIds.has(String(point.id)) ? index : null))\n .filter((index): index is number => index !== null);\n\n // Update Plotly selection\n if (selectedIndices.length > 0) {\n plotly.restyle(\n plotElement,\n {\n selectedpoints: [selectedIndices],\n },\n [0],\n );\n } else {\n plotly.restyle(\n plotElement,\n {\n selectedpoints: [null],\n },\n [0],\n );\n }\n });\n\n return () => {\n cancelled = true;\n };\n }, [normalizedSelectedIds, processedData]);\n\n const containerClassName = className\n ? `scatter-plot-interactive relative ${className}`\n : \"scatter-plot-interactive relative\";\n\n return (\n <div className={containerClassName}>\n <div ref={plotRef} className=\"scatter-plot-interactive__plot\" style={{ width, height }} />\n {tooltipElement}\n </div>\n );\n};\n\nexport { ScatterPlotInteractive };\n"],"names":["DEFAULT_AXIS_CONFIG","DEFAULT_TOOLTIP_CONFIG","ScatterPlotInteractive","data","title","xAxis","yAxis","colorMapping","shapeMapping","sizeMapping","tooltip","enableClickSelection","enableBoxSelection","enableLassoSelection","controlledSelectedIds","onSelectionChange","onPointClick","downsampling","width","height","showColorBar","className","plotRef","useRef","theme","usePlotlyTheme","onSelectionChangeRef","onPointClickRef","selectedIdsRef","isControlledRef","internalSelectedIds","setInternalSelectedIds","useState","isControlled","selectedIds","normalizedSelectedIds","useMemo","originalIdLookup","p","processedData","downsampleData","colors","mapColors","shapes","mapShapes","sizes","mapSizes","xRange","calculateAxisRange","PLOT_CONSTANTS","yRange","tooltipEnabled","nativeTooltip","tooltipText","point","generateTooltipContent","bindTooltip","tooltipElement","useChartTooltip","points","index","plotlyColors","value","plotlyColorscale","scale","DEFAULT_COLOR_SCALE","markerConfig","config","range","calculateRange","useEffect","currentRef","xValues","yValues","ids","plotData","COLORS","layout","getPlotlyLayoutConfig","cancelled","drawnElement","loadPlotly","plotly","plotElement","eventData","clickedId","clickedPoint","mode","getSelectionMode","newSelection","applySelection","selectedPointIds","strId","id","getLoadedPlotly","selectedIndices","containerClassName","jsxs","jsx"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"ScatterPlotInteractive.cjs","sources":["../../../../src/components/charts/ScatterPlotInteractive/ScatterPlotInteractive.tsx"],"sourcesContent":["import React, { useEffect, useMemo, useRef, useState } from \"react\";\n\nimport { useChartTooltip } from \"../ChartTooltip\";\nimport { getLoadedPlotly, loadPlotly } from \"../plotly-loader\";\n\nimport { COLORS, DEFAULT_COLOR_SCALE, PLOT_CONSTANTS } from \"./constants\";\nimport {\n applySelection,\n calculateAxisRange,\n calculateRange,\n downsampleData,\n generateTooltipContent,\n getPlotlyLayoutConfig,\n getSelectionMode,\n mapColors,\n mapShapes,\n mapSizes,\n} from \"./utils\";\n\nimport type { AxisConfig, ScatterPlotInteractiveProps, SelectionMode, TooltipConfig } from \"./types\";\nimport type Plotly from \"plotly.js-dist\";\n\nimport { usePlotlyTheme } from \"@/hooks/use-plotly-theme\";\n\n// Stable default prop objects — inline defaults would create a new identity\n// on every render, retriggering the plot effect (and tearing down the\n// tooltip) whenever internal state changes\nconst DEFAULT_AXIS_CONFIG: AxisConfig = {};\nconst DEFAULT_TOOLTIP_CONFIG: TooltipConfig = { enabled: true };\n\n/**\n * ScatterPlotInteractive component for visualizing scatter plot data with advanced interactions.\n *\n * **Features:**\n * - Data-driven and static styling (color, shape, size)\n * - Interactive selection (click, box, lasso)\n * - Keyboard modifiers for click-selection modes (Shift/Ctrl)\n * - Customizable tooltips with rich content support\n * - Axis customization (ranges, log/linear scales)\n * - Performance optimizations for large datasets\n * - Selection propagation via callbacks\n *\n * **Selection Modes (click selection only):**\n * - Default click: Replace selection\n * - Shift + click: Add to selection\n * - Ctrl/Cmd + click: Remove from selection\n * - Shift + Ctrl + click: Toggle selection\n *\n * Box/lasso selection always replaces the current selection because\n * Plotly does not expose the original keyboard event for drag operations.\n */\nconst ScatterPlotInteractive: React.FC<ScatterPlotInteractiveProps> = ({\n data,\n title,\n xAxis = DEFAULT_AXIS_CONFIG,\n yAxis = DEFAULT_AXIS_CONFIG,\n colorMapping,\n shapeMapping,\n sizeMapping,\n tooltip = DEFAULT_TOOLTIP_CONFIG,\n enableClickSelection = true,\n enableBoxSelection = true,\n enableLassoSelection = true,\n selectedIds: controlledSelectedIds,\n onSelectionChange,\n onPointClick,\n downsampling,\n width = 800,\n height = 600,\n showColorBar = true,\n className,\n}) => {\n const plotRef = useRef<HTMLDivElement>(null);\n const theme = usePlotlyTheme();\n const onSelectionChangeRef = useRef(onSelectionChange);\n const onPointClickRef = useRef(onPointClick);\n const selectedIdsRef = useRef<Set<string | number>>(new Set());\n const isControlledRef = useRef(false);\n\n // Keep refs updated\n onSelectionChangeRef.current = onSelectionChange;\n onPointClickRef.current = onPointClick;\n\n // Internal selection state (only used in uncontrolled mode)\n const [internalSelectedIds, setInternalSelectedIds] = useState<Set<string | number>>(new Set());\n\n // Use controlled or uncontrolled selection\n const isControlled = controlledSelectedIds !== undefined;\n const selectedIds = isControlled ? controlledSelectedIds : internalSelectedIds;\n selectedIdsRef.current = selectedIds;\n isControlledRef.current = isControlled;\n\n // Normalize to strings so numeric IDs passed by consumers always match the\n // string IDs that Plotly stores (we always do String(p.id) when building the trace).\n const normalizedSelectedIds = useMemo(() => new Set([...selectedIds].map(String)), [selectedIds]);\n\n // Reverse-lookup: Plotly string ID → original (possibly numeric) ScatterPoint ID.\n // Used to restore original ID types when echoing selections back to consumers.\n const originalIdLookup = useMemo(() => new Map(data.map((p) => [String(p.id), p.id])), [data]);\n\n // Apply downsampling if configured\n const processedData = useMemo(() => {\n if (!downsampling) {\n return data;\n }\n return downsampleData(data, downsampling);\n }, [data, downsampling]);\n\n // Map colors, shapes, and sizes based on configuration\n const colors = useMemo(() => mapColors(processedData, colorMapping), [processedData, colorMapping]);\n\n const shapes = useMemo(() => mapShapes(processedData, shapeMapping), [processedData, shapeMapping]);\n\n const sizes = useMemo(() => mapSizes(processedData, sizeMapping), [processedData, sizeMapping]);\n\n // Calculate axis ranges\n const xRange = useMemo(() => {\n if (xAxis.range) return xAxis.range;\n if (xAxis.autoRange === false) return;\n return calculateAxisRange(processedData, \"x\", xAxis.autoRangePadding ?? PLOT_CONSTANTS.AUTO_RANGE_PADDING, xAxis.scale);\n }, [processedData, xAxis]);\n\n const yRange = useMemo(() => {\n if (yAxis.range) return yAxis.range;\n if (yAxis.autoRange === false) return;\n return calculateAxisRange(processedData, \"y\", yAxis.autoRangePadding ?? PLOT_CONSTANTS.AUTO_RANGE_PADDING, yAxis.scale);\n }, [processedData, yAxis]);\n\n const tooltipEnabled = tooltip.enabled !== false;\n const nativeTooltip = tooltip.native === true;\n\n // Build tooltip text\n const tooltipText = useMemo(() => {\n if (!tooltipEnabled) return [];\n\n return processedData.map((point) =>\n tooltip.content ? tooltip.content(point) : generateTooltipContent(point, tooltip.fields),\n );\n }, [processedData, tooltip, tooltipEnabled]);\n\n // Design-system tooltip driven by Plotly hover events\n const { bindTooltip, tooltipElement } = useChartTooltip({\n getLines: (points) => {\n const index = points[0]?.pointIndex;\n if (index === undefined) return [];\n return (tooltipText[index] ?? \"\").split(\"<br>\").filter(Boolean);\n },\n });\n\n // Prepare Plotly-compatible color data\n const plotlyColors = useMemo(() => {\n if (colorMapping?.type === \"continuous\" && colorMapping.field && processedData.length > 0) {\n // For continuous mapping, extract the numeric values\n return processedData.map((point) => {\n const value = point.metadata?.[colorMapping.field!];\n return typeof value === \"number\" && Number.isFinite(value) ? value : 0;\n });\n }\n return colors;\n }, [processedData, colorMapping, colors]);\n\n // Build colorscale for continuous mapping\n const plotlyColorscale = useMemo(() => {\n if (colorMapping?.type === \"continuous\") {\n const scale = colorMapping.colorScale || DEFAULT_COLOR_SCALE;\n return typeof scale === \"string\" ? scale : scale;\n }\n }, [colorMapping]);\n\n // Build marker configuration for Plotly\n const markerConfig = useMemo((): Partial<Plotly.PlotMarker> => {\n const config: Partial<Plotly.PlotMarker> = {\n size: sizes,\n symbol: shapes,\n // Points carry no outline — a flat fill (SW-2298 dropped the earlier CSS\n // drop-shadow). Selected points re-add a line below.\n line: {\n width: 0,\n },\n };\n\n // Configure color mapping\n if (colorMapping?.type === \"continuous\" && plotlyColorscale) {\n config.color = plotlyColors as number[];\n config.colorscale = plotlyColorscale;\n config.showscale = showColorBar;\n\n if (colorMapping.field) {\n const range =\n colorMapping.min !== undefined && colorMapping.max !== undefined\n ? { min: colorMapping.min, max: colorMapping.max }\n : calculateRange(processedData, colorMapping.field);\n config.cmin = range.min;\n config.cmax = range.max;\n config.colorbar = {\n title: { text: colorMapping.field, side: \"right\" },\n thickness: 20,\n len: 0.7,\n };\n }\n } else {\n config.color = colors;\n }\n\n return config;\n }, [sizes, shapes, colorMapping, plotlyColorscale, plotlyColors, showColorBar, processedData, colors]);\n\n // Create Plotly plot\n useEffect(() => {\n const currentRef = plotRef.current;\n if (!currentRef || processedData.length === 0) return;\n\n const xValues = processedData.map((p) => p.x);\n const yValues = processedData.map((p) => p.y);\n const ids = processedData.map((p) => String(p.id));\n\n // Create base trace\n const trace: Partial<Plotly.PlotData> & {\n unselected?: { marker?: { opacity?: number } };\n selected?: { marker?: { opacity?: number; line?: { color?: string; width?: number } } };\n } = {\n x: xValues,\n y: yValues,\n ids,\n mode: \"markers\",\n type: \"scatter\",\n marker: markerConfig,\n // \"none\" keeps hover events firing for the HTML tooltip without\n // showing Plotly's hover label; \"skip\" disables hover entirely\n hoverinfo: tooltipEnabled ? (nativeTooltip ? \"text\" : \"none\") : \"skip\",\n text: tooltipText,\n hovertemplate: tooltipEnabled && nativeTooltip ? \"%{text}<extra></extra>\" : undefined,\n showlegend: false,\n unselected: {\n marker: {\n opacity: 0.3,\n },\n },\n selected: {\n marker: {\n opacity: 1,\n line: {\n color: COLORS.selected,\n width: 2,\n },\n },\n },\n };\n\n const plotData: Plotly.Data[] = [trace as Plotly.Data];\n\n // Configure layout\n const layout: Partial<Plotly.Layout> = getPlotlyLayoutConfig({\n title,\n xAxis,\n yAxis,\n width,\n height,\n xRange,\n yRange,\n enableLassoSelection,\n enableBoxSelection,\n theme,\n });\n\n const config: Partial<Plotly.Config> = {\n responsive: true,\n displayModeBar: true,\n displaylogo: false,\n modeBarButtonsToAdd: [],\n modeBarButtonsToRemove: [\"toImage\"],\n };\n\n // Add selection mode buttons if enabled\n if (enableBoxSelection) {\n config.modeBarButtonsToAdd?.push(\"select2d\" as never);\n }\n if (enableLassoSelection) {\n config.modeBarButtonsToAdd?.push(\"lasso2d\" as never);\n }\n\n // Plotly is loaded lazily so it stays out of the consumer's main chunk\n // (SW-2007); the draw is skipped if the effect re-runs or unmounts first.\n let cancelled = false;\n let drawnElement: HTMLDivElement | null = null;\n void loadPlotly().then((plotly) => {\n if (cancelled || !plotRef.current) return;\n plotly.newPlot(currentRef, plotData, layout, config);\n\n // Attach event handlers\n const plotElement = currentRef as unknown as Plotly.PlotlyHTMLElement;\n\n // Handle point click\n if (enableClickSelection) {\n plotElement.on(\"plotly_click\", (eventData: Plotly.PlotMouseEvent) => {\n const point = eventData.points[0];\n if (point && point.data.ids) {\n const clickedId = point.data.ids[point.pointIndex];\n const clickedPoint = processedData.find((p) => String(p.id) === clickedId);\n\n if (clickedPoint) {\n // Call point click handler if provided\n onPointClickRef.current?.(clickedPoint, eventData.event as MouseEvent);\n\n // Handle selection. Use the original-typed ID from the matched point so\n // numeric IDs (e.g., 123) are preserved instead of being coerced to \"123\".\n const mode = getSelectionMode(eventData.event as MouseEvent);\n const newSelection = applySelection(selectedIdsRef.current, new Set([clickedPoint.id]), mode);\n\n if (!isControlledRef.current) {\n setInternalSelectedIds(newSelection);\n }\n onSelectionChangeRef.current?.(newSelection, mode);\n }\n }\n });\n }\n\n // Handle box/lasso selection\n if (enableBoxSelection || enableLassoSelection) {\n plotElement.on(\"plotly_selected\", (eventData: Plotly.PlotSelectionEvent) => {\n if (eventData && eventData.points) {\n const selectedPointIds = eventData.points\n .map((p): string | number | null => {\n if (p.data.ids && p.pointIndex !== undefined) {\n const strId = p.data.ids[p.pointIndex] as string;\n // Restore the original ID type (string or number) via the lookup.\n return originalIdLookup.get(strId) ?? strId;\n }\n return null;\n })\n .filter((id: string | number | null): id is string | number => id !== null);\n\n // Get selection mode from keyboard state\n // Note: Plotly doesn't pass the original event, so we can't detect modifiers\n // For now, box/lasso selection always replaces\n const mode: SelectionMode = \"replace\";\n const newSelection = new Set<string | number>(selectedPointIds);\n\n if (!isControlledRef.current) {\n setInternalSelectedIds(newSelection);\n }\n onSelectionChangeRef.current?.(newSelection, mode);\n }\n });\n\n // Handle deselect (clicking on background)\n plotElement.on(\"plotly_deselect\", () => {\n const newSelection = new Set<string | number>();\n if (!isControlledRef.current) {\n setInternalSelectedIds(newSelection);\n }\n onSelectionChangeRef.current?.(newSelection, \"replace\");\n });\n }\n\n // Design-system tooltip driven by Plotly hover events\n if (tooltipEnabled && !nativeTooltip) {\n bindTooltip(currentRef);\n }\n\n // Capture ref value for cleanup\n drawnElement = currentRef;\n });\n\n return () => {\n cancelled = true;\n if (drawnElement) {\n getLoadedPlotly().purge(drawnElement);\n }\n };\n }, [\n processedData,\n markerConfig,\n xAxis,\n yAxis,\n xRange,\n yRange,\n title,\n width,\n height,\n tooltip,\n tooltipText,\n enableClickSelection,\n enableBoxSelection,\n enableLassoSelection,\n originalIdLookup,\n tooltipEnabled,\n nativeTooltip,\n bindTooltip,\n theme,\n ]);\n\n // Apply selection state to Plotly\n useEffect(() => {\n if (!plotRef.current || processedData.length === 0) return;\n\n // Wait for the lazily-loaded Plotly module: the draw effect registers its\n // loadPlotly().then first, so by the time this callback runs the plot has\n // been drawn — preserving the original \"restyle right after newPlot\"\n // ordering on initial mount.\n let cancelled = false;\n void loadPlotly().then((plotly) => {\n const currentRef = plotRef.current;\n if (cancelled || !currentRef) return;\n // Guard: skip if the chart hasn't been drawn (e.g. the draw was cancelled).\n if (!(currentRef as { _fullLayout?: unknown })._fullLayout) return;\n\n const plotElement = currentRef as unknown as Plotly.PlotlyHTMLElement;\n\n // Find indices of selected points. Use the string-normalized set to match the string IDs Plotly stores\n const selectedIndices = processedData\n .map((point, index) => (normalizedSelectedIds.has(String(point.id)) ? index : null))\n .filter((index): index is number => index !== null);\n\n // Update Plotly selection\n if (selectedIndices.length > 0) {\n plotly.restyle(\n plotElement,\n {\n selectedpoints: [selectedIndices],\n },\n [0],\n );\n } else {\n plotly.restyle(\n plotElement,\n {\n selectedpoints: [null],\n },\n [0],\n );\n }\n });\n\n return () => {\n cancelled = true;\n };\n }, [normalizedSelectedIds, processedData]);\n\n const containerClassName = className\n ? `scatter-plot-interactive relative ${className}`\n : \"scatter-plot-interactive relative\";\n\n return (\n <div className={containerClassName}>\n <div ref={plotRef} className=\"scatter-plot-interactive__plot\" style={{ width, height }} />\n {tooltipElement}\n </div>\n );\n};\n\nexport { ScatterPlotInteractive };\n"],"names":["DEFAULT_AXIS_CONFIG","DEFAULT_TOOLTIP_CONFIG","ScatterPlotInteractive","data","title","xAxis","yAxis","colorMapping","shapeMapping","sizeMapping","tooltip","enableClickSelection","enableBoxSelection","enableLassoSelection","controlledSelectedIds","onSelectionChange","onPointClick","downsampling","width","height","showColorBar","className","plotRef","useRef","theme","usePlotlyTheme","onSelectionChangeRef","onPointClickRef","selectedIdsRef","isControlledRef","internalSelectedIds","setInternalSelectedIds","useState","isControlled","selectedIds","normalizedSelectedIds","useMemo","originalIdLookup","p","processedData","downsampleData","colors","mapColors","shapes","mapShapes","sizes","mapSizes","xRange","calculateAxisRange","PLOT_CONSTANTS","yRange","tooltipEnabled","nativeTooltip","tooltipText","point","generateTooltipContent","bindTooltip","tooltipElement","useChartTooltip","points","index","plotlyColors","value","plotlyColorscale","scale","DEFAULT_COLOR_SCALE","markerConfig","config","range","calculateRange","useEffect","currentRef","xValues","yValues","ids","plotData","COLORS","layout","getPlotlyLayoutConfig","cancelled","drawnElement","loadPlotly","plotly","plotElement","eventData","clickedId","clickedPoint","mode","getSelectionMode","newSelection","applySelection","selectedPointIds","strId","id","getLoadedPlotly","selectedIndices","containerClassName","jsxs","jsx"],"mappings":"kUA2BMA,GAAkC,CAAA,EAClCC,GAAwC,CAAE,QAAS,EAAA,EAuBnDC,GAAgE,CAAC,CACrE,KAAAC,EACA,MAAAC,EACA,MAAAC,EAAQL,GACR,MAAAM,EAAQN,GACR,aAAAO,EACA,aAAAC,EACA,YAAAC,EACA,QAAAC,EAAUT,GACV,qBAAAU,EAAuB,GACvB,mBAAAC,EAAqB,GACrB,qBAAAC,EAAuB,GACvB,YAAaC,EACb,kBAAAC,EACA,aAAAC,EACA,aAAAC,EACA,MAAAC,EAAQ,IACR,OAAAC,EAAS,IACT,aAAAC,EAAe,GACf,UAAAC,CACF,IAAM,CACJ,MAAMC,EAAUC,EAAAA,OAAuB,IAAI,EACrCC,EAAQC,GAAAA,eAAA,EACRC,EAAuBH,EAAAA,OAAOR,CAAiB,EAC/CY,EAAkBJ,EAAAA,OAAOP,CAAY,EACrCY,EAAiBL,EAAAA,OAA6B,IAAI,GAAK,EACvDM,EAAkBN,EAAAA,OAAO,EAAK,EAGpCG,EAAqB,QAAUX,EAC/BY,EAAgB,QAAUX,EAG1B,KAAM,CAACc,GAAqBC,CAAsB,EAAIC,EAAAA,SAA+B,IAAI,GAAK,EAGxFC,GAAenB,IAA0B,OACzCoB,EAAcD,GAAenB,EAAwBgB,GAC3DF,EAAe,QAAUM,EACzBL,EAAgB,QAAUI,GAI1B,MAAME,GAAwBC,EAAAA,QAAQ,IAAM,IAAI,IAAI,CAAC,GAAGF,CAAW,EAAE,IAAI,MAAM,CAAC,EAAG,CAACA,CAAW,CAAC,EAI1FG,GAAmBD,EAAAA,QAAQ,IAAM,IAAI,IAAIjC,EAAK,IAAKmC,GAAM,CAAC,OAAOA,EAAE,EAAE,EAAGA,EAAE,EAAE,CAAC,CAAC,EAAG,CAACnC,CAAI,CAAC,EAGvFoC,EAAgBH,EAAAA,QAAQ,IACvBnB,EAGEuB,EAAAA,eAAerC,EAAMc,CAAY,EAF/Bd,EAGR,CAACA,EAAMc,CAAY,CAAC,EAGjBwB,EAASL,UAAQ,IAAMM,YAAUH,EAAehC,CAAY,EAAG,CAACgC,EAAehC,CAAY,CAAC,EAE5FoC,GAASP,UAAQ,IAAMQ,YAAUL,EAAe/B,CAAY,EAAG,CAAC+B,EAAe/B,CAAY,CAAC,EAE5FqC,GAAQT,UAAQ,IAAMU,WAASP,EAAe9B,CAAW,EAAG,CAAC8B,EAAe9B,CAAW,CAAC,EAGxFsC,GAASX,EAAAA,QAAQ,IAAM,CAC3B,GAAI/B,EAAM,MAAO,OAAOA,EAAM,MAC9B,GAAIA,EAAM,YAAc,GACxB,OAAO2C,EAAAA,mBAAmBT,EAAe,IAAKlC,EAAM,kBAAoB4C,iBAAe,mBAAoB5C,EAAM,KAAK,CACxH,EAAG,CAACkC,EAAelC,CAAK,CAAC,EAEnB6C,GAASd,EAAAA,QAAQ,IAAM,CAC3B,GAAI9B,EAAM,MAAO,OAAOA,EAAM,MAC9B,GAAIA,EAAM,YAAc,GACxB,OAAO0C,EAAAA,mBAAmBT,EAAe,IAAKjC,EAAM,kBAAoB2C,iBAAe,mBAAoB3C,EAAM,KAAK,CACxH,EAAG,CAACiC,EAAejC,CAAK,CAAC,EAEnB6C,EAAiBzC,EAAQ,UAAY,GACrC0C,EAAgB1C,EAAQ,SAAW,GAGnC2C,EAAcjB,EAAAA,QAAQ,IACrBe,EAEEZ,EAAc,IAAKe,GACxB5C,EAAQ,QAAUA,EAAQ,QAAQ4C,CAAK,EAAIC,EAAAA,uBAAuBD,EAAO5C,EAAQ,MAAM,CAAA,EAH7D,CAAA,EAK3B,CAAC6B,EAAe7B,EAASyC,CAAc,CAAC,EAGrC,CAAE,YAAAK,GAAa,eAAAC,EAAA,EAAmBC,mBAAgB,CACtD,SAAWC,GAAW,CACpB,MAAMC,EAAQD,EAAO,CAAC,GAAG,WACzB,OAAIC,IAAU,OAAkB,CAAA,GACxBP,EAAYO,CAAK,GAAK,IAAI,MAAM,MAAM,EAAE,OAAO,OAAO,CAChE,CAAA,CACD,EAGKC,GAAezB,EAAAA,QAAQ,IACvB7B,GAAc,OAAS,cAAgBA,EAAa,OAASgC,EAAc,OAAS,EAE/EA,EAAc,IAAKe,GAAU,CAClC,MAAMQ,EAAQR,EAAM,WAAW/C,EAAa,KAAM,EAClD,OAAO,OAAOuD,GAAU,UAAY,OAAO,SAASA,CAAK,EAAIA,EAAQ,CACvE,CAAC,EAEIrB,EACN,CAACF,EAAehC,EAAckC,CAAM,CAAC,EAGlCsB,EAAmB3B,EAAAA,QAAQ,IAAM,CACrC,GAAI7B,GAAc,OAAS,aAAc,CACvC,MAAMyD,EAAQzD,EAAa,YAAc0D,EAAAA,oBACzC,OAAmCD,CACrC,CACF,EAAG,CAACzD,CAAY,CAAC,EAGX2D,GAAe9B,EAAAA,QAAQ,IAAkC,CAC7D,MAAM+B,EAAqC,CACzC,KAAMtB,GACN,OAAQF,GAGR,KAAM,CACJ,MAAO,CAAA,CACT,EAIF,GAAIpC,GAAc,OAAS,cAAgBwD,GAKzC,GAJAI,EAAO,MAAQN,GACfM,EAAO,WAAaJ,EACpBI,EAAO,UAAY/C,EAEfb,EAAa,MAAO,CACtB,MAAM6D,EACJ7D,EAAa,MAAQ,QAAaA,EAAa,MAAQ,OACnD,CAAE,IAAKA,EAAa,IAAK,IAAKA,EAAa,GAAA,EAC3C8D,EAAAA,eAAe9B,EAAehC,EAAa,KAAK,EACtD4D,EAAO,KAAOC,EAAM,IACpBD,EAAO,KAAOC,EAAM,IACpBD,EAAO,SAAW,CAChB,MAAO,CAAE,KAAM5D,EAAa,MAAO,KAAM,OAAA,EACzC,UAAW,GACX,IAAK,EAAA,CAET,OAEA4D,EAAO,MAAQ1B,EAGjB,OAAO0B,CACT,EAAG,CAACtB,GAAOF,GAAQpC,EAAcwD,EAAkBF,GAAczC,EAAcmB,EAAeE,CAAM,CAAC,EAGrG6B,EAAAA,UAAU,IAAM,CACd,MAAMC,EAAajD,EAAQ,QAC3B,GAAI,CAACiD,GAAchC,EAAc,SAAW,EAAG,OAE/C,MAAMiC,EAAUjC,EAAc,IAAKD,GAAMA,EAAE,CAAC,EACtCmC,EAAUlC,EAAc,IAAKD,GAAMA,EAAE,CAAC,EACtCoC,EAAMnC,EAAc,IAAKD,GAAM,OAAOA,EAAE,EAAE,CAAC,EAmC3CqC,EAA0B,CA7B5B,CACF,EAAGH,EACH,EAAGC,EACH,IAAAC,EACA,KAAM,UACN,KAAM,UACN,OAAQR,GAGR,UAAWf,EAAkBC,EAAgB,OAAS,OAAU,OAChE,KAAMC,EACN,cAAeF,GAAkBC,EAAgB,yBAA2B,OAC5E,WAAY,GACZ,WAAY,CACV,OAAQ,CACN,QAAS,EAAA,CACX,EAEF,SAAU,CACR,OAAQ,CACN,QAAS,EACT,KAAM,CACJ,MAAOwB,EAAAA,OAAO,SACd,MAAO,CAAA,CACT,CACF,CACF,CAGmD,EAG/CC,EAAiCC,EAAAA,sBAAsB,CAC3D,MAAA1E,EACA,MAAAC,EACA,MAAAC,EACA,MAAAY,EACA,OAAAC,EACA,OAAA4B,GACA,OAAAG,GACA,qBAAArC,EACA,mBAAAD,EACA,MAAAY,CAAA,CACD,EAEK2C,EAAiC,CACrC,WAAY,GACZ,eAAgB,GAChB,YAAa,GACb,oBAAqB,CAAA,EACrB,uBAAwB,CAAC,SAAS,CAAA,EAIhCvD,GACFuD,EAAO,qBAAqB,KAAK,UAAmB,EAElDtD,GACFsD,EAAO,qBAAqB,KAAK,SAAkB,EAKrD,IAAIY,GAAY,GACZC,EAAsC,KAC1C,OAAKC,aAAA,EAAa,KAAMC,GAAW,CACjC,GAAIH,IAAa,CAACzD,EAAQ,QAAS,OACnC4D,EAAO,QAAQX,EAAYI,EAAUE,EAAQV,CAAM,EAGnD,MAAMgB,EAAcZ,EAGhB5D,GACFwE,EAAY,GAAG,eAAiBC,GAAqC,CACnE,MAAM9B,EAAQ8B,EAAU,OAAO,CAAC,EAChC,GAAI9B,GAASA,EAAM,KAAK,IAAK,CAC3B,MAAM+B,EAAY/B,EAAM,KAAK,IAAIA,EAAM,UAAU,EAC3CgC,EAAe/C,EAAc,KAAMD,GAAM,OAAOA,EAAE,EAAE,IAAM+C,CAAS,EAEzE,GAAIC,EAAc,CAEhB3D,EAAgB,UAAU2D,EAAcF,EAAU,KAAmB,EAIrE,MAAMG,EAAOC,EAAAA,iBAAiBJ,EAAU,KAAmB,EACrDK,EAAeC,EAAAA,eAAe9D,EAAe,QAAS,IAAI,IAAI,CAAC0D,EAAa,EAAE,CAAC,EAAGC,CAAI,EAEvF1D,EAAgB,SACnBE,EAAuB0D,CAAY,EAErC/D,EAAqB,UAAU+D,EAAcF,CAAI,CACnD,CACF,CACF,CAAC,GAIC3E,GAAsBC,KACxBsE,EAAY,GAAG,kBAAoBC,GAAyC,CAC1E,GAAIA,GAAaA,EAAU,OAAQ,CACjC,MAAMO,EAAmBP,EAAU,OAChC,IAAK9C,GAA8B,CAClC,GAAIA,EAAE,KAAK,KAAOA,EAAE,aAAe,OAAW,CAC5C,MAAMsD,EAAQtD,EAAE,KAAK,IAAIA,EAAE,UAAU,EAErC,OAAOD,GAAiB,IAAIuD,CAAK,GAAKA,CACxC,CACA,OAAO,IACT,CAAC,EACA,OAAQC,GAAsDA,IAAO,IAAI,EAKtEN,EAAsB,UACtBE,EAAe,IAAI,IAAqBE,CAAgB,EAEzD9D,EAAgB,SACnBE,EAAuB0D,CAAY,EAErC/D,EAAqB,UAAU+D,EAAcF,CAAI,CACnD,CACF,CAAC,EAGDJ,EAAY,GAAG,kBAAmB,IAAM,CACtC,MAAMM,MAAmB,IACpB5D,EAAgB,SACnBE,EAAuB0D,CAAY,EAErC/D,EAAqB,UAAU+D,EAAc,SAAS,CACxD,CAAC,GAICtC,GAAkB,CAACC,GACrBI,GAAYe,CAAU,EAIxBS,EAAeT,CACjB,CAAC,EAEM,IAAM,CACXQ,GAAY,GACRC,GACFc,kBAAA,EAAkB,MAAMd,CAAY,CAExC,CACF,EAAG,CACDzC,EACA2B,GACA7D,EACAC,EACAyC,GACAG,GACA9C,EACAc,EACAC,EACAT,EACA2C,EACA1C,EACAC,EACAC,EACAwB,GACAc,EACAC,EACAI,GACAhC,CAAA,CACD,EAGD8C,EAAAA,UAAU,IAAM,CACd,GAAI,CAAChD,EAAQ,SAAWiB,EAAc,SAAW,EAAG,OAMpD,IAAIwC,EAAY,GAChB,OAAKE,aAAA,EAAa,KAAMC,GAAW,CACjC,MAAMX,EAAajD,EAAQ,QAG3B,GAFIyD,GAAa,CAACR,GAEd,CAAEA,EAAyC,YAAa,OAE5D,MAAMY,EAAcZ,EAGdwB,EAAkBxD,EACrB,IAAI,CAACe,EAAOM,IAAWzB,GAAsB,IAAI,OAAOmB,EAAM,EAAE,CAAC,EAAIM,EAAQ,IAAK,EAClF,OAAQA,GAA2BA,IAAU,IAAI,EAGhDmC,EAAgB,OAAS,EAC3Bb,EAAO,QACLC,EACA,CACE,eAAgB,CAACY,CAAe,CAAA,EAElC,CAAC,CAAC,CAAA,EAGJb,EAAO,QACLC,EACA,CACE,eAAgB,CAAC,IAAI,CAAA,EAEvB,CAAC,CAAC,CAAA,CAGR,CAAC,EAEM,IAAM,CACXJ,EAAY,EACd,CACF,EAAG,CAAC5C,GAAuBI,CAAa,CAAC,EAEzC,MAAMyD,GAAqB3E,EACvB,qCAAqCA,CAAS,GAC9C,oCAEJ,OACE4E,GAAAA,KAAC,MAAA,CAAI,UAAWD,GACd,SAAA,CAAAE,GAAAA,IAAC,MAAA,CAAI,IAAK5E,EAAS,UAAU,iCAAiC,MAAO,CAAE,MAAAJ,EAAO,OAAAC,CAAA,CAAO,CAAG,EACvFsC,EAAA,EACH,CAEJ"}
|
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
import { jsxs as ve, jsx as Re } from "react/jsx-runtime";
|
|
2
2
|
import { useRef as S, useState as Se, useMemo as o, useEffect as de } from "react";
|
|
3
|
-
/* empty css */
|
|
4
3
|
import { useChartTooltip as Te } from "../ChartTooltip/ChartTooltip.js";
|
|
5
4
|
import { loadPlotly as ue, getLoadedPlotly as ge } from "../plotly-loader.js";
|
|
6
5
|
import { PLOT_CONSTANTS as ae, DEFAULT_COLOR_SCALE as _e, COLORS as he } from "./constants.js";
|
|
7
6
|
import { downsampleData as Ce, mapColors as Pe, mapShapes as we, mapSizes as Ee, calculateAxisRange as fe, generateTooltipContent as Oe, calculateRange as Ne, getPlotlyLayoutConfig as ke, getSelectionMode as Le, applySelection as Be } from "./utils.js";
|
|
8
7
|
import { usePlotlyTheme as De } from "../../../hooks/use-plotly-theme.js";
|
|
9
|
-
const me = {}, Fe = { enabled: !0 },
|
|
8
|
+
const me = {}, Fe = { enabled: !0 }, Ve = ({
|
|
10
9
|
data: p,
|
|
11
10
|
title: x,
|
|
12
11
|
xAxis: l = me,
|
|
@@ -58,8 +57,8 @@ const me = {}, Fe = { enabled: !0 }, Xe = ({
|
|
|
58
57
|
const e = {
|
|
59
58
|
size: ne,
|
|
60
59
|
symbol: te,
|
|
61
|
-
// Points carry no outline
|
|
62
|
-
//
|
|
60
|
+
// Points carry no outline — a flat fill (SW-2298 dropped the earlier CSS
|
|
61
|
+
// drop-shadow). Selected points re-add a line below.
|
|
63
62
|
line: {
|
|
64
63
|
width: 0
|
|
65
64
|
}
|
|
@@ -210,6 +209,6 @@ const me = {}, Fe = { enabled: !0 }, Xe = ({
|
|
|
210
209
|
] });
|
|
211
210
|
};
|
|
212
211
|
export {
|
|
213
|
-
|
|
212
|
+
Ve as ScatterPlotInteractive
|
|
214
213
|
};
|
|
215
214
|
//# sourceMappingURL=ScatterPlotInteractive.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ScatterPlotInteractive.js","sources":["../../../../src/components/charts/ScatterPlotInteractive/ScatterPlotInteractive.tsx"],"sourcesContent":["import React, { useEffect, useMemo, useRef, useState } from \"react\";\n\nimport \"./ScatterPlotInteractive.scss\";\nimport { useChartTooltip } from \"../ChartTooltip\";\nimport { getLoadedPlotly, loadPlotly } from \"../plotly-loader\";\n\nimport { COLORS, DEFAULT_COLOR_SCALE, PLOT_CONSTANTS } from \"./constants\";\nimport {\n applySelection,\n calculateAxisRange,\n calculateRange,\n downsampleData,\n generateTooltipContent,\n getPlotlyLayoutConfig,\n getSelectionMode,\n mapColors,\n mapShapes,\n mapSizes,\n} from \"./utils\";\n\nimport type { AxisConfig, ScatterPlotInteractiveProps, SelectionMode, TooltipConfig } from \"./types\";\nimport type Plotly from \"plotly.js-dist\";\n\nimport { usePlotlyTheme } from \"@/hooks/use-plotly-theme\";\n\n// Stable default prop objects — inline defaults would create a new identity\n// on every render, retriggering the plot effect (and tearing down the\n// tooltip) whenever internal state changes\nconst DEFAULT_AXIS_CONFIG: AxisConfig = {};\nconst DEFAULT_TOOLTIP_CONFIG: TooltipConfig = { enabled: true };\n\n/**\n * ScatterPlotInteractive component for visualizing scatter plot data with advanced interactions.\n *\n * **Features:**\n * - Data-driven and static styling (color, shape, size)\n * - Interactive selection (click, box, lasso)\n * - Keyboard modifiers for click-selection modes (Shift/Ctrl)\n * - Customizable tooltips with rich content support\n * - Axis customization (ranges, log/linear scales)\n * - Performance optimizations for large datasets\n * - Selection propagation via callbacks\n *\n * **Selection Modes (click selection only):**\n * - Default click: Replace selection\n * - Shift + click: Add to selection\n * - Ctrl/Cmd + click: Remove from selection\n * - Shift + Ctrl + click: Toggle selection\n *\n * Box/lasso selection always replaces the current selection because\n * Plotly does not expose the original keyboard event for drag operations.\n */\nconst ScatterPlotInteractive: React.FC<ScatterPlotInteractiveProps> = ({\n data,\n title,\n xAxis = DEFAULT_AXIS_CONFIG,\n yAxis = DEFAULT_AXIS_CONFIG,\n colorMapping,\n shapeMapping,\n sizeMapping,\n tooltip = DEFAULT_TOOLTIP_CONFIG,\n enableClickSelection = true,\n enableBoxSelection = true,\n enableLassoSelection = true,\n selectedIds: controlledSelectedIds,\n onSelectionChange,\n onPointClick,\n downsampling,\n width = 800,\n height = 600,\n showColorBar = true,\n className,\n}) => {\n const plotRef = useRef<HTMLDivElement>(null);\n const theme = usePlotlyTheme();\n const onSelectionChangeRef = useRef(onSelectionChange);\n const onPointClickRef = useRef(onPointClick);\n const selectedIdsRef = useRef<Set<string | number>>(new Set());\n const isControlledRef = useRef(false);\n\n // Keep refs updated\n onSelectionChangeRef.current = onSelectionChange;\n onPointClickRef.current = onPointClick;\n\n // Internal selection state (only used in uncontrolled mode)\n const [internalSelectedIds, setInternalSelectedIds] = useState<Set<string | number>>(new Set());\n\n // Use controlled or uncontrolled selection\n const isControlled = controlledSelectedIds !== undefined;\n const selectedIds = isControlled ? controlledSelectedIds : internalSelectedIds;\n selectedIdsRef.current = selectedIds;\n isControlledRef.current = isControlled;\n\n // Normalize to strings so numeric IDs passed by consumers always match the\n // string IDs that Plotly stores (we always do String(p.id) when building the trace).\n const normalizedSelectedIds = useMemo(() => new Set([...selectedIds].map(String)), [selectedIds]);\n\n // Reverse-lookup: Plotly string ID → original (possibly numeric) ScatterPoint ID.\n // Used to restore original ID types when echoing selections back to consumers.\n const originalIdLookup = useMemo(() => new Map(data.map((p) => [String(p.id), p.id])), [data]);\n\n // Apply downsampling if configured\n const processedData = useMemo(() => {\n if (!downsampling) {\n return data;\n }\n return downsampleData(data, downsampling);\n }, [data, downsampling]);\n\n // Map colors, shapes, and sizes based on configuration\n const colors = useMemo(() => mapColors(processedData, colorMapping), [processedData, colorMapping]);\n\n const shapes = useMemo(() => mapShapes(processedData, shapeMapping), [processedData, shapeMapping]);\n\n const sizes = useMemo(() => mapSizes(processedData, sizeMapping), [processedData, sizeMapping]);\n\n // Calculate axis ranges\n const xRange = useMemo(() => {\n if (xAxis.range) return xAxis.range;\n if (xAxis.autoRange === false) return;\n return calculateAxisRange(processedData, \"x\", xAxis.autoRangePadding ?? PLOT_CONSTANTS.AUTO_RANGE_PADDING, xAxis.scale);\n }, [processedData, xAxis]);\n\n const yRange = useMemo(() => {\n if (yAxis.range) return yAxis.range;\n if (yAxis.autoRange === false) return;\n return calculateAxisRange(processedData, \"y\", yAxis.autoRangePadding ?? PLOT_CONSTANTS.AUTO_RANGE_PADDING, yAxis.scale);\n }, [processedData, yAxis]);\n\n const tooltipEnabled = tooltip.enabled !== false;\n const nativeTooltip = tooltip.native === true;\n\n // Build tooltip text\n const tooltipText = useMemo(() => {\n if (!tooltipEnabled) return [];\n\n return processedData.map((point) =>\n tooltip.content ? tooltip.content(point) : generateTooltipContent(point, tooltip.fields),\n );\n }, [processedData, tooltip, tooltipEnabled]);\n\n // Design-system tooltip driven by Plotly hover events\n const { bindTooltip, tooltipElement } = useChartTooltip({\n getLines: (points) => {\n const index = points[0]?.pointIndex;\n if (index === undefined) return [];\n return (tooltipText[index] ?? \"\").split(\"<br>\").filter(Boolean);\n },\n });\n\n // Prepare Plotly-compatible color data\n const plotlyColors = useMemo(() => {\n if (colorMapping?.type === \"continuous\" && colorMapping.field && processedData.length > 0) {\n // For continuous mapping, extract the numeric values\n return processedData.map((point) => {\n const value = point.metadata?.[colorMapping.field!];\n return typeof value === \"number\" && Number.isFinite(value) ? value : 0;\n });\n }\n return colors;\n }, [processedData, colorMapping, colors]);\n\n // Build colorscale for continuous mapping\n const plotlyColorscale = useMemo(() => {\n if (colorMapping?.type === \"continuous\") {\n const scale = colorMapping.colorScale || DEFAULT_COLOR_SCALE;\n return typeof scale === \"string\" ? scale : scale;\n }\n }, [colorMapping]);\n\n // Build marker configuration for Plotly\n const markerConfig = useMemo((): Partial<Plotly.PlotMarker> => {\n const config: Partial<Plotly.PlotMarker> = {\n size: sizes,\n symbol: shapes,\n // Points carry no outline; depth comes from a CSS drop-shadow\n // (see ScatterPlotInteractive.scss). Selected points re-add a line below.\n line: {\n width: 0,\n },\n };\n\n // Configure color mapping\n if (colorMapping?.type === \"continuous\" && plotlyColorscale) {\n config.color = plotlyColors as number[];\n config.colorscale = plotlyColorscale;\n config.showscale = showColorBar;\n\n if (colorMapping.field) {\n const range =\n colorMapping.min !== undefined && colorMapping.max !== undefined\n ? { min: colorMapping.min, max: colorMapping.max }\n : calculateRange(processedData, colorMapping.field);\n config.cmin = range.min;\n config.cmax = range.max;\n config.colorbar = {\n title: { text: colorMapping.field, side: \"right\" },\n thickness: 20,\n len: 0.7,\n };\n }\n } else {\n config.color = colors;\n }\n\n return config;\n }, [sizes, shapes, colorMapping, plotlyColorscale, plotlyColors, showColorBar, processedData, colors]);\n\n // Create Plotly plot\n useEffect(() => {\n const currentRef = plotRef.current;\n if (!currentRef || processedData.length === 0) return;\n\n const xValues = processedData.map((p) => p.x);\n const yValues = processedData.map((p) => p.y);\n const ids = processedData.map((p) => String(p.id));\n\n // Create base trace\n const trace: Partial<Plotly.PlotData> & {\n unselected?: { marker?: { opacity?: number } };\n selected?: { marker?: { opacity?: number; line?: { color?: string; width?: number } } };\n } = {\n x: xValues,\n y: yValues,\n ids,\n mode: \"markers\",\n type: \"scatter\",\n marker: markerConfig,\n // \"none\" keeps hover events firing for the HTML tooltip without\n // showing Plotly's hover label; \"skip\" disables hover entirely\n hoverinfo: tooltipEnabled ? (nativeTooltip ? \"text\" : \"none\") : \"skip\",\n text: tooltipText,\n hovertemplate: tooltipEnabled && nativeTooltip ? \"%{text}<extra></extra>\" : undefined,\n showlegend: false,\n unselected: {\n marker: {\n opacity: 0.3,\n },\n },\n selected: {\n marker: {\n opacity: 1,\n line: {\n color: COLORS.selected,\n width: 2,\n },\n },\n },\n };\n\n const plotData: Plotly.Data[] = [trace as Plotly.Data];\n\n // Configure layout\n const layout: Partial<Plotly.Layout> = getPlotlyLayoutConfig({\n title,\n xAxis,\n yAxis,\n width,\n height,\n xRange,\n yRange,\n enableLassoSelection,\n enableBoxSelection,\n theme,\n });\n\n const config: Partial<Plotly.Config> = {\n responsive: true,\n displayModeBar: true,\n displaylogo: false,\n modeBarButtonsToAdd: [],\n modeBarButtonsToRemove: [\"toImage\"],\n };\n\n // Add selection mode buttons if enabled\n if (enableBoxSelection) {\n config.modeBarButtonsToAdd?.push(\"select2d\" as never);\n }\n if (enableLassoSelection) {\n config.modeBarButtonsToAdd?.push(\"lasso2d\" as never);\n }\n\n // Plotly is loaded lazily so it stays out of the consumer's main chunk\n // (SW-2007); the draw is skipped if the effect re-runs or unmounts first.\n let cancelled = false;\n let drawnElement: HTMLDivElement | null = null;\n void loadPlotly().then((plotly) => {\n if (cancelled || !plotRef.current) return;\n plotly.newPlot(currentRef, plotData, layout, config);\n\n // Attach event handlers\n const plotElement = currentRef as unknown as Plotly.PlotlyHTMLElement;\n\n // Handle point click\n if (enableClickSelection) {\n plotElement.on(\"plotly_click\", (eventData: Plotly.PlotMouseEvent) => {\n const point = eventData.points[0];\n if (point && point.data.ids) {\n const clickedId = point.data.ids[point.pointIndex];\n const clickedPoint = processedData.find((p) => String(p.id) === clickedId);\n\n if (clickedPoint) {\n // Call point click handler if provided\n onPointClickRef.current?.(clickedPoint, eventData.event as MouseEvent);\n\n // Handle selection. Use the original-typed ID from the matched point so\n // numeric IDs (e.g., 123) are preserved instead of being coerced to \"123\".\n const mode = getSelectionMode(eventData.event as MouseEvent);\n const newSelection = applySelection(selectedIdsRef.current, new Set([clickedPoint.id]), mode);\n\n if (!isControlledRef.current) {\n setInternalSelectedIds(newSelection);\n }\n onSelectionChangeRef.current?.(newSelection, mode);\n }\n }\n });\n }\n\n // Handle box/lasso selection\n if (enableBoxSelection || enableLassoSelection) {\n plotElement.on(\"plotly_selected\", (eventData: Plotly.PlotSelectionEvent) => {\n if (eventData && eventData.points) {\n const selectedPointIds = eventData.points\n .map((p): string | number | null => {\n if (p.data.ids && p.pointIndex !== undefined) {\n const strId = p.data.ids[p.pointIndex] as string;\n // Restore the original ID type (string or number) via the lookup.\n return originalIdLookup.get(strId) ?? strId;\n }\n return null;\n })\n .filter((id: string | number | null): id is string | number => id !== null);\n\n // Get selection mode from keyboard state\n // Note: Plotly doesn't pass the original event, so we can't detect modifiers\n // For now, box/lasso selection always replaces\n const mode: SelectionMode = \"replace\";\n const newSelection = new Set<string | number>(selectedPointIds);\n\n if (!isControlledRef.current) {\n setInternalSelectedIds(newSelection);\n }\n onSelectionChangeRef.current?.(newSelection, mode);\n }\n });\n\n // Handle deselect (clicking on background)\n plotElement.on(\"plotly_deselect\", () => {\n const newSelection = new Set<string | number>();\n if (!isControlledRef.current) {\n setInternalSelectedIds(newSelection);\n }\n onSelectionChangeRef.current?.(newSelection, \"replace\");\n });\n }\n\n // Design-system tooltip driven by Plotly hover events\n if (tooltipEnabled && !nativeTooltip) {\n bindTooltip(currentRef);\n }\n\n // Capture ref value for cleanup\n drawnElement = currentRef;\n });\n\n return () => {\n cancelled = true;\n if (drawnElement) {\n getLoadedPlotly().purge(drawnElement);\n }\n };\n }, [\n processedData,\n markerConfig,\n xAxis,\n yAxis,\n xRange,\n yRange,\n title,\n width,\n height,\n tooltip,\n tooltipText,\n enableClickSelection,\n enableBoxSelection,\n enableLassoSelection,\n originalIdLookup,\n tooltipEnabled,\n nativeTooltip,\n bindTooltip,\n theme,\n ]);\n\n // Apply selection state to Plotly\n useEffect(() => {\n if (!plotRef.current || processedData.length === 0) return;\n\n // Wait for the lazily-loaded Plotly module: the draw effect registers its\n // loadPlotly().then first, so by the time this callback runs the plot has\n // been drawn — preserving the original \"restyle right after newPlot\"\n // ordering on initial mount.\n let cancelled = false;\n void loadPlotly().then((plotly) => {\n const currentRef = plotRef.current;\n if (cancelled || !currentRef) return;\n // Guard: skip if the chart hasn't been drawn (e.g. the draw was cancelled).\n if (!(currentRef as { _fullLayout?: unknown })._fullLayout) return;\n\n const plotElement = currentRef as unknown as Plotly.PlotlyHTMLElement;\n\n // Find indices of selected points. Use the string-normalized set to match the string IDs Plotly stores\n const selectedIndices = processedData\n .map((point, index) => (normalizedSelectedIds.has(String(point.id)) ? index : null))\n .filter((index): index is number => index !== null);\n\n // Update Plotly selection\n if (selectedIndices.length > 0) {\n plotly.restyle(\n plotElement,\n {\n selectedpoints: [selectedIndices],\n },\n [0],\n );\n } else {\n plotly.restyle(\n plotElement,\n {\n selectedpoints: [null],\n },\n [0],\n );\n }\n });\n\n return () => {\n cancelled = true;\n };\n }, [normalizedSelectedIds, processedData]);\n\n const containerClassName = className\n ? `scatter-plot-interactive relative ${className}`\n : \"scatter-plot-interactive relative\";\n\n return (\n <div className={containerClassName}>\n <div ref={plotRef} className=\"scatter-plot-interactive__plot\" style={{ width, height }} />\n {tooltipElement}\n </div>\n );\n};\n\nexport { ScatterPlotInteractive };\n"],"names":["DEFAULT_AXIS_CONFIG","DEFAULT_TOOLTIP_CONFIG","ScatterPlotInteractive","data","title","xAxis","yAxis","colorMapping","shapeMapping","sizeMapping","tooltip","enableClickSelection","enableBoxSelection","enableLassoSelection","controlledSelectedIds","onSelectionChange","onPointClick","downsampling","width","height","showColorBar","className","plotRef","useRef","theme","usePlotlyTheme","onSelectionChangeRef","onPointClickRef","selectedIdsRef","isControlledRef","internalSelectedIds","setInternalSelectedIds","useState","isControlled","selectedIds","normalizedSelectedIds","useMemo","originalIdLookup","p","processedData","downsampleData","colors","mapColors","shapes","mapShapes","sizes","mapSizes","xRange","calculateAxisRange","PLOT_CONSTANTS","yRange","tooltipEnabled","nativeTooltip","tooltipText","point","generateTooltipContent","bindTooltip","tooltipElement","useChartTooltip","points","index","plotlyColors","value","plotlyColorscale","scale","DEFAULT_COLOR_SCALE","markerConfig","config","range","calculateRange","useEffect","currentRef","xValues","yValues","ids","plotData","COLORS","layout","getPlotlyLayoutConfig","cancelled","drawnElement","loadPlotly","plotly","plotElement","eventData","clickedId","clickedPoint","mode","getSelectionMode","newSelection","applySelection","selectedPointIds","strId","id","getLoadedPlotly","selectedIndices","containerClassName","jsxs","jsx"],"mappings":";;;;;;;;AA4BA,MAAMA,KAAkC,CAAA,GAClCC,KAAwC,EAAE,SAAS,GAAA,GAuBnDC,KAAgE,CAAC;AAAA,EACrE,MAAAC;AAAA,EACA,OAAAC;AAAA,EACA,OAAAC,IAAQL;AAAA,EACR,OAAAM,IAAQN;AAAA,EACR,cAAAO;AAAA,EACA,cAAAC;AAAA,EACA,aAAAC;AAAA,EACA,SAAAC,IAAUT;AAAA,EACV,sBAAAU,IAAuB;AAAA,EACvB,oBAAAC,IAAqB;AAAA,EACrB,sBAAAC,IAAuB;AAAA,EACvB,aAAaC;AAAA,EACb,mBAAAC;AAAA,EACA,cAAAC;AAAA,EACA,cAAAC;AAAA,EACA,OAAAC,IAAQ;AAAA,EACR,QAAAC,IAAS;AAAA,EACT,cAAAC,IAAe;AAAA,EACf,WAAAC;AACF,MAAM;AACJ,QAAMC,IAAUC,EAAuB,IAAI,GACrCC,IAAQC,GAAA,GACRC,IAAuBH,EAAOR,CAAiB,GAC/CY,IAAkBJ,EAAOP,CAAY,GACrCY,IAAiBL,EAA6B,oBAAI,KAAK,GACvDM,IAAkBN,EAAO,EAAK;AAGpC,EAAAG,EAAqB,UAAUX,GAC/BY,EAAgB,UAAUX;AAG1B,QAAM,CAACc,IAAqBC,CAAsB,IAAIC,GAA+B,oBAAI,KAAK,GAGxFC,IAAenB,MAA0B,QACzCoB,IAAcD,IAAenB,IAAwBgB;AAC3D,EAAAF,EAAe,UAAUM,GACzBL,EAAgB,UAAUI;AAI1B,QAAME,IAAwBC,EAAQ,MAAM,IAAI,IAAI,CAAC,GAAGF,CAAW,EAAE,IAAI,MAAM,CAAC,GAAG,CAACA,CAAW,CAAC,GAI1FG,KAAmBD,EAAQ,MAAM,IAAI,IAAIjC,EAAK,IAAI,CAACmC,MAAM,CAAC,OAAOA,EAAE,EAAE,GAAGA,EAAE,EAAE,CAAC,CAAC,GAAG,CAACnC,CAAI,CAAC,GAGvFoC,IAAgBH,EAAQ,MACvBnB,IAGEuB,GAAerC,GAAMc,CAAY,IAF/Bd,GAGR,CAACA,GAAMc,CAAY,CAAC,GAGjBwB,IAASL,EAAQ,MAAMM,GAAUH,GAAehC,CAAY,GAAG,CAACgC,GAAehC,CAAY,CAAC,GAE5FoC,KAASP,EAAQ,MAAMQ,GAAUL,GAAe/B,CAAY,GAAG,CAAC+B,GAAe/B,CAAY,CAAC,GAE5FqC,KAAQT,EAAQ,MAAMU,GAASP,GAAe9B,CAAW,GAAG,CAAC8B,GAAe9B,CAAW,CAAC,GAGxFsC,KAASX,EAAQ,MAAM;AAC3B,QAAI/B,EAAM,MAAO,QAAOA,EAAM;AAC9B,QAAIA,EAAM,cAAc;AACxB,aAAO2C,GAAmBT,GAAe,KAAKlC,EAAM,oBAAoB4C,GAAe,oBAAoB5C,EAAM,KAAK;AAAA,EACxH,GAAG,CAACkC,GAAelC,CAAK,CAAC,GAEnB6C,KAASd,EAAQ,MAAM;AAC3B,QAAI9B,EAAM,MAAO,QAAOA,EAAM;AAC9B,QAAIA,EAAM,cAAc;AACxB,aAAO0C,GAAmBT,GAAe,KAAKjC,EAAM,oBAAoB2C,GAAe,oBAAoB3C,EAAM,KAAK;AAAA,EACxH,GAAG,CAACiC,GAAejC,CAAK,CAAC,GAEnB6C,IAAiBzC,EAAQ,YAAY,IACrC0C,IAAgB1C,EAAQ,WAAW,IAGnC2C,IAAcjB,EAAQ,MACrBe,IAEEZ,EAAc;AAAA,IAAI,CAACe,MACxB5C,EAAQ,UAAUA,EAAQ,QAAQ4C,CAAK,IAAIC,GAAuBD,GAAO5C,EAAQ,MAAM;AAAA,EAAA,IAH7D,CAAA,GAK3B,CAAC6B,GAAe7B,GAASyC,CAAc,CAAC,GAGrC,EAAE,aAAAK,IAAa,gBAAAC,GAAA,IAAmBC,GAAgB;AAAA,IACtD,UAAU,CAACC,MAAW;AACpB,YAAMC,IAAQD,EAAO,CAAC,GAAG;AACzB,aAAIC,MAAU,SAAkB,CAAA,KACxBP,EAAYO,CAAK,KAAK,IAAI,MAAM,MAAM,EAAE,OAAO,OAAO;AAAA,IAChE;AAAA,EAAA,CACD,GAGKC,KAAezB,EAAQ,MACvB7B,GAAc,SAAS,gBAAgBA,EAAa,SAASgC,EAAc,SAAS,IAE/EA,EAAc,IAAI,CAACe,MAAU;AAClC,UAAMQ,IAAQR,EAAM,WAAW/C,EAAa,KAAM;AAClD,WAAO,OAAOuD,KAAU,YAAY,OAAO,SAASA,CAAK,IAAIA,IAAQ;AAAA,EACvE,CAAC,IAEIrB,GACN,CAACF,GAAehC,GAAckC,CAAM,CAAC,GAGlCsB,IAAmB3B,EAAQ,MAAM;AACrC,QAAI7B,GAAc,SAAS,cAAc;AACvC,YAAMyD,IAAQzD,EAAa,cAAc0D;AACzC,aAAmCD;AAAA,IACrC;AAAA,EACF,GAAG,CAACzD,CAAY,CAAC,GAGX2D,KAAe9B,EAAQ,MAAkC;AAC7D,UAAM+B,IAAqC;AAAA,MACzC,MAAMtB;AAAA,MACN,QAAQF;AAAA;AAAA;AAAA,MAGR,MAAM;AAAA,QACJ,OAAO;AAAA,MAAA;AAAA,IACT;AAIF,QAAIpC,GAAc,SAAS,gBAAgBwD;AAKzC,UAJAI,EAAO,QAAQN,IACfM,EAAO,aAAaJ,GACpBI,EAAO,YAAY/C,GAEfb,EAAa,OAAO;AACtB,cAAM6D,IACJ7D,EAAa,QAAQ,UAAaA,EAAa,QAAQ,SACnD,EAAE,KAAKA,EAAa,KAAK,KAAKA,EAAa,IAAA,IAC3C8D,GAAe9B,GAAehC,EAAa,KAAK;AACtD,QAAA4D,EAAO,OAAOC,EAAM,KACpBD,EAAO,OAAOC,EAAM,KACpBD,EAAO,WAAW;AAAA,UAChB,OAAO,EAAE,MAAM5D,EAAa,OAAO,MAAM,QAAA;AAAA,UACzC,WAAW;AAAA,UACX,KAAK;AAAA,QAAA;AAAA,MAET;AAAA;AAEA,MAAA4D,EAAO,QAAQ1B;AAGjB,WAAO0B;AAAA,EACT,GAAG,CAACtB,IAAOF,IAAQpC,GAAcwD,GAAkBF,IAAczC,GAAcmB,GAAeE,CAAM,CAAC;AAGrG,EAAA6B,GAAU,MAAM;AACd,UAAMC,IAAajD,EAAQ;AAC3B,QAAI,CAACiD,KAAchC,EAAc,WAAW,EAAG;AAE/C,UAAMiC,IAAUjC,EAAc,IAAI,CAACD,MAAMA,EAAE,CAAC,GACtCmC,IAAUlC,EAAc,IAAI,CAACD,MAAMA,EAAE,CAAC,GACtCoC,IAAMnC,EAAc,IAAI,CAACD,MAAM,OAAOA,EAAE,EAAE,CAAC,GAmC3CqC,IAA0B,CA7B5B;AAAA,MACF,GAAGH;AAAA,MACH,GAAGC;AAAA,MACH,KAAAC;AAAA,MACA,MAAM;AAAA,MACN,MAAM;AAAA,MACN,QAAQR;AAAA;AAAA;AAAA,MAGR,WAAWf,IAAkBC,IAAgB,SAAS,SAAU;AAAA,MAChE,MAAMC;AAAA,MACN,eAAeF,KAAkBC,IAAgB,2BAA2B;AAAA,MAC5E,YAAY;AAAA,MACZ,YAAY;AAAA,QACV,QAAQ;AAAA,UACN,SAAS;AAAA,QAAA;AAAA,MACX;AAAA,MAEF,UAAU;AAAA,QACR,QAAQ;AAAA,UACN,SAAS;AAAA,UACT,MAAM;AAAA,YACJ,OAAOwB,GAAO;AAAA,YACd,OAAO;AAAA,UAAA;AAAA,QACT;AAAA,MACF;AAAA,IACF,CAGmD,GAG/CC,IAAiCC,GAAsB;AAAA,MAC3D,OAAA1E;AAAA,MACA,OAAAC;AAAA,MACA,OAAAC;AAAA,MACA,OAAAY;AAAA,MACA,QAAAC;AAAA,MACA,QAAA4B;AAAA,MACA,QAAAG;AAAA,MACA,sBAAArC;AAAA,MACA,oBAAAD;AAAA,MACA,OAAAY;AAAA,IAAA,CACD,GAEK2C,IAAiC;AAAA,MACrC,YAAY;AAAA,MACZ,gBAAgB;AAAA,MAChB,aAAa;AAAA,MACb,qBAAqB,CAAA;AAAA,MACrB,wBAAwB,CAAC,SAAS;AAAA,IAAA;AAIpC,IAAIvD,KACFuD,EAAO,qBAAqB,KAAK,UAAmB,GAElDtD,KACFsD,EAAO,qBAAqB,KAAK,SAAkB;AAKrD,QAAIY,KAAY,IACZC,IAAsC;AAC1C,WAAKC,GAAA,EAAa,KAAK,CAACC,MAAW;AACjC,UAAIH,MAAa,CAACzD,EAAQ,QAAS;AACnC,MAAA4D,EAAO,QAAQX,GAAYI,GAAUE,GAAQV,CAAM;AAGnD,YAAMgB,IAAcZ;AAGpB,MAAI5D,KACFwE,EAAY,GAAG,gBAAgB,CAACC,MAAqC;AACnE,cAAM9B,IAAQ8B,EAAU,OAAO,CAAC;AAChC,YAAI9B,KAASA,EAAM,KAAK,KAAK;AAC3B,gBAAM+B,IAAY/B,EAAM,KAAK,IAAIA,EAAM,UAAU,GAC3CgC,IAAe/C,EAAc,KAAK,CAACD,MAAM,OAAOA,EAAE,EAAE,MAAM+C,CAAS;AAEzE,cAAIC,GAAc;AAEhB,YAAA3D,EAAgB,UAAU2D,GAAcF,EAAU,KAAmB;AAIrE,kBAAMG,IAAOC,GAAiBJ,EAAU,KAAmB,GACrDK,IAAeC,GAAe9D,EAAe,SAAS,oBAAI,IAAI,CAAC0D,EAAa,EAAE,CAAC,GAAGC,CAAI;AAE5F,YAAK1D,EAAgB,WACnBE,EAAuB0D,CAAY,GAErC/D,EAAqB,UAAU+D,GAAcF,CAAI;AAAA,UACnD;AAAA,QACF;AAAA,MACF,CAAC,IAIC3E,KAAsBC,OACxBsE,EAAY,GAAG,mBAAmB,CAACC,MAAyC;AAC1E,YAAIA,KAAaA,EAAU,QAAQ;AACjC,gBAAMO,IAAmBP,EAAU,OAChC,IAAI,CAAC9C,MAA8B;AAClC,gBAAIA,EAAE,KAAK,OAAOA,EAAE,eAAe,QAAW;AAC5C,oBAAMsD,IAAQtD,EAAE,KAAK,IAAIA,EAAE,UAAU;AAErC,qBAAOD,GAAiB,IAAIuD,CAAK,KAAKA;AAAA,YACxC;AACA,mBAAO;AAAA,UACT,CAAC,EACA,OAAO,CAACC,MAAsDA,MAAO,IAAI,GAKtEN,IAAsB,WACtBE,IAAe,IAAI,IAAqBE,CAAgB;AAE9D,UAAK9D,EAAgB,WACnBE,EAAuB0D,CAAY,GAErC/D,EAAqB,UAAU+D,GAAcF,CAAI;AAAA,QACnD;AAAA,MACF,CAAC,GAGDJ,EAAY,GAAG,mBAAmB,MAAM;AACtC,cAAMM,wBAAmB,IAAA;AACzB,QAAK5D,EAAgB,WACnBE,EAAuB0D,CAAY,GAErC/D,EAAqB,UAAU+D,GAAc,SAAS;AAAA,MACxD,CAAC,IAICtC,KAAkB,CAACC,KACrBI,GAAYe,CAAU,GAIxBS,IAAeT;AAAA,IACjB,CAAC,GAEM,MAAM;AACX,MAAAQ,KAAY,IACRC,KACFc,GAAA,EAAkB,MAAMd,CAAY;AAAA,IAExC;AAAA,EACF,GAAG;AAAA,IACDzC;AAAA,IACA2B;AAAA,IACA7D;AAAA,IACAC;AAAA,IACAyC;AAAA,IACAG;AAAA,IACA9C;AAAA,IACAc;AAAA,IACAC;AAAA,IACAT;AAAA,IACA2C;AAAA,IACA1C;AAAA,IACAC;AAAA,IACAC;AAAA,IACAwB;AAAA,IACAc;AAAA,IACAC;AAAA,IACAI;AAAA,IACAhC;AAAA,EAAA,CACD,GAGD8C,GAAU,MAAM;AACd,QAAI,CAAChD,EAAQ,WAAWiB,EAAc,WAAW,EAAG;AAMpD,QAAIwC,IAAY;AAChB,WAAKE,GAAA,EAAa,KAAK,CAACC,MAAW;AACjC,YAAMX,IAAajD,EAAQ;AAG3B,UAFIyD,KAAa,CAACR,KAEd,CAAEA,EAAyC,YAAa;AAE5D,YAAMY,IAAcZ,GAGdwB,IAAkBxD,EACrB,IAAI,CAACe,GAAOM,MAAWzB,EAAsB,IAAI,OAAOmB,EAAM,EAAE,CAAC,IAAIM,IAAQ,IAAK,EAClF,OAAO,CAACA,MAA2BA,MAAU,IAAI;AAGpD,MAAImC,EAAgB,SAAS,IAC3Bb,EAAO;AAAA,QACLC;AAAA,QACA;AAAA,UACE,gBAAgB,CAACY,CAAe;AAAA,QAAA;AAAA,QAElC,CAAC,CAAC;AAAA,MAAA,IAGJb,EAAO;AAAA,QACLC;AAAA,QACA;AAAA,UACE,gBAAgB,CAAC,IAAI;AAAA,QAAA;AAAA,QAEvB,CAAC,CAAC;AAAA,MAAA;AAAA,IAGR,CAAC,GAEM,MAAM;AACX,MAAAJ,IAAY;AAAA,IACd;AAAA,EACF,GAAG,CAAC5C,GAAuBI,CAAa,CAAC;AAEzC,QAAMyD,KAAqB3E,IACvB,qCAAqCA,CAAS,KAC9C;AAEJ,SACE,gBAAA4E,GAAC,OAAA,EAAI,WAAWD,IACd,UAAA;AAAA,IAAA,gBAAAE,GAAC,OAAA,EAAI,KAAK5E,GAAS,WAAU,kCAAiC,OAAO,EAAE,OAAAJ,GAAO,QAAAC,EAAA,EAAO,CAAG;AAAA,IACvFsC;AAAA,EAAA,GACH;AAEJ;"}
|
|
1
|
+
{"version":3,"file":"ScatterPlotInteractive.js","sources":["../../../../src/components/charts/ScatterPlotInteractive/ScatterPlotInteractive.tsx"],"sourcesContent":["import React, { useEffect, useMemo, useRef, useState } from \"react\";\n\nimport { useChartTooltip } from \"../ChartTooltip\";\nimport { getLoadedPlotly, loadPlotly } from \"../plotly-loader\";\n\nimport { COLORS, DEFAULT_COLOR_SCALE, PLOT_CONSTANTS } from \"./constants\";\nimport {\n applySelection,\n calculateAxisRange,\n calculateRange,\n downsampleData,\n generateTooltipContent,\n getPlotlyLayoutConfig,\n getSelectionMode,\n mapColors,\n mapShapes,\n mapSizes,\n} from \"./utils\";\n\nimport type { AxisConfig, ScatterPlotInteractiveProps, SelectionMode, TooltipConfig } from \"./types\";\nimport type Plotly from \"plotly.js-dist\";\n\nimport { usePlotlyTheme } from \"@/hooks/use-plotly-theme\";\n\n// Stable default prop objects — inline defaults would create a new identity\n// on every render, retriggering the plot effect (and tearing down the\n// tooltip) whenever internal state changes\nconst DEFAULT_AXIS_CONFIG: AxisConfig = {};\nconst DEFAULT_TOOLTIP_CONFIG: TooltipConfig = { enabled: true };\n\n/**\n * ScatterPlotInteractive component for visualizing scatter plot data with advanced interactions.\n *\n * **Features:**\n * - Data-driven and static styling (color, shape, size)\n * - Interactive selection (click, box, lasso)\n * - Keyboard modifiers for click-selection modes (Shift/Ctrl)\n * - Customizable tooltips with rich content support\n * - Axis customization (ranges, log/linear scales)\n * - Performance optimizations for large datasets\n * - Selection propagation via callbacks\n *\n * **Selection Modes (click selection only):**\n * - Default click: Replace selection\n * - Shift + click: Add to selection\n * - Ctrl/Cmd + click: Remove from selection\n * - Shift + Ctrl + click: Toggle selection\n *\n * Box/lasso selection always replaces the current selection because\n * Plotly does not expose the original keyboard event for drag operations.\n */\nconst ScatterPlotInteractive: React.FC<ScatterPlotInteractiveProps> = ({\n data,\n title,\n xAxis = DEFAULT_AXIS_CONFIG,\n yAxis = DEFAULT_AXIS_CONFIG,\n colorMapping,\n shapeMapping,\n sizeMapping,\n tooltip = DEFAULT_TOOLTIP_CONFIG,\n enableClickSelection = true,\n enableBoxSelection = true,\n enableLassoSelection = true,\n selectedIds: controlledSelectedIds,\n onSelectionChange,\n onPointClick,\n downsampling,\n width = 800,\n height = 600,\n showColorBar = true,\n className,\n}) => {\n const plotRef = useRef<HTMLDivElement>(null);\n const theme = usePlotlyTheme();\n const onSelectionChangeRef = useRef(onSelectionChange);\n const onPointClickRef = useRef(onPointClick);\n const selectedIdsRef = useRef<Set<string | number>>(new Set());\n const isControlledRef = useRef(false);\n\n // Keep refs updated\n onSelectionChangeRef.current = onSelectionChange;\n onPointClickRef.current = onPointClick;\n\n // Internal selection state (only used in uncontrolled mode)\n const [internalSelectedIds, setInternalSelectedIds] = useState<Set<string | number>>(new Set());\n\n // Use controlled or uncontrolled selection\n const isControlled = controlledSelectedIds !== undefined;\n const selectedIds = isControlled ? controlledSelectedIds : internalSelectedIds;\n selectedIdsRef.current = selectedIds;\n isControlledRef.current = isControlled;\n\n // Normalize to strings so numeric IDs passed by consumers always match the\n // string IDs that Plotly stores (we always do String(p.id) when building the trace).\n const normalizedSelectedIds = useMemo(() => new Set([...selectedIds].map(String)), [selectedIds]);\n\n // Reverse-lookup: Plotly string ID → original (possibly numeric) ScatterPoint ID.\n // Used to restore original ID types when echoing selections back to consumers.\n const originalIdLookup = useMemo(() => new Map(data.map((p) => [String(p.id), p.id])), [data]);\n\n // Apply downsampling if configured\n const processedData = useMemo(() => {\n if (!downsampling) {\n return data;\n }\n return downsampleData(data, downsampling);\n }, [data, downsampling]);\n\n // Map colors, shapes, and sizes based on configuration\n const colors = useMemo(() => mapColors(processedData, colorMapping), [processedData, colorMapping]);\n\n const shapes = useMemo(() => mapShapes(processedData, shapeMapping), [processedData, shapeMapping]);\n\n const sizes = useMemo(() => mapSizes(processedData, sizeMapping), [processedData, sizeMapping]);\n\n // Calculate axis ranges\n const xRange = useMemo(() => {\n if (xAxis.range) return xAxis.range;\n if (xAxis.autoRange === false) return;\n return calculateAxisRange(processedData, \"x\", xAxis.autoRangePadding ?? PLOT_CONSTANTS.AUTO_RANGE_PADDING, xAxis.scale);\n }, [processedData, xAxis]);\n\n const yRange = useMemo(() => {\n if (yAxis.range) return yAxis.range;\n if (yAxis.autoRange === false) return;\n return calculateAxisRange(processedData, \"y\", yAxis.autoRangePadding ?? PLOT_CONSTANTS.AUTO_RANGE_PADDING, yAxis.scale);\n }, [processedData, yAxis]);\n\n const tooltipEnabled = tooltip.enabled !== false;\n const nativeTooltip = tooltip.native === true;\n\n // Build tooltip text\n const tooltipText = useMemo(() => {\n if (!tooltipEnabled) return [];\n\n return processedData.map((point) =>\n tooltip.content ? tooltip.content(point) : generateTooltipContent(point, tooltip.fields),\n );\n }, [processedData, tooltip, tooltipEnabled]);\n\n // Design-system tooltip driven by Plotly hover events\n const { bindTooltip, tooltipElement } = useChartTooltip({\n getLines: (points) => {\n const index = points[0]?.pointIndex;\n if (index === undefined) return [];\n return (tooltipText[index] ?? \"\").split(\"<br>\").filter(Boolean);\n },\n });\n\n // Prepare Plotly-compatible color data\n const plotlyColors = useMemo(() => {\n if (colorMapping?.type === \"continuous\" && colorMapping.field && processedData.length > 0) {\n // For continuous mapping, extract the numeric values\n return processedData.map((point) => {\n const value = point.metadata?.[colorMapping.field!];\n return typeof value === \"number\" && Number.isFinite(value) ? value : 0;\n });\n }\n return colors;\n }, [processedData, colorMapping, colors]);\n\n // Build colorscale for continuous mapping\n const plotlyColorscale = useMemo(() => {\n if (colorMapping?.type === \"continuous\") {\n const scale = colorMapping.colorScale || DEFAULT_COLOR_SCALE;\n return typeof scale === \"string\" ? scale : scale;\n }\n }, [colorMapping]);\n\n // Build marker configuration for Plotly\n const markerConfig = useMemo((): Partial<Plotly.PlotMarker> => {\n const config: Partial<Plotly.PlotMarker> = {\n size: sizes,\n symbol: shapes,\n // Points carry no outline — a flat fill (SW-2298 dropped the earlier CSS\n // drop-shadow). Selected points re-add a line below.\n line: {\n width: 0,\n },\n };\n\n // Configure color mapping\n if (colorMapping?.type === \"continuous\" && plotlyColorscale) {\n config.color = plotlyColors as number[];\n config.colorscale = plotlyColorscale;\n config.showscale = showColorBar;\n\n if (colorMapping.field) {\n const range =\n colorMapping.min !== undefined && colorMapping.max !== undefined\n ? { min: colorMapping.min, max: colorMapping.max }\n : calculateRange(processedData, colorMapping.field);\n config.cmin = range.min;\n config.cmax = range.max;\n config.colorbar = {\n title: { text: colorMapping.field, side: \"right\" },\n thickness: 20,\n len: 0.7,\n };\n }\n } else {\n config.color = colors;\n }\n\n return config;\n }, [sizes, shapes, colorMapping, plotlyColorscale, plotlyColors, showColorBar, processedData, colors]);\n\n // Create Plotly plot\n useEffect(() => {\n const currentRef = plotRef.current;\n if (!currentRef || processedData.length === 0) return;\n\n const xValues = processedData.map((p) => p.x);\n const yValues = processedData.map((p) => p.y);\n const ids = processedData.map((p) => String(p.id));\n\n // Create base trace\n const trace: Partial<Plotly.PlotData> & {\n unselected?: { marker?: { opacity?: number } };\n selected?: { marker?: { opacity?: number; line?: { color?: string; width?: number } } };\n } = {\n x: xValues,\n y: yValues,\n ids,\n mode: \"markers\",\n type: \"scatter\",\n marker: markerConfig,\n // \"none\" keeps hover events firing for the HTML tooltip without\n // showing Plotly's hover label; \"skip\" disables hover entirely\n hoverinfo: tooltipEnabled ? (nativeTooltip ? \"text\" : \"none\") : \"skip\",\n text: tooltipText,\n hovertemplate: tooltipEnabled && nativeTooltip ? \"%{text}<extra></extra>\" : undefined,\n showlegend: false,\n unselected: {\n marker: {\n opacity: 0.3,\n },\n },\n selected: {\n marker: {\n opacity: 1,\n line: {\n color: COLORS.selected,\n width: 2,\n },\n },\n },\n };\n\n const plotData: Plotly.Data[] = [trace as Plotly.Data];\n\n // Configure layout\n const layout: Partial<Plotly.Layout> = getPlotlyLayoutConfig({\n title,\n xAxis,\n yAxis,\n width,\n height,\n xRange,\n yRange,\n enableLassoSelection,\n enableBoxSelection,\n theme,\n });\n\n const config: Partial<Plotly.Config> = {\n responsive: true,\n displayModeBar: true,\n displaylogo: false,\n modeBarButtonsToAdd: [],\n modeBarButtonsToRemove: [\"toImage\"],\n };\n\n // Add selection mode buttons if enabled\n if (enableBoxSelection) {\n config.modeBarButtonsToAdd?.push(\"select2d\" as never);\n }\n if (enableLassoSelection) {\n config.modeBarButtonsToAdd?.push(\"lasso2d\" as never);\n }\n\n // Plotly is loaded lazily so it stays out of the consumer's main chunk\n // (SW-2007); the draw is skipped if the effect re-runs or unmounts first.\n let cancelled = false;\n let drawnElement: HTMLDivElement | null = null;\n void loadPlotly().then((plotly) => {\n if (cancelled || !plotRef.current) return;\n plotly.newPlot(currentRef, plotData, layout, config);\n\n // Attach event handlers\n const plotElement = currentRef as unknown as Plotly.PlotlyHTMLElement;\n\n // Handle point click\n if (enableClickSelection) {\n plotElement.on(\"plotly_click\", (eventData: Plotly.PlotMouseEvent) => {\n const point = eventData.points[0];\n if (point && point.data.ids) {\n const clickedId = point.data.ids[point.pointIndex];\n const clickedPoint = processedData.find((p) => String(p.id) === clickedId);\n\n if (clickedPoint) {\n // Call point click handler if provided\n onPointClickRef.current?.(clickedPoint, eventData.event as MouseEvent);\n\n // Handle selection. Use the original-typed ID from the matched point so\n // numeric IDs (e.g., 123) are preserved instead of being coerced to \"123\".\n const mode = getSelectionMode(eventData.event as MouseEvent);\n const newSelection = applySelection(selectedIdsRef.current, new Set([clickedPoint.id]), mode);\n\n if (!isControlledRef.current) {\n setInternalSelectedIds(newSelection);\n }\n onSelectionChangeRef.current?.(newSelection, mode);\n }\n }\n });\n }\n\n // Handle box/lasso selection\n if (enableBoxSelection || enableLassoSelection) {\n plotElement.on(\"plotly_selected\", (eventData: Plotly.PlotSelectionEvent) => {\n if (eventData && eventData.points) {\n const selectedPointIds = eventData.points\n .map((p): string | number | null => {\n if (p.data.ids && p.pointIndex !== undefined) {\n const strId = p.data.ids[p.pointIndex] as string;\n // Restore the original ID type (string or number) via the lookup.\n return originalIdLookup.get(strId) ?? strId;\n }\n return null;\n })\n .filter((id: string | number | null): id is string | number => id !== null);\n\n // Get selection mode from keyboard state\n // Note: Plotly doesn't pass the original event, so we can't detect modifiers\n // For now, box/lasso selection always replaces\n const mode: SelectionMode = \"replace\";\n const newSelection = new Set<string | number>(selectedPointIds);\n\n if (!isControlledRef.current) {\n setInternalSelectedIds(newSelection);\n }\n onSelectionChangeRef.current?.(newSelection, mode);\n }\n });\n\n // Handle deselect (clicking on background)\n plotElement.on(\"plotly_deselect\", () => {\n const newSelection = new Set<string | number>();\n if (!isControlledRef.current) {\n setInternalSelectedIds(newSelection);\n }\n onSelectionChangeRef.current?.(newSelection, \"replace\");\n });\n }\n\n // Design-system tooltip driven by Plotly hover events\n if (tooltipEnabled && !nativeTooltip) {\n bindTooltip(currentRef);\n }\n\n // Capture ref value for cleanup\n drawnElement = currentRef;\n });\n\n return () => {\n cancelled = true;\n if (drawnElement) {\n getLoadedPlotly().purge(drawnElement);\n }\n };\n }, [\n processedData,\n markerConfig,\n xAxis,\n yAxis,\n xRange,\n yRange,\n title,\n width,\n height,\n tooltip,\n tooltipText,\n enableClickSelection,\n enableBoxSelection,\n enableLassoSelection,\n originalIdLookup,\n tooltipEnabled,\n nativeTooltip,\n bindTooltip,\n theme,\n ]);\n\n // Apply selection state to Plotly\n useEffect(() => {\n if (!plotRef.current || processedData.length === 0) return;\n\n // Wait for the lazily-loaded Plotly module: the draw effect registers its\n // loadPlotly().then first, so by the time this callback runs the plot has\n // been drawn — preserving the original \"restyle right after newPlot\"\n // ordering on initial mount.\n let cancelled = false;\n void loadPlotly().then((plotly) => {\n const currentRef = plotRef.current;\n if (cancelled || !currentRef) return;\n // Guard: skip if the chart hasn't been drawn (e.g. the draw was cancelled).\n if (!(currentRef as { _fullLayout?: unknown })._fullLayout) return;\n\n const plotElement = currentRef as unknown as Plotly.PlotlyHTMLElement;\n\n // Find indices of selected points. Use the string-normalized set to match the string IDs Plotly stores\n const selectedIndices = processedData\n .map((point, index) => (normalizedSelectedIds.has(String(point.id)) ? index : null))\n .filter((index): index is number => index !== null);\n\n // Update Plotly selection\n if (selectedIndices.length > 0) {\n plotly.restyle(\n plotElement,\n {\n selectedpoints: [selectedIndices],\n },\n [0],\n );\n } else {\n plotly.restyle(\n plotElement,\n {\n selectedpoints: [null],\n },\n [0],\n );\n }\n });\n\n return () => {\n cancelled = true;\n };\n }, [normalizedSelectedIds, processedData]);\n\n const containerClassName = className\n ? `scatter-plot-interactive relative ${className}`\n : \"scatter-plot-interactive relative\";\n\n return (\n <div className={containerClassName}>\n <div ref={plotRef} className=\"scatter-plot-interactive__plot\" style={{ width, height }} />\n {tooltipElement}\n </div>\n );\n};\n\nexport { ScatterPlotInteractive };\n"],"names":["DEFAULT_AXIS_CONFIG","DEFAULT_TOOLTIP_CONFIG","ScatterPlotInteractive","data","title","xAxis","yAxis","colorMapping","shapeMapping","sizeMapping","tooltip","enableClickSelection","enableBoxSelection","enableLassoSelection","controlledSelectedIds","onSelectionChange","onPointClick","downsampling","width","height","showColorBar","className","plotRef","useRef","theme","usePlotlyTheme","onSelectionChangeRef","onPointClickRef","selectedIdsRef","isControlledRef","internalSelectedIds","setInternalSelectedIds","useState","isControlled","selectedIds","normalizedSelectedIds","useMemo","originalIdLookup","p","processedData","downsampleData","colors","mapColors","shapes","mapShapes","sizes","mapSizes","xRange","calculateAxisRange","PLOT_CONSTANTS","yRange","tooltipEnabled","nativeTooltip","tooltipText","point","generateTooltipContent","bindTooltip","tooltipElement","useChartTooltip","points","index","plotlyColors","value","plotlyColorscale","scale","DEFAULT_COLOR_SCALE","markerConfig","config","range","calculateRange","useEffect","currentRef","xValues","yValues","ids","plotData","COLORS","layout","getPlotlyLayoutConfig","cancelled","drawnElement","loadPlotly","plotly","plotElement","eventData","clickedId","clickedPoint","mode","getSelectionMode","newSelection","applySelection","selectedPointIds","strId","id","getLoadedPlotly","selectedIndices","containerClassName","jsxs","jsx"],"mappings":"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"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const h=360,S=480,_={tickFontSize:16,ticklen:12,axisTitleFontSize:16,axisTitleStandoff:15,legendFontSize:13,legendLineHeight:18,titleFontSize:32,margin:{l:80,r:40,b:96,tTitle:80,tNoTitle:40}},r={tickFontSize:11,ticklen:6,axisTitleFontSize:12,axisTitleStandoff:8,legendFontSize:11,legendLineHeight:14,titleFontSize:18,margin:{l:48,r:16,b:76,tTitle:36,tNoTitle:12}},g=t=>t<h,m=t=>t<S,T=new WeakMap;function f(t,i,e=_){const o=g(i),n=m(t);if(!o&&!n)return e;if(o&&n)return r;const l=o?"short":"narrow";let c=T.get(e);c||(c=new Map,T.set(e,c));let a=c.get(l);if(!a){const s=o?r:e,C=n?r:e;a={...s,margin:{...s.margin,l:C.margin.l,r:C.margin.r}},c.set(l,a)}return a}const A=2.2;function M(t,i){return i<=0?2:Math.max(2,Math.floor(t/i))}function d(t,i){const e=Math.max(2,i);if(t.length<=e)return t;const o=Math.ceil(t.length/e),n=t.filter((c,a)=>a%o===0),l=t[t.length-1];return n[n.length-1]===l?n:[...n.slice(0,-1),l]}exports.COMPACT_HEIGHT_PX=h;exports.COMPACT_SCALE=r;exports.COMPACT_WIDTH_PX=S;exports.REGULAR_SCALE=_;exports.Y_TICK_LABEL_SPACING=A;exports.isCompactHeight=g;exports.isCompactWidth=m;exports.maxTickCount=M;exports.resolveChartScale=f;exports.thinTicks=d;
|
|
2
|
+
//# sourceMappingURL=chart-scale.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"chart-scale.cjs","sources":["../../../src/components/charts/chart-scale.ts"],"sourcesContent":["/**\n * Shared sizing rules for the cartesian charts (AreaPlot, BarChart, BoxPlot).\n *\n * Their chrome is tuned for dashboard-sized canvases: 16px tick labels, 80px\n * side margins, a 32px title. Below a few hundred pixels that fixed chrome\n * eats most of the canvas — y-tick labels pile onto each other and the legend\n * grows a scrollbar (SW-2298). Charts switch to the compact scale here so the\n * plot area stays legible in small tiles.\n */\n\n/** Heights below this switch to the compact scale */\nexport const COMPACT_HEIGHT_PX = 360;\n/** Widths below this switch to the compact scale */\nexport const COMPACT_WIDTH_PX = 480;\n\nexport interface ChartScale {\n tickFontSize: number;\n ticklen: number;\n axisTitleFontSize: number;\n axisTitleStandoff: number;\n legendFontSize: number;\n legendLineHeight: number;\n titleFontSize: number;\n margin: {\n l: number;\n r: number;\n b: number;\n /** Top margin when a title is rendered */\n tTitle: number;\n /** Top margin without a title */\n tNoTitle: number;\n };\n}\n\nexport const REGULAR_SCALE: ChartScale = {\n tickFontSize: 16,\n ticklen: 12,\n axisTitleFontSize: 16,\n axisTitleStandoff: 15,\n legendFontSize: 13,\n legendLineHeight: 18,\n titleFontSize: 32,\n // Bottom margin reserves room for tick labels, the x-axis title, and the\n // container-anchored bottom legend stacked beneath them (SW-2157).\n margin: { l: 80, r: 40, b: 96, tTitle: 80, tNoTitle: 40 },\n};\n\nexport const COMPACT_SCALE: ChartScale = {\n tickFontSize: 11,\n ticklen: 6,\n axisTitleFontSize: 12,\n axisTitleStandoff: 8,\n legendFontSize: 11,\n legendLineHeight: 14,\n titleFontSize: 18,\n margin: { l: 48, r: 16, b: 76, tTitle: 36, tNoTitle: 12 },\n};\n\nexport const isCompactHeight = (height: number): boolean => height < COMPACT_HEIGHT_PX;\nexport const isCompactWidth = (width: number): boolean => width < COMPACT_WIDTH_PX;\n\n/** True when either dimension is below its compact threshold */\nexport function isCompactChart(width: number, height: number): boolean {\n return isCompactHeight(height) || isCompactWidth(width);\n}\n\n// Mixed scales are cached per `regular` so repeated calls return the same\n// object: charts list the scale in effect dependencies, and a fresh object\n// every render would re-plot every render.\nconst mixedScaleCache = new WeakMap<ChartScale, Map<\"short\" | \"narrow\", ChartScale>>();\n\n/**\n * Pick the scale for a canvas size. `regular` lets a chart keep its own\n * full-size values (e.g. BarChart's larger legend) while sharing the compact\n * fallback.\n *\n * Short and narrow are judged separately: a 1200×300 dashboard strip is short\n * but not narrow, so it gets compact fonts and top/bottom margins while keeping\n * the full left/right margins — vertical chrome follows height, horizontal\n * chrome follows width.\n */\nexport function resolveChartScale(\n width: number,\n height: number,\n regular: ChartScale = REGULAR_SCALE,\n): ChartScale {\n const short = isCompactHeight(height);\n const narrow = isCompactWidth(width);\n if (!short && !narrow) return regular;\n if (short && narrow) return COMPACT_SCALE;\n\n const key = short ? \"short\" : \"narrow\";\n let cache = mixedScaleCache.get(regular);\n if (!cache) {\n cache = new Map();\n mixedScaleCache.set(regular, cache);\n }\n let scale = cache.get(key);\n if (!scale) {\n const vertical = short ? COMPACT_SCALE : regular;\n const horizontal = narrow ? COMPACT_SCALE : regular;\n scale = {\n ...vertical,\n margin: { ...vertical.margin, l: horizontal.margin.l, r: horizontal.margin.r },\n };\n cache.set(key, scale);\n }\n return scale;\n}\n\n/**\n * Approximate vertical room one y-tick label needs (font size plus breathing\n * space) so labels never touch.\n */\nexport const Y_TICK_LABEL_SPACING = 2.2;\n\n/** How many labels fit along `extentPx` at `labelPx` each (never fewer than 2) */\nexport function maxTickCount(extentPx: number, labelPx: number): number {\n if (labelPx <= 0) return 2;\n return Math.max(2, Math.floor(extentPx / labelPx));\n}\n\n/**\n * Evenly drop ticks so at most `maxCount` remain, keeping both the first and\n * the last so the axis extremes stay labelled. Used to stop \"nice\" tick arrays\n * from stacking on top of each other when the plot area is short.\n */\nexport function thinTicks<T>(ticks: T[], maxCount: number): T[] {\n // Both ends are always kept, so fewer than two makes no sense\n const limit = Math.max(2, maxCount);\n if (ticks.length <= limit) return ticks;\n const stride = Math.ceil(ticks.length / limit);\n const kept = ticks.filter((_, index) => index % stride === 0);\n const last = ticks[ticks.length - 1];\n // Swap the final kept tick for the true maximum (count stays ≤ maxCount)\n return kept[kept.length - 1] === last ? kept : [...kept.slice(0, -1), last];\n}\n"],"names":["COMPACT_HEIGHT_PX","COMPACT_WIDTH_PX","REGULAR_SCALE","COMPACT_SCALE","isCompactHeight","height","isCompactWidth","width","mixedScaleCache","resolveChartScale","regular","short","narrow","key","cache","scale","vertical","horizontal","Y_TICK_LABEL_SPACING","maxTickCount","extentPx","labelPx","thinTicks","ticks","maxCount","limit","stride","kept","_","index","last"],"mappings":"gFAWO,MAAMA,EAAoB,IAEpBC,EAAmB,IAqBnBC,EAA4B,CACvC,aAAc,GACd,QAAS,GACT,kBAAmB,GACnB,kBAAmB,GACnB,eAAgB,GAChB,iBAAkB,GAClB,cAAe,GAGf,OAAQ,CAAE,EAAG,GAAI,EAAG,GAAI,EAAG,GAAI,OAAQ,GAAI,SAAU,EAAA,CACvD,EAEaC,EAA4B,CACvC,aAAc,GACd,QAAS,EACT,kBAAmB,GACnB,kBAAmB,EACnB,eAAgB,GAChB,iBAAkB,GAClB,cAAe,GACf,OAAQ,CAAE,EAAG,GAAI,EAAG,GAAI,EAAG,GAAI,OAAQ,GAAI,SAAU,EAAA,CACvD,EAEaC,EAAmBC,GAA4BA,EAASL,EACxDM,EAAkBC,GAA2BA,EAAQN,EAU5DO,MAAsB,QAYrB,SAASC,EACdF,EACAF,EACAK,EAAsBR,EACV,CACZ,MAAMS,EAAQP,EAAgBC,CAAM,EAC9BO,EAASN,EAAeC,CAAK,EACnC,GAAI,CAACI,GAAS,CAACC,EAAQ,OAAOF,EAC9B,GAAIC,GAASC,EAAQ,OAAOT,EAE5B,MAAMU,EAAMF,EAAQ,QAAU,SAC9B,IAAIG,EAAQN,EAAgB,IAAIE,CAAO,EAClCI,IACHA,MAAY,IACZN,EAAgB,IAAIE,EAASI,CAAK,GAEpC,IAAIC,EAAQD,EAAM,IAAID,CAAG,EACzB,GAAI,CAACE,EAAO,CACV,MAAMC,EAAWL,EAAQR,EAAgBO,EACnCO,EAAaL,EAAST,EAAgBO,EAC5CK,EAAQ,CACN,GAAGC,EACH,OAAQ,CAAE,GAAGA,EAAS,OAAQ,EAAGC,EAAW,OAAO,EAAG,EAAGA,EAAW,OAAO,CAAA,CAAE,EAE/EH,EAAM,IAAID,EAAKE,CAAK,CACtB,CACA,OAAOA,CACT,CAMO,MAAMG,EAAuB,IAG7B,SAASC,EAAaC,EAAkBC,EAAyB,CACtE,OAAIA,GAAW,EAAU,EAClB,KAAK,IAAI,EAAG,KAAK,MAAMD,EAAWC,CAAO,CAAC,CACnD,CAOO,SAASC,EAAaC,EAAYC,EAAuB,CAE9D,MAAMC,EAAQ,KAAK,IAAI,EAAGD,CAAQ,EAClC,GAAID,EAAM,QAAUE,EAAO,OAAOF,EAClC,MAAMG,EAAS,KAAK,KAAKH,EAAM,OAASE,CAAK,EACvCE,EAAOJ,EAAM,OAAO,CAACK,EAAGC,IAAUA,EAAQH,IAAW,CAAC,EACtDI,EAAOP,EAAMA,EAAM,OAAS,CAAC,EAEnC,OAAOI,EAAKA,EAAK,OAAS,CAAC,IAAMG,EAAOH,EAAO,CAAC,GAAGA,EAAK,MAAM,EAAG,EAAE,EAAGG,CAAI,CAC5E"}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared sizing rules for the cartesian charts (AreaPlot, BarChart, BoxPlot).
|
|
3
|
+
*
|
|
4
|
+
* Their chrome is tuned for dashboard-sized canvases: 16px tick labels, 80px
|
|
5
|
+
* side margins, a 32px title. Below a few hundred pixels that fixed chrome
|
|
6
|
+
* eats most of the canvas — y-tick labels pile onto each other and the legend
|
|
7
|
+
* grows a scrollbar (SW-2298). Charts switch to the compact scale here so the
|
|
8
|
+
* plot area stays legible in small tiles.
|
|
9
|
+
*/
|
|
10
|
+
/** Heights below this switch to the compact scale */
|
|
11
|
+
export declare const COMPACT_HEIGHT_PX = 360;
|
|
12
|
+
/** Widths below this switch to the compact scale */
|
|
13
|
+
export declare const COMPACT_WIDTH_PX = 480;
|
|
14
|
+
export interface ChartScale {
|
|
15
|
+
tickFontSize: number;
|
|
16
|
+
ticklen: number;
|
|
17
|
+
axisTitleFontSize: number;
|
|
18
|
+
axisTitleStandoff: number;
|
|
19
|
+
legendFontSize: number;
|
|
20
|
+
legendLineHeight: number;
|
|
21
|
+
titleFontSize: number;
|
|
22
|
+
margin: {
|
|
23
|
+
l: number;
|
|
24
|
+
r: number;
|
|
25
|
+
b: number;
|
|
26
|
+
/** Top margin when a title is rendered */
|
|
27
|
+
tTitle: number;
|
|
28
|
+
/** Top margin without a title */
|
|
29
|
+
tNoTitle: number;
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
export declare const REGULAR_SCALE: ChartScale;
|
|
33
|
+
export declare const COMPACT_SCALE: ChartScale;
|
|
34
|
+
export declare const isCompactHeight: (height: number) => boolean;
|
|
35
|
+
export declare const isCompactWidth: (width: number) => boolean;
|
|
36
|
+
/** True when either dimension is below its compact threshold */
|
|
37
|
+
export declare function isCompactChart(width: number, height: number): boolean;
|
|
38
|
+
/**
|
|
39
|
+
* Pick the scale for a canvas size. `regular` lets a chart keep its own
|
|
40
|
+
* full-size values (e.g. BarChart's larger legend) while sharing the compact
|
|
41
|
+
* fallback.
|
|
42
|
+
*
|
|
43
|
+
* Short and narrow are judged separately: a 1200×300 dashboard strip is short
|
|
44
|
+
* but not narrow, so it gets compact fonts and top/bottom margins while keeping
|
|
45
|
+
* the full left/right margins — vertical chrome follows height, horizontal
|
|
46
|
+
* chrome follows width.
|
|
47
|
+
*/
|
|
48
|
+
export declare function resolveChartScale(width: number, height: number, regular?: ChartScale): ChartScale;
|
|
49
|
+
/**
|
|
50
|
+
* Approximate vertical room one y-tick label needs (font size plus breathing
|
|
51
|
+
* space) so labels never touch.
|
|
52
|
+
*/
|
|
53
|
+
export declare const Y_TICK_LABEL_SPACING = 2.2;
|
|
54
|
+
/** How many labels fit along `extentPx` at `labelPx` each (never fewer than 2) */
|
|
55
|
+
export declare function maxTickCount(extentPx: number, labelPx: number): number;
|
|
56
|
+
/**
|
|
57
|
+
* Evenly drop ticks so at most `maxCount` remain, keeping both the first and
|
|
58
|
+
* the last so the axis extremes stay labelled. Used to stop "nice" tick arrays
|
|
59
|
+
* from stacking on top of each other when the plot area is short.
|
|
60
|
+
*/
|
|
61
|
+
export declare function thinTicks<T>(ticks: T[], maxCount: number): T[];
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
const S = 360, _ = 480, C = {
|
|
2
|
+
tickFontSize: 16,
|
|
3
|
+
ticklen: 12,
|
|
4
|
+
axisTitleFontSize: 16,
|
|
5
|
+
axisTitleStandoff: 15,
|
|
6
|
+
legendFontSize: 13,
|
|
7
|
+
legendLineHeight: 18,
|
|
8
|
+
titleFontSize: 32,
|
|
9
|
+
// Bottom margin reserves room for tick labels, the x-axis title, and the
|
|
10
|
+
// container-anchored bottom legend stacked beneath them (SW-2157).
|
|
11
|
+
margin: { l: 80, r: 40, b: 96, tTitle: 80, tNoTitle: 40 }
|
|
12
|
+
}, r = {
|
|
13
|
+
tickFontSize: 11,
|
|
14
|
+
ticklen: 6,
|
|
15
|
+
axisTitleFontSize: 12,
|
|
16
|
+
axisTitleStandoff: 8,
|
|
17
|
+
legendFontSize: 11,
|
|
18
|
+
legendLineHeight: 14,
|
|
19
|
+
titleFontSize: 18,
|
|
20
|
+
margin: { l: 48, r: 16, b: 76, tTitle: 36, tNoTitle: 12 }
|
|
21
|
+
}, f = (t) => t < 360, g = (t) => t < 480, h = /* @__PURE__ */ new WeakMap();
|
|
22
|
+
function m(t, i, n = C) {
|
|
23
|
+
const o = f(i), e = g(t);
|
|
24
|
+
if (!o && !e) return n;
|
|
25
|
+
if (o && e) return r;
|
|
26
|
+
const a = o ? "short" : "narrow";
|
|
27
|
+
let c = h.get(n);
|
|
28
|
+
c || (c = /* @__PURE__ */ new Map(), h.set(n, c));
|
|
29
|
+
let l = c.get(a);
|
|
30
|
+
if (!l) {
|
|
31
|
+
const s = o ? r : n, T = e ? r : n;
|
|
32
|
+
l = {
|
|
33
|
+
...s,
|
|
34
|
+
margin: { ...s.margin, l: T.margin.l, r: T.margin.r }
|
|
35
|
+
}, c.set(a, l);
|
|
36
|
+
}
|
|
37
|
+
return l;
|
|
38
|
+
}
|
|
39
|
+
const M = 2.2;
|
|
40
|
+
function A(t, i) {
|
|
41
|
+
return i <= 0 ? 2 : Math.max(2, Math.floor(t / i));
|
|
42
|
+
}
|
|
43
|
+
function d(t, i) {
|
|
44
|
+
const n = Math.max(2, i);
|
|
45
|
+
if (t.length <= n) return t;
|
|
46
|
+
const o = Math.ceil(t.length / n), e = t.filter((c, l) => l % o === 0), a = t[t.length - 1];
|
|
47
|
+
return e[e.length - 1] === a ? e : [...e.slice(0, -1), a];
|
|
48
|
+
}
|
|
49
|
+
export {
|
|
50
|
+
S as COMPACT_HEIGHT_PX,
|
|
51
|
+
r as COMPACT_SCALE,
|
|
52
|
+
_ as COMPACT_WIDTH_PX,
|
|
53
|
+
C as REGULAR_SCALE,
|
|
54
|
+
M as Y_TICK_LABEL_SPACING,
|
|
55
|
+
f as isCompactHeight,
|
|
56
|
+
g as isCompactWidth,
|
|
57
|
+
A as maxTickCount,
|
|
58
|
+
m as resolveChartScale,
|
|
59
|
+
d as thinTicks
|
|
60
|
+
};
|
|
61
|
+
//# sourceMappingURL=chart-scale.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"chart-scale.js","sources":["../../../src/components/charts/chart-scale.ts"],"sourcesContent":["/**\n * Shared sizing rules for the cartesian charts (AreaPlot, BarChart, BoxPlot).\n *\n * Their chrome is tuned for dashboard-sized canvases: 16px tick labels, 80px\n * side margins, a 32px title. Below a few hundred pixels that fixed chrome\n * eats most of the canvas — y-tick labels pile onto each other and the legend\n * grows a scrollbar (SW-2298). Charts switch to the compact scale here so the\n * plot area stays legible in small tiles.\n */\n\n/** Heights below this switch to the compact scale */\nexport const COMPACT_HEIGHT_PX = 360;\n/** Widths below this switch to the compact scale */\nexport const COMPACT_WIDTH_PX = 480;\n\nexport interface ChartScale {\n tickFontSize: number;\n ticklen: number;\n axisTitleFontSize: number;\n axisTitleStandoff: number;\n legendFontSize: number;\n legendLineHeight: number;\n titleFontSize: number;\n margin: {\n l: number;\n r: number;\n b: number;\n /** Top margin when a title is rendered */\n tTitle: number;\n /** Top margin without a title */\n tNoTitle: number;\n };\n}\n\nexport const REGULAR_SCALE: ChartScale = {\n tickFontSize: 16,\n ticklen: 12,\n axisTitleFontSize: 16,\n axisTitleStandoff: 15,\n legendFontSize: 13,\n legendLineHeight: 18,\n titleFontSize: 32,\n // Bottom margin reserves room for tick labels, the x-axis title, and the\n // container-anchored bottom legend stacked beneath them (SW-2157).\n margin: { l: 80, r: 40, b: 96, tTitle: 80, tNoTitle: 40 },\n};\n\nexport const COMPACT_SCALE: ChartScale = {\n tickFontSize: 11,\n ticklen: 6,\n axisTitleFontSize: 12,\n axisTitleStandoff: 8,\n legendFontSize: 11,\n legendLineHeight: 14,\n titleFontSize: 18,\n margin: { l: 48, r: 16, b: 76, tTitle: 36, tNoTitle: 12 },\n};\n\nexport const isCompactHeight = (height: number): boolean => height < COMPACT_HEIGHT_PX;\nexport const isCompactWidth = (width: number): boolean => width < COMPACT_WIDTH_PX;\n\n/** True when either dimension is below its compact threshold */\nexport function isCompactChart(width: number, height: number): boolean {\n return isCompactHeight(height) || isCompactWidth(width);\n}\n\n// Mixed scales are cached per `regular` so repeated calls return the same\n// object: charts list the scale in effect dependencies, and a fresh object\n// every render would re-plot every render.\nconst mixedScaleCache = new WeakMap<ChartScale, Map<\"short\" | \"narrow\", ChartScale>>();\n\n/**\n * Pick the scale for a canvas size. `regular` lets a chart keep its own\n * full-size values (e.g. BarChart's larger legend) while sharing the compact\n * fallback.\n *\n * Short and narrow are judged separately: a 1200×300 dashboard strip is short\n * but not narrow, so it gets compact fonts and top/bottom margins while keeping\n * the full left/right margins — vertical chrome follows height, horizontal\n * chrome follows width.\n */\nexport function resolveChartScale(\n width: number,\n height: number,\n regular: ChartScale = REGULAR_SCALE,\n): ChartScale {\n const short = isCompactHeight(height);\n const narrow = isCompactWidth(width);\n if (!short && !narrow) return regular;\n if (short && narrow) return COMPACT_SCALE;\n\n const key = short ? \"short\" : \"narrow\";\n let cache = mixedScaleCache.get(regular);\n if (!cache) {\n cache = new Map();\n mixedScaleCache.set(regular, cache);\n }\n let scale = cache.get(key);\n if (!scale) {\n const vertical = short ? COMPACT_SCALE : regular;\n const horizontal = narrow ? COMPACT_SCALE : regular;\n scale = {\n ...vertical,\n margin: { ...vertical.margin, l: horizontal.margin.l, r: horizontal.margin.r },\n };\n cache.set(key, scale);\n }\n return scale;\n}\n\n/**\n * Approximate vertical room one y-tick label needs (font size plus breathing\n * space) so labels never touch.\n */\nexport const Y_TICK_LABEL_SPACING = 2.2;\n\n/** How many labels fit along `extentPx` at `labelPx` each (never fewer than 2) */\nexport function maxTickCount(extentPx: number, labelPx: number): number {\n if (labelPx <= 0) return 2;\n return Math.max(2, Math.floor(extentPx / labelPx));\n}\n\n/**\n * Evenly drop ticks so at most `maxCount` remain, keeping both the first and\n * the last so the axis extremes stay labelled. Used to stop \"nice\" tick arrays\n * from stacking on top of each other when the plot area is short.\n */\nexport function thinTicks<T>(ticks: T[], maxCount: number): T[] {\n // Both ends are always kept, so fewer than two makes no sense\n const limit = Math.max(2, maxCount);\n if (ticks.length <= limit) return ticks;\n const stride = Math.ceil(ticks.length / limit);\n const kept = ticks.filter((_, index) => index % stride === 0);\n const last = ticks[ticks.length - 1];\n // Swap the final kept tick for the true maximum (count stays ≤ maxCount)\n return kept[kept.length - 1] === last ? kept : [...kept.slice(0, -1), last];\n}\n"],"names":["COMPACT_HEIGHT_PX","COMPACT_WIDTH_PX","REGULAR_SCALE","COMPACT_SCALE","isCompactHeight","height","isCompactWidth","width","mixedScaleCache","resolveChartScale","regular","short","narrow","key","cache","scale","vertical","horizontal","Y_TICK_LABEL_SPACING","maxTickCount","extentPx","labelPx","thinTicks","ticks","maxCount","limit","stride","kept","_","index","last"],"mappings":"AAWO,MAAMA,IAAoB,KAEpBC,IAAmB,KAqBnBC,IAA4B;AAAA,EACvC,cAAc;AAAA,EACd,SAAS;AAAA,EACT,mBAAmB;AAAA,EACnB,mBAAmB;AAAA,EACnB,gBAAgB;AAAA,EAChB,kBAAkB;AAAA,EAClB,eAAe;AAAA;AAAA;AAAA,EAGf,QAAQ,EAAE,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,QAAQ,IAAI,UAAU,GAAA;AACvD,GAEaC,IAA4B;AAAA,EACvC,cAAc;AAAA,EACd,SAAS;AAAA,EACT,mBAAmB;AAAA,EACnB,mBAAmB;AAAA,EACnB,gBAAgB;AAAA,EAChB,kBAAkB;AAAA,EAClB,eAAe;AAAA,EACf,QAAQ,EAAE,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,QAAQ,IAAI,UAAU,GAAA;AACvD,GAEaC,IAAkB,CAACC,MAA4BA,IAAS,KACxDC,IAAiB,CAACC,MAA2BA,IAAQ,KAU5DC,wBAAsB,QAAA;AAYrB,SAASC,EACdF,GACAF,GACAK,IAAsBR,GACV;AACZ,QAAMS,IAAQP,EAAgBC,CAAM,GAC9BO,IAASN,EAAeC,CAAK;AACnC,MAAI,CAACI,KAAS,CAACC,EAAQ,QAAOF;AAC9B,MAAIC,KAASC,EAAQ,QAAOT;AAE5B,QAAMU,IAAMF,IAAQ,UAAU;AAC9B,MAAIG,IAAQN,EAAgB,IAAIE,CAAO;AACvC,EAAKI,MACHA,wBAAY,IAAA,GACZN,EAAgB,IAAIE,GAASI,CAAK;AAEpC,MAAIC,IAAQD,EAAM,IAAID,CAAG;AACzB,MAAI,CAACE,GAAO;AACV,UAAMC,IAAWL,IAAQR,IAAgBO,GACnCO,IAAaL,IAAST,IAAgBO;AAC5C,IAAAK,IAAQ;AAAA,MACN,GAAGC;AAAA,MACH,QAAQ,EAAE,GAAGA,EAAS,QAAQ,GAAGC,EAAW,OAAO,GAAG,GAAGA,EAAW,OAAO,EAAA;AAAA,IAAE,GAE/EH,EAAM,IAAID,GAAKE,CAAK;AAAA,EACtB;AACA,SAAOA;AACT;AAMO,MAAMG,IAAuB;AAG7B,SAASC,EAAaC,GAAkBC,GAAyB;AACtE,SAAIA,KAAW,IAAU,IAClB,KAAK,IAAI,GAAG,KAAK,MAAMD,IAAWC,CAAO,CAAC;AACnD;AAOO,SAASC,EAAaC,GAAYC,GAAuB;AAE9D,QAAMC,IAAQ,KAAK,IAAI,GAAGD,CAAQ;AAClC,MAAID,EAAM,UAAUE,EAAO,QAAOF;AAClC,QAAMG,IAAS,KAAK,KAAKH,EAAM,SAASE,CAAK,GACvCE,IAAOJ,EAAM,OAAO,CAACK,GAAGC,MAAUA,IAAQH,MAAW,CAAC,GACtDI,IAAOP,EAAMA,EAAM,SAAS,CAAC;AAEnC,SAAOI,EAAKA,EAAK,SAAS,CAAC,MAAMG,IAAOH,IAAO,CAAC,GAAGA,EAAK,MAAM,GAAG,EAAE,GAAGG,CAAI;AAC5E;"}
|