@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":"Chromatogram.cjs","sources":["../../../../src/components/charts/Chromatogram/Chromatogram.tsx"],"sourcesContent":["import React, { useEffect, useMemo, useRef } from \"react\";\n\nimport { useChartTooltip } from \"../ChartTooltip\";\nimport { getLoadedPlotly, loadPlotly } from \"../plotly-loader\";\n\nimport {\n groupOverlappingPeaks,\n createGroupAnnotations,\n resolveSelectionAppearance,\n} from \"./annotations\";\nimport {\n validateSeriesData,\n applyBaselineCorrection,\n processUserAnnotations,\n} from \"./dataProcessing\";\nimport { detectPeaks } from \"./peakDetection\";\nimport {\n buildTraceData,\n buildLayout,\n buildConfig,\n createClickHandler,\n createHoverHandler,\n createUnhoverHandler,\n} from \"./plotBuilder\";\n\nimport type {\n ChromatogramSeries,\n PeakAnnotation,\n PeakSelectEvent,\n PeakSelectionAppearance,\n BaselineCorrectionMethod,\n BoundaryMarkerStyle,\n BoundaryMarkerType,\n PeakDetectionOptions,\n ChromatogramProps,\n PeakWithMeta,\n} from \"./types\";\nimport type Plotly from \"plotly.js-dist\";\n\nimport { EmptyState } from \"@/components/composed/EmptyState\";\nimport { usePlotlyTheme } from \"@/hooks/use-plotly-theme\";\n\n// Re-export types for external use\nexport type {\n ChromatogramSeries,\n PeakAnnotation,\n PeakSelectEvent,\n PeakSelectionAppearance,\n BaselineCorrectionMethod,\n BoundaryMarkerStyle,\n BoundaryMarkerType,\n PeakDetectionOptions,\n ChromatogramProps,\n};\n\n\n// Stable default so the no-annotations case keeps a constant identity; an\n// inline `[]` default would change every render, re-running the plot effect\n// (and tearing down the hover tooltip) on each re-render.\nconst EMPTY_ANNOTATIONS: PeakAnnotation[] = [];\n\nconst Chromatogram: React.FC<ChromatogramProps> = ({\n series,\n width = 900,\n height = 500,\n title,\n xAxisTitle = \"Retention Time (min)\",\n yAxisTitle = \"Signal (mAU)\",\n annotations = EMPTY_ANNOTATIONS,\n xRange,\n yRange,\n showLegend = true,\n showGridX = true,\n showGridY = true,\n showMarkers = false,\n markerSize = 4,\n showCrosshairs = false,\n baselineCorrection = \"none\",\n baselineWindowSize = 50,\n peakDetectionOptions,\n showPeakAreas = false,\n boundaryMarkers = \"none\",\n annotationOverlapThreshold = 0.4,\n showExportButton = true,\n selectedPeakIds,\n onPeakClick,\n onPeakHover,\n selectionAppearance,\n annotationStyle = \"arrow\",\n titleFontSize = 20,\n titleTopMargin,\n}) => {\n const enablePeakDetection = peakDetectionOptions !== undefined;\n const plotRef = useRef<HTMLDivElement>(null);\n const theme = usePlotlyTheme();\n const { bindTooltip, tooltipElement } = useChartTooltip({\n xLabel: xAxisTitle,\n yLabel: yAxisTitle,\n });\n // Stable refs for callbacks — avoids including them in effect dep arrays\n // (consumers often pass arrow functions that change identity every render).\n const onPeakClickRef = useRef(onPeakClick);\n const onPeakHoverRef = useRef(onPeakHover);\n onPeakClickRef.current = onPeakClick;\n onPeakHoverRef.current = onPeakHover;\n\n // Tracks the series index whose line is currently thickened on hover.\n const thickenedSeriesRef = useRef<number | null>(null);\n\n // Holds the latest peak Plotly annotations so that closures in effects always\n // read the latest selection-styled annotations.\n const peakAnnotationsRef = useRef<Partial<Plotly.Annotations>[]>([]);\n\n // Memoize processed series with baseline correction\n const processedSeries = useMemo(() => {\n return series.map((s) => {\n const validated = validateSeriesData(s.x, s.y);\n return {\n ...s,\n x: validated.x,\n y: applyBaselineCorrection(validated.y, baselineCorrection, baselineWindowSize),\n };\n });\n }, [series, baselineCorrection, baselineWindowSize]);\n\n // Process user annotations to convert startX/endX to indices and compute areas\n const processedAnnotations = useMemo(() => {\n if (annotations.length === 0 || processedSeries.length === 0) {\n return annotations;\n }\n const { x, y } = processedSeries[0];\n return processUserAnnotations(annotations, x, y);\n }, [annotations, processedSeries]);\n\n // Memoize peak detection results\n const allDetectedPeaks = useMemo(() => {\n const peaks: { peaks: PeakAnnotation[]; seriesIndex: number }[] = [];\n if (enablePeakDetection && peakDetectionOptions) {\n processedSeries.forEach((s, index) => {\n const detected = detectPeaks(s.x, s.y, peakDetectionOptions);\n if (detected.length > 0) {\n peaks.push({ peaks: detected, seriesIndex: index });\n }\n });\n }\n return peaks;\n }, [processedSeries, enablePeakDetection, peakDetectionOptions]);\n\n // Normalize the selection appearance into primitive fields up front so the\n // memo below depends on stable values rather than the (possibly unstable)\n // selectionAppearance object reference. This keeps the dependency array\n // exhaustive-deps clean without requiring callers to memoize the prop.\n const selectedBorderColor = selectionAppearance?.selected?.borderColor;\n const selectedBackgroundColor = selectionAppearance?.selected?.backgroundColor;\n const selectedBold = selectionAppearance?.selected?.bold;\n const unselectedOpacity = selectionAppearance?.unselected?.opacity;\n const hoverLineWidthMultiplier = selectionAppearance?.hoverLineWidthMultiplier;\n\n // Resolve selection appearance defaults once (stable as long as the\n // individual fields above don't change).\n const resolvedAppearance = useMemo(\n () =>\n resolveSelectionAppearance({\n selected: {\n borderColor: selectedBorderColor,\n backgroundColor: selectedBackgroundColor,\n bold: selectedBold,\n },\n unselected: { opacity: unselectedOpacity },\n hoverLineWidthMultiplier,\n }),\n [\n selectedBorderColor,\n selectedBackgroundColor,\n selectedBold,\n unselectedOpacity,\n hoverLineWidthMultiplier,\n ]\n );\n\n // All peaks that can be interacted with (clicked / hovered / selected),\n // each with a stable ID and the metadata needed for PeakSelectEvent.\n const allPeaksForInteraction = useMemo(() => {\n const result: Array<{\n peak: PeakAnnotation & { id: string };\n seriesIndex: number;\n seriesName: string;\n isAutoDetected: boolean;\n }> = [];\n\n processedAnnotations.forEach((ann, i) => {\n result.push({\n peak: { ...ann, id: ann.id ?? `user-ann-${i}` },\n seriesIndex: 0,\n seriesName: series[0]?.name ?? \"\",\n isAutoDetected: false,\n });\n });\n\n allDetectedPeaks.forEach(({ peaks, seriesIndex }) => {\n peaks.forEach((peak, peakIndex) => {\n result.push({\n peak: { ...peak, id: `peak-${seriesIndex}-${peakIndex}` },\n seriesIndex,\n seriesName: series[seriesIndex]?.name ?? `Series ${seriesIndex + 1}`,\n isAutoDetected: true,\n });\n });\n });\n\n return result;\n }, [processedAnnotations, allDetectedPeaks, series]);\n\n // Build Plotly annotation objects for all peaks, applying selection styling.\n // This memo re-runs when selectedPeakIds or resolvedAppearance changes so the\n // selection effect can call Plotly.relayout with updated annotations without\n // triggering a full chart rebuild.\n const peakAnnotations = useMemo(() => {\n const anySelected = (selectedPeakIds?.length ?? 0) > 0;\n const options = {\n selectedPeakIds: selectedPeakIds ?? [],\n anySelected,\n appearance: resolvedAppearance,\n annotationStyle,\n };\n\n const allPeaksWithMeta: PeakWithMeta[] = [];\n\n processedAnnotations.forEach((ann, i) => {\n allPeaksWithMeta.push({\n peak: { ...ann, id: ann.id ?? `user-ann-${i}` },\n seriesIndex: -1,\n });\n });\n\n if (showPeakAreas && enablePeakDetection) {\n allDetectedPeaks.forEach(({ peaks, seriesIndex }) => {\n peaks.forEach((peak, peakIndex) => {\n allPeaksWithMeta.push({\n peak: { ...peak, id: `peak-${seriesIndex}-${peakIndex}` },\n seriesIndex,\n });\n });\n });\n }\n\n const groups = groupOverlappingPeaks(allPeaksWithMeta, annotationOverlapThreshold);\n const result: Partial<Plotly.Annotations>[] = [];\n for (const group of groups) {\n result.push(...createGroupAnnotations(group, options));\n }\n return result;\n }, [\n processedAnnotations,\n allDetectedPeaks,\n showPeakAreas,\n enablePeakDetection,\n annotationOverlapThreshold,\n selectedPeakIds,\n resolvedAppearance,\n annotationStyle,\n ]);\n\n // Keep the ref in sync every render so that closures in effects always read\n // the latest selection-styled annotations.\n peakAnnotationsRef.current = peakAnnotations;\n\n // ── Main chart effect ──────────────────────────────────────────────────────\n // Rebuilds the full chart when structural props change (data, size, axes, …).\n // selectedPeakIds and selectionAppearance are intentionally excluded from\n // deps — selection changes are handled by the lightweight selection effect\n // below via Plotly.relayout, which preserves the user's current zoom/pan.\n useEffect(() => {\n const currentRef = plotRef.current;\n if (!currentRef || series.length === 0) return;\n\n // Build trace data (line traces + boundary markers + region overlays +\n // the invisible hit-area trace that carries peak customdata for\n // click/hover interaction).\n const plotData = buildTraceData({\n processedSeries,\n processedAnnotations,\n allDetectedPeaks,\n allPeaksForInteraction,\n showMarkers,\n markerSize,\n xAxisTitle,\n yAxisTitle,\n boundaryMarkers,\n });\n\n const layout = buildLayout({\n title,\n titleFontSize,\n titleTopMargin,\n width,\n height,\n xAxisTitle,\n yAxisTitle,\n xRange,\n yRange,\n showLegend,\n seriesCount: series.length,\n showGridX,\n showGridY,\n showCrosshairs,\n theme,\n peakAnnotations: peakAnnotationsRef.current,\n });\n\n const config = buildConfig({ showExportButton, width, height });\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 plotElement: HTMLDivElement | null = null;\n void loadPlotly().then((plotly) => {\n if (cancelled || !plotRef.current) return;\n plotly.newPlot(currentRef, plotData, layout, config);\n bindTooltip(currentRef);\n\n // ── Event: peak click ────────────────────────────────────────────────\n (currentRef as unknown as Plotly.PlotlyHTMLElement).on(\n \"plotly_click\",\n createClickHandler(onPeakClickRef)\n );\n\n (currentRef as unknown as Plotly.PlotlyHTMLElement).on(\n \"plotly_hover\",\n createHoverHandler(currentRef, processedSeries.length, thickenedSeriesRef, onPeakHoverRef, resolvedAppearance.hoverLineWidthMultiplier)\n );\n\n (currentRef as unknown as Plotly.PlotlyHTMLElement).on(\n \"plotly_unhover\",\n createUnhoverHandler(currentRef, thickenedSeriesRef, onPeakHoverRef)\n );\n\n // Capture ref value for cleanup\n plotElement = currentRef;\n });\n\n return () => {\n cancelled = true;\n thickenedSeriesRef.current = null;\n if (plotElement) getLoadedPlotly().purge(plotElement);\n };\n }, [\n processedSeries, allDetectedPeaks, allPeaksForInteraction, series.length,\n width, height, title, titleFontSize, titleTopMargin, xAxisTitle, yAxisTitle,\n processedAnnotations, xRange, yRange, showLegend, showGridX, showGridY,\n showMarkers, markerSize, showCrosshairs, enablePeakDetection, peakDetectionOptions,\n showPeakAreas, boundaryMarkers, annotationOverlapThreshold, showExportButton,\n theme, bindTooltip,\n // resolvedAppearance included so hover multiplier stays in sync with the\n // event handler closure without it being in a ref itself.\n resolvedAppearance,\n ]);\n\n // ── Selection update effect ────────────────────────────────────────────────\n // Runs when selectedPeakIds / selectionAppearance change (peakAnnotations\n // recomputes). Uses Plotly.relayout so the user's zoom/pan state is preserved.\n useEffect(() => {\n const el = plotRef.current;\n if (!el) return;\n // Guard: skip if the chart hasn't been initialized by the main effect yet.\n // (A truthy _fullLayout also proves newPlot ran, so the lazily-loaded\n // Plotly module is guaranteed to be available via getLoadedPlotly.)\n if (!(el as { _fullLayout?: unknown })._fullLayout) return;\n\n getLoadedPlotly().relayout(el, {\n annotations: peakAnnotations,\n } as unknown as Partial<Plotly.Layout>);\n }, [peakAnnotations]);\n\n // No data: render an explicit empty state instead of an uninitialized\n // (blank) Plotly container, so the chart reads as \"no data yet\" rather\n // than looking broken while data is still loading.\n if (series.length === 0) {\n return (\n <div\n className=\"chromatogram-chart-container relative flex items-center justify-center\"\n style={{ width, height }}\n >\n <EmptyState\n variant=\"no-data\"\n title=\"No chromatogram data\"\n description=\"There is no signal data to display yet.\"\n />\n </div>\n );\n }\n\n return (\n <div className=\"chromatogram-chart-container relative\">\n <div ref={plotRef} style={{ width: \"100%\", height: \"100%\" }} />\n {tooltipElement}\n </div>\n );\n};\n\nexport { Chromatogram };\n"],"names":["EMPTY_ANNOTATIONS","Chromatogram","series","width","height","title","xAxisTitle","yAxisTitle","annotations","xRange","yRange","showLegend","showGridX","showGridY","showMarkers","markerSize","showCrosshairs","baselineCorrection","baselineWindowSize","peakDetectionOptions","showPeakAreas","boundaryMarkers","annotationOverlapThreshold","showExportButton","selectedPeakIds","onPeakClick","onPeakHover","selectionAppearance","annotationStyle","titleFontSize","titleTopMargin","enablePeakDetection","plotRef","useRef","theme","usePlotlyTheme","bindTooltip","tooltipElement","useChartTooltip","onPeakClickRef","onPeakHoverRef","thickenedSeriesRef","peakAnnotationsRef","processedSeries","useMemo","s","validated","validateSeriesData","applyBaselineCorrection","processedAnnotations","x","y","processUserAnnotations","allDetectedPeaks","peaks","index","detected","detectPeaks","selectedBorderColor","selectedBackgroundColor","selectedBold","unselectedOpacity","hoverLineWidthMultiplier","resolvedAppearance","resolveSelectionAppearance","allPeaksForInteraction","result","ann","i","seriesIndex","peak","peakIndex","peakAnnotations","anySelected","options","allPeaksWithMeta","groups","groupOverlappingPeaks","group","createGroupAnnotations","useEffect","currentRef","plotData","buildTraceData","layout","buildLayout","config","buildConfig","cancelled","plotElement","loadPlotly","plotly","createClickHandler","createHoverHandler","createUnhoverHandler","getLoadedPlotly","el","jsx","EmptyState","jsxs"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"Chromatogram.cjs","sources":["../../../../src/components/charts/Chromatogram/Chromatogram.tsx"],"sourcesContent":["import React, { useEffect, useMemo, useRef } from \"react\";\n\nimport { useChartTooltip } from \"../ChartTooltip\";\nimport { getLoadedPlotly, loadPlotly } from \"../plotly-loader\";\n\nimport {\n groupOverlappingPeaks,\n createGroupAnnotations,\n resolveSelectionAppearance,\n} from \"./annotations\";\nimport {\n validateSeriesData,\n applyBaselineCorrection,\n processUserAnnotations,\n} from \"./dataProcessing\";\nimport { detectPeaks } from \"./peakDetection\";\nimport {\n buildTraceData,\n buildLayout,\n buildConfig,\n buildChromatogramTooltipLines,\n createClickHandler,\n createHoverHandler,\n createUnhoverHandler,\n} from \"./plotBuilder\";\n\nimport type {\n ChromatogramSeries,\n PeakAnnotation,\n PeakSelectEvent,\n PeakSelectionAppearance,\n BaselineCorrectionMethod,\n BoundaryMarkerStyle,\n BoundaryMarkerType,\n PeakDetectionOptions,\n ChromatogramProps,\n PeakWithMeta,\n} from \"./types\";\nimport type Plotly from \"plotly.js-dist\";\n\nimport { EmptyState } from \"@/components/composed/EmptyState\";\nimport { usePlotlyTheme } from \"@/hooks/use-plotly-theme\";\n\n// Re-export types for external use\nexport type {\n ChromatogramSeries,\n PeakAnnotation,\n PeakSelectEvent,\n PeakSelectionAppearance,\n BaselineCorrectionMethod,\n BoundaryMarkerStyle,\n BoundaryMarkerType,\n PeakDetectionOptions,\n ChromatogramProps,\n};\n\n\n// Stable default so the no-annotations case keeps a constant identity; an\n// inline `[]` default would change every render, re-running the plot effect\n// (and tearing down the hover tooltip) on each re-render.\nconst EMPTY_ANNOTATIONS: PeakAnnotation[] = [];\n\nconst Chromatogram: React.FC<ChromatogramProps> = ({\n series,\n width = 900,\n height = 500,\n title,\n xAxisTitle = \"Retention Time (min)\",\n yAxisTitle = \"Signal (mAU)\",\n annotations = EMPTY_ANNOTATIONS,\n xRange,\n yRange,\n showLegend = true,\n showGridX = true,\n showGridY = true,\n showMarkers = false,\n markerSize = 4,\n showCrosshairs = false,\n baselineCorrection = \"none\",\n baselineWindowSize = 50,\n peakDetectionOptions,\n showPeakAreas = false,\n boundaryMarkers = \"none\",\n annotationOverlapThreshold = 0.4,\n showExportButton = true,\n selectedPeakIds,\n onPeakClick,\n onPeakHover,\n selectionAppearance,\n annotationStyle = \"arrow\",\n titleFontSize = 20,\n titleTopMargin,\n}) => {\n const enablePeakDetection = peakDetectionOptions !== undefined;\n const plotRef = useRef<HTMLDivElement>(null);\n const theme = usePlotlyTheme();\n // Every trace sets hoverinfo \"none\", so this is the only tooltip on hover\n // (Plotly's unified label used to render alongside it — SW-2298). The lines\n // are built from the series points, their metadata, and any hovered peak.\n const { bindTooltip, tooltipElement } = useChartTooltip({\n getLines: (points) =>\n buildChromatogramTooltipLines(points, { series, xAxisTitle, yAxisTitle }),\n });\n // Stable refs for callbacks — avoids including them in effect dep arrays\n // (consumers often pass arrow functions that change identity every render).\n const onPeakClickRef = useRef(onPeakClick);\n const onPeakHoverRef = useRef(onPeakHover);\n onPeakClickRef.current = onPeakClick;\n onPeakHoverRef.current = onPeakHover;\n\n // Tracks the series index whose line is currently thickened on hover.\n const thickenedSeriesRef = useRef<number | null>(null);\n\n // Holds the latest peak Plotly annotations so that closures in effects always\n // read the latest selection-styled annotations.\n const peakAnnotationsRef = useRef<Partial<Plotly.Annotations>[]>([]);\n\n // Memoize processed series with baseline correction\n const processedSeries = useMemo(() => {\n return series.map((s) => {\n const validated = validateSeriesData(s.x, s.y);\n return {\n ...s,\n x: validated.x,\n y: applyBaselineCorrection(validated.y, baselineCorrection, baselineWindowSize),\n };\n });\n }, [series, baselineCorrection, baselineWindowSize]);\n\n // Process user annotations to convert startX/endX to indices and compute areas\n const processedAnnotations = useMemo(() => {\n if (annotations.length === 0 || processedSeries.length === 0) {\n return annotations;\n }\n const { x, y } = processedSeries[0];\n return processUserAnnotations(annotations, x, y);\n }, [annotations, processedSeries]);\n\n // Memoize peak detection results\n const allDetectedPeaks = useMemo(() => {\n const peaks: { peaks: PeakAnnotation[]; seriesIndex: number }[] = [];\n if (enablePeakDetection && peakDetectionOptions) {\n processedSeries.forEach((s, index) => {\n const detected = detectPeaks(s.x, s.y, peakDetectionOptions);\n if (detected.length > 0) {\n peaks.push({ peaks: detected, seriesIndex: index });\n }\n });\n }\n return peaks;\n }, [processedSeries, enablePeakDetection, peakDetectionOptions]);\n\n // Normalize the selection appearance into primitive fields up front so the\n // memo below depends on stable values rather than the (possibly unstable)\n // selectionAppearance object reference. This keeps the dependency array\n // exhaustive-deps clean without requiring callers to memoize the prop.\n const selectedBorderColor = selectionAppearance?.selected?.borderColor;\n const selectedBackgroundColor = selectionAppearance?.selected?.backgroundColor;\n const selectedBold = selectionAppearance?.selected?.bold;\n const unselectedOpacity = selectionAppearance?.unselected?.opacity;\n const hoverLineWidthMultiplier = selectionAppearance?.hoverLineWidthMultiplier;\n\n // Resolve selection appearance defaults once (stable as long as the\n // individual fields above don't change).\n const resolvedAppearance = useMemo(\n () =>\n resolveSelectionAppearance({\n selected: {\n borderColor: selectedBorderColor,\n backgroundColor: selectedBackgroundColor,\n bold: selectedBold,\n },\n unselected: { opacity: unselectedOpacity },\n hoverLineWidthMultiplier,\n }),\n [\n selectedBorderColor,\n selectedBackgroundColor,\n selectedBold,\n unselectedOpacity,\n hoverLineWidthMultiplier,\n ]\n );\n\n // All peaks that can be interacted with (clicked / hovered / selected),\n // each with a stable ID and the metadata needed for PeakSelectEvent.\n const allPeaksForInteraction = useMemo(() => {\n const result: Array<{\n peak: PeakAnnotation & { id: string };\n seriesIndex: number;\n seriesName: string;\n isAutoDetected: boolean;\n }> = [];\n\n processedAnnotations.forEach((ann, i) => {\n result.push({\n peak: { ...ann, id: ann.id ?? `user-ann-${i}` },\n seriesIndex: 0,\n seriesName: series[0]?.name ?? \"\",\n isAutoDetected: false,\n });\n });\n\n allDetectedPeaks.forEach(({ peaks, seriesIndex }) => {\n peaks.forEach((peak, peakIndex) => {\n result.push({\n peak: { ...peak, id: `peak-${seriesIndex}-${peakIndex}` },\n seriesIndex,\n seriesName: series[seriesIndex]?.name ?? `Series ${seriesIndex + 1}`,\n isAutoDetected: true,\n });\n });\n });\n\n return result;\n }, [processedAnnotations, allDetectedPeaks, series]);\n\n // Build Plotly annotation objects for all peaks, applying selection styling.\n // This memo re-runs when selectedPeakIds or resolvedAppearance changes so the\n // selection effect can call Plotly.relayout with updated annotations without\n // triggering a full chart rebuild.\n const peakAnnotations = useMemo(() => {\n const anySelected = (selectedPeakIds?.length ?? 0) > 0;\n const options = {\n selectedPeakIds: selectedPeakIds ?? [],\n anySelected,\n appearance: resolvedAppearance,\n annotationStyle,\n };\n\n const allPeaksWithMeta: PeakWithMeta[] = [];\n\n processedAnnotations.forEach((ann, i) => {\n allPeaksWithMeta.push({\n peak: { ...ann, id: ann.id ?? `user-ann-${i}` },\n seriesIndex: -1,\n });\n });\n\n if (showPeakAreas && enablePeakDetection) {\n allDetectedPeaks.forEach(({ peaks, seriesIndex }) => {\n peaks.forEach((peak, peakIndex) => {\n allPeaksWithMeta.push({\n peak: { ...peak, id: `peak-${seriesIndex}-${peakIndex}` },\n seriesIndex,\n });\n });\n });\n }\n\n const groups = groupOverlappingPeaks(allPeaksWithMeta, annotationOverlapThreshold);\n const result: Partial<Plotly.Annotations>[] = [];\n for (const group of groups) {\n result.push(...createGroupAnnotations(group, options));\n }\n return result;\n }, [\n processedAnnotations,\n allDetectedPeaks,\n showPeakAreas,\n enablePeakDetection,\n annotationOverlapThreshold,\n selectedPeakIds,\n resolvedAppearance,\n annotationStyle,\n ]);\n\n // Keep the ref in sync every render so that closures in effects always read\n // the latest selection-styled annotations.\n peakAnnotationsRef.current = peakAnnotations;\n\n // ── Main chart effect ──────────────────────────────────────────────────────\n // Rebuilds the full chart when structural props change (data, size, axes, …).\n // selectedPeakIds and selectionAppearance are intentionally excluded from\n // deps — selection changes are handled by the lightweight selection effect\n // below via Plotly.relayout, which preserves the user's current zoom/pan.\n useEffect(() => {\n const currentRef = plotRef.current;\n if (!currentRef || series.length === 0) return;\n\n // Build trace data (line traces + boundary markers + region overlays +\n // the invisible hit-area trace that carries peak customdata for\n // click/hover interaction).\n const plotData = buildTraceData({\n processedSeries,\n processedAnnotations,\n allDetectedPeaks,\n allPeaksForInteraction,\n showMarkers,\n markerSize,\n boundaryMarkers,\n });\n\n const layout = buildLayout({\n title,\n titleFontSize,\n titleTopMargin,\n width,\n height,\n xAxisTitle,\n yAxisTitle,\n xRange,\n yRange,\n showLegend,\n seriesCount: series.length,\n showGridX,\n showGridY,\n showCrosshairs,\n theme,\n peakAnnotations: peakAnnotationsRef.current,\n });\n\n const config = buildConfig({ showExportButton, width, height });\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 plotElement: HTMLDivElement | null = null;\n void loadPlotly().then((plotly) => {\n if (cancelled || !plotRef.current) return;\n plotly.newPlot(currentRef, plotData, layout, config);\n bindTooltip(currentRef);\n\n // ── Event: peak click ────────────────────────────────────────────────\n (currentRef as unknown as Plotly.PlotlyHTMLElement).on(\n \"plotly_click\",\n createClickHandler(onPeakClickRef)\n );\n\n (currentRef as unknown as Plotly.PlotlyHTMLElement).on(\n \"plotly_hover\",\n createHoverHandler(currentRef, processedSeries.length, thickenedSeriesRef, onPeakHoverRef, resolvedAppearance.hoverLineWidthMultiplier)\n );\n\n (currentRef as unknown as Plotly.PlotlyHTMLElement).on(\n \"plotly_unhover\",\n createUnhoverHandler(currentRef, thickenedSeriesRef, onPeakHoverRef)\n );\n\n // Capture ref value for cleanup\n plotElement = currentRef;\n });\n\n return () => {\n cancelled = true;\n thickenedSeriesRef.current = null;\n if (plotElement) getLoadedPlotly().purge(plotElement);\n };\n }, [\n processedSeries, allDetectedPeaks, allPeaksForInteraction, series.length,\n width, height, title, titleFontSize, titleTopMargin, xAxisTitle, yAxisTitle,\n processedAnnotations, xRange, yRange, showLegend, showGridX, showGridY,\n showMarkers, markerSize, showCrosshairs, enablePeakDetection, peakDetectionOptions,\n showPeakAreas, boundaryMarkers, annotationOverlapThreshold, showExportButton,\n theme, bindTooltip,\n // resolvedAppearance included so hover multiplier stays in sync with the\n // event handler closure without it being in a ref itself.\n resolvedAppearance,\n ]);\n\n // ── Selection update effect ────────────────────────────────────────────────\n // Runs when selectedPeakIds / selectionAppearance change (peakAnnotations\n // recomputes). Uses Plotly.relayout so the user's zoom/pan state is preserved.\n useEffect(() => {\n const el = plotRef.current;\n if (!el) return;\n // Guard: skip if the chart hasn't been initialized by the main effect yet.\n // (A truthy _fullLayout also proves newPlot ran, so the lazily-loaded\n // Plotly module is guaranteed to be available via getLoadedPlotly.)\n if (!(el as { _fullLayout?: unknown })._fullLayout) return;\n\n getLoadedPlotly().relayout(el, {\n annotations: peakAnnotations,\n } as unknown as Partial<Plotly.Layout>);\n }, [peakAnnotations]);\n\n // No data: render an explicit empty state instead of an uninitialized\n // (blank) Plotly container, so the chart reads as \"no data yet\" rather\n // than looking broken while data is still loading.\n if (series.length === 0) {\n return (\n <div\n className=\"chromatogram-chart-container relative flex items-center justify-center\"\n style={{ width, height }}\n >\n <EmptyState\n variant=\"no-data\"\n title=\"No chromatogram data\"\n description=\"There is no signal data to display yet.\"\n />\n </div>\n );\n }\n\n return (\n <div className=\"chromatogram-chart-container relative\">\n <div ref={plotRef} style={{ width: \"100%\", height: \"100%\" }} />\n {tooltipElement}\n </div>\n );\n};\n\nexport { Chromatogram };\n"],"names":["EMPTY_ANNOTATIONS","Chromatogram","series","width","height","title","xAxisTitle","yAxisTitle","annotations","xRange","yRange","showLegend","showGridX","showGridY","showMarkers","markerSize","showCrosshairs","baselineCorrection","baselineWindowSize","peakDetectionOptions","showPeakAreas","boundaryMarkers","annotationOverlapThreshold","showExportButton","selectedPeakIds","onPeakClick","onPeakHover","selectionAppearance","annotationStyle","titleFontSize","titleTopMargin","enablePeakDetection","plotRef","useRef","theme","usePlotlyTheme","bindTooltip","tooltipElement","useChartTooltip","points","buildChromatogramTooltipLines","onPeakClickRef","onPeakHoverRef","thickenedSeriesRef","peakAnnotationsRef","processedSeries","useMemo","s","validated","validateSeriesData","applyBaselineCorrection","processedAnnotations","x","y","processUserAnnotations","allDetectedPeaks","peaks","index","detected","detectPeaks","selectedBorderColor","selectedBackgroundColor","selectedBold","unselectedOpacity","hoverLineWidthMultiplier","resolvedAppearance","resolveSelectionAppearance","allPeaksForInteraction","result","ann","i","seriesIndex","peak","peakIndex","peakAnnotations","anySelected","options","allPeaksWithMeta","groups","groupOverlappingPeaks","group","createGroupAnnotations","useEffect","currentRef","plotData","buildTraceData","layout","buildLayout","config","buildConfig","cancelled","plotElement","loadPlotly","plotly","createClickHandler","createHoverHandler","createUnhoverHandler","getLoadedPlotly","el","jsx","EmptyState","jsxs"],"mappings":"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"}
|
|
@@ -1,69 +1,68 @@
|
|
|
1
1
|
import { jsx as _, jsxs as se } from "react/jsx-runtime";
|
|
2
|
-
import { useRef as h, useMemo as
|
|
2
|
+
import { useRef as h, useMemo as i, useEffect as ne } from "react";
|
|
3
3
|
import { useChartTooltip as de } from "../ChartTooltip/ChartTooltip.js";
|
|
4
|
-
import { loadPlotly as
|
|
5
|
-
import { resolveSelectionAppearance as
|
|
6
|
-
import { validateSeriesData as he, applyBaselineCorrection as
|
|
4
|
+
import { loadPlotly as ie, getLoadedPlotly as le } from "../plotly-loader.js";
|
|
5
|
+
import { resolveSelectionAppearance as fe, groupOverlappingPeaks as pe, createGroupAnnotations as me } from "./annotations.js";
|
|
6
|
+
import { validateSeriesData as he, applyBaselineCorrection as ge, processUserAnnotations as ye } from "./dataProcessing.js";
|
|
7
7
|
import { detectPeaks as ve } from "./peakDetection.js";
|
|
8
|
-
import {
|
|
9
|
-
import { EmptyState as
|
|
10
|
-
import { usePlotlyTheme as
|
|
11
|
-
const
|
|
12
|
-
series:
|
|
13
|
-
width:
|
|
14
|
-
height:
|
|
8
|
+
import { buildChromatogramTooltipLines as ke, buildTraceData as Ce, buildLayout as Ee, buildConfig as be, createClickHandler as Ne, createHoverHandler as Pe, createUnhoverHandler as Le } from "./plotBuilder.js";
|
|
9
|
+
import { EmptyState as $e } from "../../composed/EmptyState/EmptyState.js";
|
|
10
|
+
import { usePlotlyTheme as Re } from "../../../hooks/use-plotly-theme.js";
|
|
11
|
+
const Se = [], qe = ({
|
|
12
|
+
series: r,
|
|
13
|
+
width: g = 900,
|
|
14
|
+
height: y = 500,
|
|
15
15
|
title: H,
|
|
16
|
-
xAxisTitle:
|
|
17
|
-
yAxisTitle:
|
|
18
|
-
annotations:
|
|
16
|
+
xAxisTitle: E = "Retention Time (min)",
|
|
17
|
+
yAxisTitle: b = "Signal (mAU)",
|
|
18
|
+
annotations: v = Se,
|
|
19
19
|
xRange: j,
|
|
20
|
-
yRange:
|
|
21
|
-
showLegend:
|
|
22
|
-
showGridX:
|
|
23
|
-
showGridY:
|
|
24
|
-
showMarkers:
|
|
25
|
-
markerSize:
|
|
26
|
-
showCrosshairs:
|
|
27
|
-
baselineCorrection:
|
|
28
|
-
baselineWindowSize:
|
|
29
|
-
peakDetectionOptions:
|
|
20
|
+
yRange: A,
|
|
21
|
+
showLegend: B = !0,
|
|
22
|
+
showGridX: D = !0,
|
|
23
|
+
showGridY: M = !0,
|
|
24
|
+
showMarkers: T = !1,
|
|
25
|
+
markerSize: U = 4,
|
|
26
|
+
showCrosshairs: W = !1,
|
|
27
|
+
baselineCorrection: q = "none",
|
|
28
|
+
baselineWindowSize: x = 50,
|
|
29
|
+
peakDetectionOptions: f,
|
|
30
30
|
showPeakAreas: N = !1,
|
|
31
|
-
boundaryMarkers:
|
|
31
|
+
boundaryMarkers: F = "none",
|
|
32
32
|
annotationOverlapThreshold: P = 0.4,
|
|
33
|
-
showExportButton:
|
|
33
|
+
showExportButton: I = !0,
|
|
34
34
|
selectedPeakIds: L,
|
|
35
|
-
onPeakClick:
|
|
36
|
-
onPeakHover:
|
|
35
|
+
onPeakClick: J,
|
|
36
|
+
onPeakHover: K,
|
|
37
37
|
selectionAppearance: p,
|
|
38
|
-
annotationStyle:
|
|
39
|
-
titleFontSize:
|
|
40
|
-
titleTopMargin:
|
|
38
|
+
annotationStyle: Q = "arrow",
|
|
39
|
+
titleFontSize: V = 20,
|
|
40
|
+
titleTopMargin: Y
|
|
41
41
|
}) => {
|
|
42
|
-
const m =
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
const R = h(null), O = h([]), c = f(() => n.map((e) => {
|
|
42
|
+
const m = f !== void 0, k = h(null), Z = Re(), { bindTooltip: G, tooltipElement: ce } = de({
|
|
43
|
+
getLines: (e) => ke(e, { series: r, xAxisTitle: E, yAxisTitle: b })
|
|
44
|
+
}), X = h(J), $ = h(K);
|
|
45
|
+
X.current = J, $.current = K;
|
|
46
|
+
const R = h(null), O = h([]), l = i(() => r.map((e) => {
|
|
48
47
|
const t = he(e.x, e.y);
|
|
49
48
|
return {
|
|
50
49
|
...e,
|
|
51
50
|
x: t.x,
|
|
52
|
-
y:
|
|
51
|
+
y: ge(t.y, q, x)
|
|
53
52
|
};
|
|
54
|
-
}), [
|
|
55
|
-
if (
|
|
56
|
-
return
|
|
57
|
-
const { x: e, y: t } =
|
|
58
|
-
return
|
|
59
|
-
}, [
|
|
53
|
+
}), [r, q, x]), u = i(() => {
|
|
54
|
+
if (v.length === 0 || l.length === 0)
|
|
55
|
+
return v;
|
|
56
|
+
const { x: e, y: t } = l[0];
|
|
57
|
+
return ye(v, e, t);
|
|
58
|
+
}, [v, l]), s = i(() => {
|
|
60
59
|
const e = [];
|
|
61
|
-
return m &&
|
|
62
|
-
const
|
|
63
|
-
|
|
60
|
+
return m && f && l.forEach((t, o) => {
|
|
61
|
+
const c = ve(t.x, t.y, f);
|
|
62
|
+
c.length > 0 && e.push({ peaks: c, seriesIndex: o });
|
|
64
63
|
}), e;
|
|
65
|
-
}, [
|
|
66
|
-
() =>
|
|
64
|
+
}, [l, m, f]), z = p?.selected?.borderColor, w = p?.selected?.backgroundColor, ee = p?.selected?.bold, te = p?.unselected?.opacity, oe = p?.hoverLineWidthMultiplier, C = i(
|
|
65
|
+
() => fe({
|
|
67
66
|
selected: {
|
|
68
67
|
borderColor: z,
|
|
69
68
|
backgroundColor: w,
|
|
@@ -79,48 +78,48 @@ const Re = [], qe = ({
|
|
|
79
78
|
te,
|
|
80
79
|
oe
|
|
81
80
|
]
|
|
82
|
-
), re =
|
|
81
|
+
), re = i(() => {
|
|
83
82
|
const e = [];
|
|
84
83
|
return u.forEach((t, o) => {
|
|
85
84
|
e.push({
|
|
86
85
|
peak: { ...t, id: t.id ?? `user-ann-${o}` },
|
|
87
86
|
seriesIndex: 0,
|
|
88
|
-
seriesName:
|
|
87
|
+
seriesName: r[0]?.name ?? "",
|
|
89
88
|
isAutoDetected: !1
|
|
90
89
|
});
|
|
91
90
|
}), s.forEach(({ peaks: t, seriesIndex: o }) => {
|
|
92
|
-
t.forEach((
|
|
91
|
+
t.forEach((c, a) => {
|
|
93
92
|
e.push({
|
|
94
|
-
peak: { ...
|
|
93
|
+
peak: { ...c, id: `peak-${o}-${a}` },
|
|
95
94
|
seriesIndex: o,
|
|
96
|
-
seriesName:
|
|
95
|
+
seriesName: r[o]?.name ?? `Series ${o + 1}`,
|
|
97
96
|
isAutoDetected: !0
|
|
98
97
|
});
|
|
99
98
|
});
|
|
100
99
|
}), e;
|
|
101
|
-
}, [u, s,
|
|
100
|
+
}, [u, s, r]), S = i(() => {
|
|
102
101
|
const e = (L?.length ?? 0) > 0, t = {
|
|
103
102
|
selectedPeakIds: L ?? [],
|
|
104
103
|
anySelected: e,
|
|
105
104
|
appearance: C,
|
|
106
|
-
annotationStyle:
|
|
105
|
+
annotationStyle: Q
|
|
107
106
|
}, o = [];
|
|
108
|
-
u.forEach((
|
|
107
|
+
u.forEach((n, d) => {
|
|
109
108
|
o.push({
|
|
110
|
-
peak: { ...
|
|
109
|
+
peak: { ...n, id: n.id ?? `user-ann-${d}` },
|
|
111
110
|
seriesIndex: -1
|
|
112
111
|
});
|
|
113
|
-
}), N && m && s.forEach(({ peaks:
|
|
114
|
-
|
|
112
|
+
}), N && m && s.forEach(({ peaks: n, seriesIndex: d }) => {
|
|
113
|
+
n.forEach((ae, ue) => {
|
|
115
114
|
o.push({
|
|
116
115
|
peak: { ...ae, id: `peak-${d}-${ue}` },
|
|
117
116
|
seriesIndex: d
|
|
118
117
|
});
|
|
119
118
|
});
|
|
120
119
|
});
|
|
121
|
-
const
|
|
122
|
-
for (const
|
|
123
|
-
a.push(...me(
|
|
120
|
+
const c = pe(o, P), a = [];
|
|
121
|
+
for (const n of c)
|
|
122
|
+
a.push(...me(n, t));
|
|
124
123
|
return a;
|
|
125
124
|
}, [
|
|
126
125
|
u,
|
|
@@ -130,98 +129,96 @@ const Re = [], qe = ({
|
|
|
130
129
|
P,
|
|
131
130
|
L,
|
|
132
131
|
C,
|
|
133
|
-
|
|
132
|
+
Q
|
|
134
133
|
]);
|
|
135
134
|
return O.current = S, ne(() => {
|
|
136
|
-
const e =
|
|
137
|
-
if (!e ||
|
|
138
|
-
const t =
|
|
139
|
-
processedSeries:
|
|
135
|
+
const e = k.current;
|
|
136
|
+
if (!e || r.length === 0) return;
|
|
137
|
+
const t = Ce({
|
|
138
|
+
processedSeries: l,
|
|
140
139
|
processedAnnotations: u,
|
|
141
140
|
allDetectedPeaks: s,
|
|
142
141
|
allPeaksForInteraction: re,
|
|
143
|
-
showMarkers:
|
|
144
|
-
markerSize:
|
|
145
|
-
|
|
146
|
-
yAxisTitle: k,
|
|
147
|
-
boundaryMarkers: J
|
|
142
|
+
showMarkers: T,
|
|
143
|
+
markerSize: U,
|
|
144
|
+
boundaryMarkers: F
|
|
148
145
|
}), o = Ee({
|
|
149
146
|
title: H,
|
|
150
|
-
titleFontSize:
|
|
151
|
-
titleTopMargin:
|
|
152
|
-
width:
|
|
153
|
-
height:
|
|
154
|
-
xAxisTitle:
|
|
155
|
-
yAxisTitle:
|
|
147
|
+
titleFontSize: V,
|
|
148
|
+
titleTopMargin: Y,
|
|
149
|
+
width: g,
|
|
150
|
+
height: y,
|
|
151
|
+
xAxisTitle: E,
|
|
152
|
+
yAxisTitle: b,
|
|
156
153
|
xRange: j,
|
|
157
|
-
yRange:
|
|
158
|
-
showLegend:
|
|
159
|
-
seriesCount:
|
|
160
|
-
showGridX:
|
|
161
|
-
showGridY:
|
|
162
|
-
showCrosshairs:
|
|
163
|
-
theme:
|
|
154
|
+
yRange: A,
|
|
155
|
+
showLegend: B,
|
|
156
|
+
seriesCount: r.length,
|
|
157
|
+
showGridX: D,
|
|
158
|
+
showGridY: M,
|
|
159
|
+
showCrosshairs: W,
|
|
160
|
+
theme: Z,
|
|
164
161
|
peakAnnotations: O.current
|
|
165
|
-
}),
|
|
166
|
-
let a = !1,
|
|
167
|
-
return
|
|
168
|
-
a || !
|
|
162
|
+
}), c = be({ showExportButton: I, width: g, height: y });
|
|
163
|
+
let a = !1, n = null;
|
|
164
|
+
return ie().then((d) => {
|
|
165
|
+
a || !k.current || (d.newPlot(e, t, o, c), G(e), e.on(
|
|
169
166
|
"plotly_click",
|
|
170
|
-
|
|
167
|
+
Ne(X)
|
|
171
168
|
), e.on(
|
|
172
169
|
"plotly_hover",
|
|
173
|
-
|
|
170
|
+
Pe(e, l.length, R, $, C.hoverLineWidthMultiplier)
|
|
174
171
|
), e.on(
|
|
175
172
|
"plotly_unhover",
|
|
176
|
-
|
|
177
|
-
),
|
|
173
|
+
Le(e, R, $)
|
|
174
|
+
), n = e);
|
|
178
175
|
}), () => {
|
|
179
|
-
a = !0, R.current = null,
|
|
176
|
+
a = !0, R.current = null, n && le().purge(n);
|
|
180
177
|
};
|
|
181
178
|
}, [
|
|
182
|
-
|
|
179
|
+
l,
|
|
183
180
|
s,
|
|
184
181
|
re,
|
|
185
|
-
|
|
186
|
-
y,
|
|
182
|
+
r.length,
|
|
187
183
|
g,
|
|
184
|
+
y,
|
|
188
185
|
H,
|
|
186
|
+
V,
|
|
189
187
|
Y,
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
k,
|
|
188
|
+
E,
|
|
189
|
+
b,
|
|
193
190
|
u,
|
|
194
191
|
j,
|
|
192
|
+
A,
|
|
195
193
|
B,
|
|
196
194
|
D,
|
|
197
195
|
M,
|
|
196
|
+
T,
|
|
198
197
|
U,
|
|
199
198
|
W,
|
|
200
|
-
A,
|
|
201
|
-
q,
|
|
202
199
|
m,
|
|
203
|
-
|
|
200
|
+
f,
|
|
204
201
|
N,
|
|
205
|
-
|
|
202
|
+
F,
|
|
206
203
|
P,
|
|
207
|
-
|
|
204
|
+
I,
|
|
205
|
+
Z,
|
|
208
206
|
G,
|
|
209
|
-
X,
|
|
210
207
|
// resolvedAppearance included so hover multiplier stays in sync with the
|
|
211
208
|
// event handler closure without it being in a ref itself.
|
|
212
209
|
C
|
|
213
210
|
]), ne(() => {
|
|
214
|
-
const e =
|
|
215
|
-
e && e._fullLayout &&
|
|
211
|
+
const e = k.current;
|
|
212
|
+
e && e._fullLayout && le().relayout(e, {
|
|
216
213
|
annotations: S
|
|
217
214
|
});
|
|
218
|
-
}, [S]),
|
|
215
|
+
}, [S]), r.length === 0 ? /* @__PURE__ */ _(
|
|
219
216
|
"div",
|
|
220
217
|
{
|
|
221
218
|
className: "chromatogram-chart-container relative flex items-center justify-center",
|
|
222
|
-
style: { width:
|
|
219
|
+
style: { width: g, height: y },
|
|
223
220
|
children: /* @__PURE__ */ _(
|
|
224
|
-
|
|
221
|
+
$e,
|
|
225
222
|
{
|
|
226
223
|
variant: "no-data",
|
|
227
224
|
title: "No chromatogram data",
|
|
@@ -230,8 +227,8 @@ const Re = [], qe = ({
|
|
|
230
227
|
)
|
|
231
228
|
}
|
|
232
229
|
) : /* @__PURE__ */ se("div", { className: "chromatogram-chart-container relative", children: [
|
|
233
|
-
/* @__PURE__ */ _("div", { ref:
|
|
234
|
-
|
|
230
|
+
/* @__PURE__ */ _("div", { ref: k, style: { width: "100%", height: "100%" } }),
|
|
231
|
+
ce
|
|
235
232
|
] });
|
|
236
233
|
};
|
|
237
234
|
export {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Chromatogram.js","sources":["../../../../src/components/charts/Chromatogram/Chromatogram.tsx"],"sourcesContent":["import React, { useEffect, useMemo, useRef } from \"react\";\n\nimport { useChartTooltip } from \"../ChartTooltip\";\nimport { getLoadedPlotly, loadPlotly } from \"../plotly-loader\";\n\nimport {\n groupOverlappingPeaks,\n createGroupAnnotations,\n resolveSelectionAppearance,\n} from \"./annotations\";\nimport {\n validateSeriesData,\n applyBaselineCorrection,\n processUserAnnotations,\n} from \"./dataProcessing\";\nimport { detectPeaks } from \"./peakDetection\";\nimport {\n buildTraceData,\n buildLayout,\n buildConfig,\n createClickHandler,\n createHoverHandler,\n createUnhoverHandler,\n} from \"./plotBuilder\";\n\nimport type {\n ChromatogramSeries,\n PeakAnnotation,\n PeakSelectEvent,\n PeakSelectionAppearance,\n BaselineCorrectionMethod,\n BoundaryMarkerStyle,\n BoundaryMarkerType,\n PeakDetectionOptions,\n ChromatogramProps,\n PeakWithMeta,\n} from \"./types\";\nimport type Plotly from \"plotly.js-dist\";\n\nimport { EmptyState } from \"@/components/composed/EmptyState\";\nimport { usePlotlyTheme } from \"@/hooks/use-plotly-theme\";\n\n// Re-export types for external use\nexport type {\n ChromatogramSeries,\n PeakAnnotation,\n PeakSelectEvent,\n PeakSelectionAppearance,\n BaselineCorrectionMethod,\n BoundaryMarkerStyle,\n BoundaryMarkerType,\n PeakDetectionOptions,\n ChromatogramProps,\n};\n\n\n// Stable default so the no-annotations case keeps a constant identity; an\n// inline `[]` default would change every render, re-running the plot effect\n// (and tearing down the hover tooltip) on each re-render.\nconst EMPTY_ANNOTATIONS: PeakAnnotation[] = [];\n\nconst Chromatogram: React.FC<ChromatogramProps> = ({\n series,\n width = 900,\n height = 500,\n title,\n xAxisTitle = \"Retention Time (min)\",\n yAxisTitle = \"Signal (mAU)\",\n annotations = EMPTY_ANNOTATIONS,\n xRange,\n yRange,\n showLegend = true,\n showGridX = true,\n showGridY = true,\n showMarkers = false,\n markerSize = 4,\n showCrosshairs = false,\n baselineCorrection = \"none\",\n baselineWindowSize = 50,\n peakDetectionOptions,\n showPeakAreas = false,\n boundaryMarkers = \"none\",\n annotationOverlapThreshold = 0.4,\n showExportButton = true,\n selectedPeakIds,\n onPeakClick,\n onPeakHover,\n selectionAppearance,\n annotationStyle = \"arrow\",\n titleFontSize = 20,\n titleTopMargin,\n}) => {\n const enablePeakDetection = peakDetectionOptions !== undefined;\n const plotRef = useRef<HTMLDivElement>(null);\n const theme = usePlotlyTheme();\n const { bindTooltip, tooltipElement } = useChartTooltip({\n xLabel: xAxisTitle,\n yLabel: yAxisTitle,\n });\n // Stable refs for callbacks — avoids including them in effect dep arrays\n // (consumers often pass arrow functions that change identity every render).\n const onPeakClickRef = useRef(onPeakClick);\n const onPeakHoverRef = useRef(onPeakHover);\n onPeakClickRef.current = onPeakClick;\n onPeakHoverRef.current = onPeakHover;\n\n // Tracks the series index whose line is currently thickened on hover.\n const thickenedSeriesRef = useRef<number | null>(null);\n\n // Holds the latest peak Plotly annotations so that closures in effects always\n // read the latest selection-styled annotations.\n const peakAnnotationsRef = useRef<Partial<Plotly.Annotations>[]>([]);\n\n // Memoize processed series with baseline correction\n const processedSeries = useMemo(() => {\n return series.map((s) => {\n const validated = validateSeriesData(s.x, s.y);\n return {\n ...s,\n x: validated.x,\n y: applyBaselineCorrection(validated.y, baselineCorrection, baselineWindowSize),\n };\n });\n }, [series, baselineCorrection, baselineWindowSize]);\n\n // Process user annotations to convert startX/endX to indices and compute areas\n const processedAnnotations = useMemo(() => {\n if (annotations.length === 0 || processedSeries.length === 0) {\n return annotations;\n }\n const { x, y } = processedSeries[0];\n return processUserAnnotations(annotations, x, y);\n }, [annotations, processedSeries]);\n\n // Memoize peak detection results\n const allDetectedPeaks = useMemo(() => {\n const peaks: { peaks: PeakAnnotation[]; seriesIndex: number }[] = [];\n if (enablePeakDetection && peakDetectionOptions) {\n processedSeries.forEach((s, index) => {\n const detected = detectPeaks(s.x, s.y, peakDetectionOptions);\n if (detected.length > 0) {\n peaks.push({ peaks: detected, seriesIndex: index });\n }\n });\n }\n return peaks;\n }, [processedSeries, enablePeakDetection, peakDetectionOptions]);\n\n // Normalize the selection appearance into primitive fields up front so the\n // memo below depends on stable values rather than the (possibly unstable)\n // selectionAppearance object reference. This keeps the dependency array\n // exhaustive-deps clean without requiring callers to memoize the prop.\n const selectedBorderColor = selectionAppearance?.selected?.borderColor;\n const selectedBackgroundColor = selectionAppearance?.selected?.backgroundColor;\n const selectedBold = selectionAppearance?.selected?.bold;\n const unselectedOpacity = selectionAppearance?.unselected?.opacity;\n const hoverLineWidthMultiplier = selectionAppearance?.hoverLineWidthMultiplier;\n\n // Resolve selection appearance defaults once (stable as long as the\n // individual fields above don't change).\n const resolvedAppearance = useMemo(\n () =>\n resolveSelectionAppearance({\n selected: {\n borderColor: selectedBorderColor,\n backgroundColor: selectedBackgroundColor,\n bold: selectedBold,\n },\n unselected: { opacity: unselectedOpacity },\n hoverLineWidthMultiplier,\n }),\n [\n selectedBorderColor,\n selectedBackgroundColor,\n selectedBold,\n unselectedOpacity,\n hoverLineWidthMultiplier,\n ]\n );\n\n // All peaks that can be interacted with (clicked / hovered / selected),\n // each with a stable ID and the metadata needed for PeakSelectEvent.\n const allPeaksForInteraction = useMemo(() => {\n const result: Array<{\n peak: PeakAnnotation & { id: string };\n seriesIndex: number;\n seriesName: string;\n isAutoDetected: boolean;\n }> = [];\n\n processedAnnotations.forEach((ann, i) => {\n result.push({\n peak: { ...ann, id: ann.id ?? `user-ann-${i}` },\n seriesIndex: 0,\n seriesName: series[0]?.name ?? \"\",\n isAutoDetected: false,\n });\n });\n\n allDetectedPeaks.forEach(({ peaks, seriesIndex }) => {\n peaks.forEach((peak, peakIndex) => {\n result.push({\n peak: { ...peak, id: `peak-${seriesIndex}-${peakIndex}` },\n seriesIndex,\n seriesName: series[seriesIndex]?.name ?? `Series ${seriesIndex + 1}`,\n isAutoDetected: true,\n });\n });\n });\n\n return result;\n }, [processedAnnotations, allDetectedPeaks, series]);\n\n // Build Plotly annotation objects for all peaks, applying selection styling.\n // This memo re-runs when selectedPeakIds or resolvedAppearance changes so the\n // selection effect can call Plotly.relayout with updated annotations without\n // triggering a full chart rebuild.\n const peakAnnotations = useMemo(() => {\n const anySelected = (selectedPeakIds?.length ?? 0) > 0;\n const options = {\n selectedPeakIds: selectedPeakIds ?? [],\n anySelected,\n appearance: resolvedAppearance,\n annotationStyle,\n };\n\n const allPeaksWithMeta: PeakWithMeta[] = [];\n\n processedAnnotations.forEach((ann, i) => {\n allPeaksWithMeta.push({\n peak: { ...ann, id: ann.id ?? `user-ann-${i}` },\n seriesIndex: -1,\n });\n });\n\n if (showPeakAreas && enablePeakDetection) {\n allDetectedPeaks.forEach(({ peaks, seriesIndex }) => {\n peaks.forEach((peak, peakIndex) => {\n allPeaksWithMeta.push({\n peak: { ...peak, id: `peak-${seriesIndex}-${peakIndex}` },\n seriesIndex,\n });\n });\n });\n }\n\n const groups = groupOverlappingPeaks(allPeaksWithMeta, annotationOverlapThreshold);\n const result: Partial<Plotly.Annotations>[] = [];\n for (const group of groups) {\n result.push(...createGroupAnnotations(group, options));\n }\n return result;\n }, [\n processedAnnotations,\n allDetectedPeaks,\n showPeakAreas,\n enablePeakDetection,\n annotationOverlapThreshold,\n selectedPeakIds,\n resolvedAppearance,\n annotationStyle,\n ]);\n\n // Keep the ref in sync every render so that closures in effects always read\n // the latest selection-styled annotations.\n peakAnnotationsRef.current = peakAnnotations;\n\n // ── Main chart effect ──────────────────────────────────────────────────────\n // Rebuilds the full chart when structural props change (data, size, axes, …).\n // selectedPeakIds and selectionAppearance are intentionally excluded from\n // deps — selection changes are handled by the lightweight selection effect\n // below via Plotly.relayout, which preserves the user's current zoom/pan.\n useEffect(() => {\n const currentRef = plotRef.current;\n if (!currentRef || series.length === 0) return;\n\n // Build trace data (line traces + boundary markers + region overlays +\n // the invisible hit-area trace that carries peak customdata for\n // click/hover interaction).\n const plotData = buildTraceData({\n processedSeries,\n processedAnnotations,\n allDetectedPeaks,\n allPeaksForInteraction,\n showMarkers,\n markerSize,\n xAxisTitle,\n yAxisTitle,\n boundaryMarkers,\n });\n\n const layout = buildLayout({\n title,\n titleFontSize,\n titleTopMargin,\n width,\n height,\n xAxisTitle,\n yAxisTitle,\n xRange,\n yRange,\n showLegend,\n seriesCount: series.length,\n showGridX,\n showGridY,\n showCrosshairs,\n theme,\n peakAnnotations: peakAnnotationsRef.current,\n });\n\n const config = buildConfig({ showExportButton, width, height });\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 plotElement: HTMLDivElement | null = null;\n void loadPlotly().then((plotly) => {\n if (cancelled || !plotRef.current) return;\n plotly.newPlot(currentRef, plotData, layout, config);\n bindTooltip(currentRef);\n\n // ── Event: peak click ────────────────────────────────────────────────\n (currentRef as unknown as Plotly.PlotlyHTMLElement).on(\n \"plotly_click\",\n createClickHandler(onPeakClickRef)\n );\n\n (currentRef as unknown as Plotly.PlotlyHTMLElement).on(\n \"plotly_hover\",\n createHoverHandler(currentRef, processedSeries.length, thickenedSeriesRef, onPeakHoverRef, resolvedAppearance.hoverLineWidthMultiplier)\n );\n\n (currentRef as unknown as Plotly.PlotlyHTMLElement).on(\n \"plotly_unhover\",\n createUnhoverHandler(currentRef, thickenedSeriesRef, onPeakHoverRef)\n );\n\n // Capture ref value for cleanup\n plotElement = currentRef;\n });\n\n return () => {\n cancelled = true;\n thickenedSeriesRef.current = null;\n if (plotElement) getLoadedPlotly().purge(plotElement);\n };\n }, [\n processedSeries, allDetectedPeaks, allPeaksForInteraction, series.length,\n width, height, title, titleFontSize, titleTopMargin, xAxisTitle, yAxisTitle,\n processedAnnotations, xRange, yRange, showLegend, showGridX, showGridY,\n showMarkers, markerSize, showCrosshairs, enablePeakDetection, peakDetectionOptions,\n showPeakAreas, boundaryMarkers, annotationOverlapThreshold, showExportButton,\n theme, bindTooltip,\n // resolvedAppearance included so hover multiplier stays in sync with the\n // event handler closure without it being in a ref itself.\n resolvedAppearance,\n ]);\n\n // ── Selection update effect ────────────────────────────────────────────────\n // Runs when selectedPeakIds / selectionAppearance change (peakAnnotations\n // recomputes). Uses Plotly.relayout so the user's zoom/pan state is preserved.\n useEffect(() => {\n const el = plotRef.current;\n if (!el) return;\n // Guard: skip if the chart hasn't been initialized by the main effect yet.\n // (A truthy _fullLayout also proves newPlot ran, so the lazily-loaded\n // Plotly module is guaranteed to be available via getLoadedPlotly.)\n if (!(el as { _fullLayout?: unknown })._fullLayout) return;\n\n getLoadedPlotly().relayout(el, {\n annotations: peakAnnotations,\n } as unknown as Partial<Plotly.Layout>);\n }, [peakAnnotations]);\n\n // No data: render an explicit empty state instead of an uninitialized\n // (blank) Plotly container, so the chart reads as \"no data yet\" rather\n // than looking broken while data is still loading.\n if (series.length === 0) {\n return (\n <div\n className=\"chromatogram-chart-container relative flex items-center justify-center\"\n style={{ width, height }}\n >\n <EmptyState\n variant=\"no-data\"\n title=\"No chromatogram data\"\n description=\"There is no signal data to display yet.\"\n />\n </div>\n );\n }\n\n return (\n <div className=\"chromatogram-chart-container relative\">\n <div ref={plotRef} style={{ width: \"100%\", height: \"100%\" }} />\n {tooltipElement}\n </div>\n );\n};\n\nexport { Chromatogram };\n"],"names":["EMPTY_ANNOTATIONS","Chromatogram","series","width","height","title","xAxisTitle","yAxisTitle","annotations","xRange","yRange","showLegend","showGridX","showGridY","showMarkers","markerSize","showCrosshairs","baselineCorrection","baselineWindowSize","peakDetectionOptions","showPeakAreas","boundaryMarkers","annotationOverlapThreshold","showExportButton","selectedPeakIds","onPeakClick","onPeakHover","selectionAppearance","annotationStyle","titleFontSize","titleTopMargin","enablePeakDetection","plotRef","useRef","theme","usePlotlyTheme","bindTooltip","tooltipElement","useChartTooltip","onPeakClickRef","onPeakHoverRef","thickenedSeriesRef","peakAnnotationsRef","processedSeries","useMemo","s","validated","validateSeriesData","applyBaselineCorrection","processedAnnotations","x","y","processUserAnnotations","allDetectedPeaks","peaks","index","detected","detectPeaks","selectedBorderColor","selectedBackgroundColor","selectedBold","unselectedOpacity","hoverLineWidthMultiplier","resolvedAppearance","resolveSelectionAppearance","allPeaksForInteraction","result","ann","i","seriesIndex","peak","peakIndex","peakAnnotations","anySelected","options","allPeaksWithMeta","groups","groupOverlappingPeaks","group","createGroupAnnotations","useEffect","currentRef","plotData","buildTraceData","layout","buildLayout","config","buildConfig","cancelled","plotElement","loadPlotly","plotly","createClickHandler","createHoverHandler","createUnhoverHandler","getLoadedPlotly","el","jsx","EmptyState","jsxs"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"Chromatogram.js","sources":["../../../../src/components/charts/Chromatogram/Chromatogram.tsx"],"sourcesContent":["import React, { useEffect, useMemo, useRef } from \"react\";\n\nimport { useChartTooltip } from \"../ChartTooltip\";\nimport { getLoadedPlotly, loadPlotly } from \"../plotly-loader\";\n\nimport {\n groupOverlappingPeaks,\n createGroupAnnotations,\n resolveSelectionAppearance,\n} from \"./annotations\";\nimport {\n validateSeriesData,\n applyBaselineCorrection,\n processUserAnnotations,\n} from \"./dataProcessing\";\nimport { detectPeaks } from \"./peakDetection\";\nimport {\n buildTraceData,\n buildLayout,\n buildConfig,\n buildChromatogramTooltipLines,\n createClickHandler,\n createHoverHandler,\n createUnhoverHandler,\n} from \"./plotBuilder\";\n\nimport type {\n ChromatogramSeries,\n PeakAnnotation,\n PeakSelectEvent,\n PeakSelectionAppearance,\n BaselineCorrectionMethod,\n BoundaryMarkerStyle,\n BoundaryMarkerType,\n PeakDetectionOptions,\n ChromatogramProps,\n PeakWithMeta,\n} from \"./types\";\nimport type Plotly from \"plotly.js-dist\";\n\nimport { EmptyState } from \"@/components/composed/EmptyState\";\nimport { usePlotlyTheme } from \"@/hooks/use-plotly-theme\";\n\n// Re-export types for external use\nexport type {\n ChromatogramSeries,\n PeakAnnotation,\n PeakSelectEvent,\n PeakSelectionAppearance,\n BaselineCorrectionMethod,\n BoundaryMarkerStyle,\n BoundaryMarkerType,\n PeakDetectionOptions,\n ChromatogramProps,\n};\n\n\n// Stable default so the no-annotations case keeps a constant identity; an\n// inline `[]` default would change every render, re-running the plot effect\n// (and tearing down the hover tooltip) on each re-render.\nconst EMPTY_ANNOTATIONS: PeakAnnotation[] = [];\n\nconst Chromatogram: React.FC<ChromatogramProps> = ({\n series,\n width = 900,\n height = 500,\n title,\n xAxisTitle = \"Retention Time (min)\",\n yAxisTitle = \"Signal (mAU)\",\n annotations = EMPTY_ANNOTATIONS,\n xRange,\n yRange,\n showLegend = true,\n showGridX = true,\n showGridY = true,\n showMarkers = false,\n markerSize = 4,\n showCrosshairs = false,\n baselineCorrection = \"none\",\n baselineWindowSize = 50,\n peakDetectionOptions,\n showPeakAreas = false,\n boundaryMarkers = \"none\",\n annotationOverlapThreshold = 0.4,\n showExportButton = true,\n selectedPeakIds,\n onPeakClick,\n onPeakHover,\n selectionAppearance,\n annotationStyle = \"arrow\",\n titleFontSize = 20,\n titleTopMargin,\n}) => {\n const enablePeakDetection = peakDetectionOptions !== undefined;\n const plotRef = useRef<HTMLDivElement>(null);\n const theme = usePlotlyTheme();\n // Every trace sets hoverinfo \"none\", so this is the only tooltip on hover\n // (Plotly's unified label used to render alongside it — SW-2298). The lines\n // are built from the series points, their metadata, and any hovered peak.\n const { bindTooltip, tooltipElement } = useChartTooltip({\n getLines: (points) =>\n buildChromatogramTooltipLines(points, { series, xAxisTitle, yAxisTitle }),\n });\n // Stable refs for callbacks — avoids including them in effect dep arrays\n // (consumers often pass arrow functions that change identity every render).\n const onPeakClickRef = useRef(onPeakClick);\n const onPeakHoverRef = useRef(onPeakHover);\n onPeakClickRef.current = onPeakClick;\n onPeakHoverRef.current = onPeakHover;\n\n // Tracks the series index whose line is currently thickened on hover.\n const thickenedSeriesRef = useRef<number | null>(null);\n\n // Holds the latest peak Plotly annotations so that closures in effects always\n // read the latest selection-styled annotations.\n const peakAnnotationsRef = useRef<Partial<Plotly.Annotations>[]>([]);\n\n // Memoize processed series with baseline correction\n const processedSeries = useMemo(() => {\n return series.map((s) => {\n const validated = validateSeriesData(s.x, s.y);\n return {\n ...s,\n x: validated.x,\n y: applyBaselineCorrection(validated.y, baselineCorrection, baselineWindowSize),\n };\n });\n }, [series, baselineCorrection, baselineWindowSize]);\n\n // Process user annotations to convert startX/endX to indices and compute areas\n const processedAnnotations = useMemo(() => {\n if (annotations.length === 0 || processedSeries.length === 0) {\n return annotations;\n }\n const { x, y } = processedSeries[0];\n return processUserAnnotations(annotations, x, y);\n }, [annotations, processedSeries]);\n\n // Memoize peak detection results\n const allDetectedPeaks = useMemo(() => {\n const peaks: { peaks: PeakAnnotation[]; seriesIndex: number }[] = [];\n if (enablePeakDetection && peakDetectionOptions) {\n processedSeries.forEach((s, index) => {\n const detected = detectPeaks(s.x, s.y, peakDetectionOptions);\n if (detected.length > 0) {\n peaks.push({ peaks: detected, seriesIndex: index });\n }\n });\n }\n return peaks;\n }, [processedSeries, enablePeakDetection, peakDetectionOptions]);\n\n // Normalize the selection appearance into primitive fields up front so the\n // memo below depends on stable values rather than the (possibly unstable)\n // selectionAppearance object reference. This keeps the dependency array\n // exhaustive-deps clean without requiring callers to memoize the prop.\n const selectedBorderColor = selectionAppearance?.selected?.borderColor;\n const selectedBackgroundColor = selectionAppearance?.selected?.backgroundColor;\n const selectedBold = selectionAppearance?.selected?.bold;\n const unselectedOpacity = selectionAppearance?.unselected?.opacity;\n const hoverLineWidthMultiplier = selectionAppearance?.hoverLineWidthMultiplier;\n\n // Resolve selection appearance defaults once (stable as long as the\n // individual fields above don't change).\n const resolvedAppearance = useMemo(\n () =>\n resolveSelectionAppearance({\n selected: {\n borderColor: selectedBorderColor,\n backgroundColor: selectedBackgroundColor,\n bold: selectedBold,\n },\n unselected: { opacity: unselectedOpacity },\n hoverLineWidthMultiplier,\n }),\n [\n selectedBorderColor,\n selectedBackgroundColor,\n selectedBold,\n unselectedOpacity,\n hoverLineWidthMultiplier,\n ]\n );\n\n // All peaks that can be interacted with (clicked / hovered / selected),\n // each with a stable ID and the metadata needed for PeakSelectEvent.\n const allPeaksForInteraction = useMemo(() => {\n const result: Array<{\n peak: PeakAnnotation & { id: string };\n seriesIndex: number;\n seriesName: string;\n isAutoDetected: boolean;\n }> = [];\n\n processedAnnotations.forEach((ann, i) => {\n result.push({\n peak: { ...ann, id: ann.id ?? `user-ann-${i}` },\n seriesIndex: 0,\n seriesName: series[0]?.name ?? \"\",\n isAutoDetected: false,\n });\n });\n\n allDetectedPeaks.forEach(({ peaks, seriesIndex }) => {\n peaks.forEach((peak, peakIndex) => {\n result.push({\n peak: { ...peak, id: `peak-${seriesIndex}-${peakIndex}` },\n seriesIndex,\n seriesName: series[seriesIndex]?.name ?? `Series ${seriesIndex + 1}`,\n isAutoDetected: true,\n });\n });\n });\n\n return result;\n }, [processedAnnotations, allDetectedPeaks, series]);\n\n // Build Plotly annotation objects for all peaks, applying selection styling.\n // This memo re-runs when selectedPeakIds or resolvedAppearance changes so the\n // selection effect can call Plotly.relayout with updated annotations without\n // triggering a full chart rebuild.\n const peakAnnotations = useMemo(() => {\n const anySelected = (selectedPeakIds?.length ?? 0) > 0;\n const options = {\n selectedPeakIds: selectedPeakIds ?? [],\n anySelected,\n appearance: resolvedAppearance,\n annotationStyle,\n };\n\n const allPeaksWithMeta: PeakWithMeta[] = [];\n\n processedAnnotations.forEach((ann, i) => {\n allPeaksWithMeta.push({\n peak: { ...ann, id: ann.id ?? `user-ann-${i}` },\n seriesIndex: -1,\n });\n });\n\n if (showPeakAreas && enablePeakDetection) {\n allDetectedPeaks.forEach(({ peaks, seriesIndex }) => {\n peaks.forEach((peak, peakIndex) => {\n allPeaksWithMeta.push({\n peak: { ...peak, id: `peak-${seriesIndex}-${peakIndex}` },\n seriesIndex,\n });\n });\n });\n }\n\n const groups = groupOverlappingPeaks(allPeaksWithMeta, annotationOverlapThreshold);\n const result: Partial<Plotly.Annotations>[] = [];\n for (const group of groups) {\n result.push(...createGroupAnnotations(group, options));\n }\n return result;\n }, [\n processedAnnotations,\n allDetectedPeaks,\n showPeakAreas,\n enablePeakDetection,\n annotationOverlapThreshold,\n selectedPeakIds,\n resolvedAppearance,\n annotationStyle,\n ]);\n\n // Keep the ref in sync every render so that closures in effects always read\n // the latest selection-styled annotations.\n peakAnnotationsRef.current = peakAnnotations;\n\n // ── Main chart effect ──────────────────────────────────────────────────────\n // Rebuilds the full chart when structural props change (data, size, axes, …).\n // selectedPeakIds and selectionAppearance are intentionally excluded from\n // deps — selection changes are handled by the lightweight selection effect\n // below via Plotly.relayout, which preserves the user's current zoom/pan.\n useEffect(() => {\n const currentRef = plotRef.current;\n if (!currentRef || series.length === 0) return;\n\n // Build trace data (line traces + boundary markers + region overlays +\n // the invisible hit-area trace that carries peak customdata for\n // click/hover interaction).\n const plotData = buildTraceData({\n processedSeries,\n processedAnnotations,\n allDetectedPeaks,\n allPeaksForInteraction,\n showMarkers,\n markerSize,\n boundaryMarkers,\n });\n\n const layout = buildLayout({\n title,\n titleFontSize,\n titleTopMargin,\n width,\n height,\n xAxisTitle,\n yAxisTitle,\n xRange,\n yRange,\n showLegend,\n seriesCount: series.length,\n showGridX,\n showGridY,\n showCrosshairs,\n theme,\n peakAnnotations: peakAnnotationsRef.current,\n });\n\n const config = buildConfig({ showExportButton, width, height });\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 plotElement: HTMLDivElement | null = null;\n void loadPlotly().then((plotly) => {\n if (cancelled || !plotRef.current) return;\n plotly.newPlot(currentRef, plotData, layout, config);\n bindTooltip(currentRef);\n\n // ── Event: peak click ────────────────────────────────────────────────\n (currentRef as unknown as Plotly.PlotlyHTMLElement).on(\n \"plotly_click\",\n createClickHandler(onPeakClickRef)\n );\n\n (currentRef as unknown as Plotly.PlotlyHTMLElement).on(\n \"plotly_hover\",\n createHoverHandler(currentRef, processedSeries.length, thickenedSeriesRef, onPeakHoverRef, resolvedAppearance.hoverLineWidthMultiplier)\n );\n\n (currentRef as unknown as Plotly.PlotlyHTMLElement).on(\n \"plotly_unhover\",\n createUnhoverHandler(currentRef, thickenedSeriesRef, onPeakHoverRef)\n );\n\n // Capture ref value for cleanup\n plotElement = currentRef;\n });\n\n return () => {\n cancelled = true;\n thickenedSeriesRef.current = null;\n if (plotElement) getLoadedPlotly().purge(plotElement);\n };\n }, [\n processedSeries, allDetectedPeaks, allPeaksForInteraction, series.length,\n width, height, title, titleFontSize, titleTopMargin, xAxisTitle, yAxisTitle,\n processedAnnotations, xRange, yRange, showLegend, showGridX, showGridY,\n showMarkers, markerSize, showCrosshairs, enablePeakDetection, peakDetectionOptions,\n showPeakAreas, boundaryMarkers, annotationOverlapThreshold, showExportButton,\n theme, bindTooltip,\n // resolvedAppearance included so hover multiplier stays in sync with the\n // event handler closure without it being in a ref itself.\n resolvedAppearance,\n ]);\n\n // ── Selection update effect ────────────────────────────────────────────────\n // Runs when selectedPeakIds / selectionAppearance change (peakAnnotations\n // recomputes). Uses Plotly.relayout so the user's zoom/pan state is preserved.\n useEffect(() => {\n const el = plotRef.current;\n if (!el) return;\n // Guard: skip if the chart hasn't been initialized by the main effect yet.\n // (A truthy _fullLayout also proves newPlot ran, so the lazily-loaded\n // Plotly module is guaranteed to be available via getLoadedPlotly.)\n if (!(el as { _fullLayout?: unknown })._fullLayout) return;\n\n getLoadedPlotly().relayout(el, {\n annotations: peakAnnotations,\n } as unknown as Partial<Plotly.Layout>);\n }, [peakAnnotations]);\n\n // No data: render an explicit empty state instead of an uninitialized\n // (blank) Plotly container, so the chart reads as \"no data yet\" rather\n // than looking broken while data is still loading.\n if (series.length === 0) {\n return (\n <div\n className=\"chromatogram-chart-container relative flex items-center justify-center\"\n style={{ width, height }}\n >\n <EmptyState\n variant=\"no-data\"\n title=\"No chromatogram data\"\n description=\"There is no signal data to display yet.\"\n />\n </div>\n );\n }\n\n return (\n <div className=\"chromatogram-chart-container relative\">\n <div ref={plotRef} style={{ width: \"100%\", height: \"100%\" }} />\n {tooltipElement}\n </div>\n );\n};\n\nexport { Chromatogram };\n"],"names":["EMPTY_ANNOTATIONS","Chromatogram","series","width","height","title","xAxisTitle","yAxisTitle","annotations","xRange","yRange","showLegend","showGridX","showGridY","showMarkers","markerSize","showCrosshairs","baselineCorrection","baselineWindowSize","peakDetectionOptions","showPeakAreas","boundaryMarkers","annotationOverlapThreshold","showExportButton","selectedPeakIds","onPeakClick","onPeakHover","selectionAppearance","annotationStyle","titleFontSize","titleTopMargin","enablePeakDetection","plotRef","useRef","theme","usePlotlyTheme","bindTooltip","tooltipElement","useChartTooltip","points","buildChromatogramTooltipLines","onPeakClickRef","onPeakHoverRef","thickenedSeriesRef","peakAnnotationsRef","processedSeries","useMemo","s","validated","validateSeriesData","applyBaselineCorrection","processedAnnotations","x","y","processUserAnnotations","allDetectedPeaks","peaks","index","detected","detectPeaks","selectedBorderColor","selectedBackgroundColor","selectedBold","unselectedOpacity","hoverLineWidthMultiplier","resolvedAppearance","resolveSelectionAppearance","allPeaksForInteraction","result","ann","i","seriesIndex","peak","peakIndex","peakAnnotations","anySelected","options","allPeaksWithMeta","groups","groupOverlappingPeaks","group","createGroupAnnotations","useEffect","currentRef","plotData","buildTraceData","layout","buildLayout","config","buildConfig","cancelled","plotElement","loadPlotly","plotly","createClickHandler","createHoverHandler","createUnhoverHandler","getLoadedPlotly","el","jsx","EmptyState","jsxs"],"mappings":"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"}
|
|
@@ -1,2 +1,5 @@
|
|
|
1
|
-
"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const
|
|
1
|
+
"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const _=require("../../../utils/colors.cjs"),g=require("../plotly-loader.cjs"),L=require("./boundaryMarkers.cjs"),d=require("./constants.cjs"),x=require("./dataProcessing.cjs"),C=require("./regionOverlays.cjs"),m=require("../../../hooks/use-plotly-theme.cjs");function H(t){const{processedSeries:e,processedAnnotations:o,allDetectedPeaks:i,allPeaksForInteraction:a,showMarkers:c,markerSize:p,boundaryMarkers:u}=t,s=e.map((n,r)=>{const T=n.color||_.CHART_COLORS[r%_.CHART_COLORS.length],f={x:n.x,y:n.y,type:"scatter",mode:c?"lines+markers":"lines",name:n.name,line:{color:T,width:d.CHROMATOGRAM_TRACE.BASE_LINE_WIDTH},hoverinfo:"none"};return c&&(f.marker={size:p,color:T}),f});if(u!=="none"){const n=x.collectPeaksWithBoundaryData(i,o,e);n.length>0&&s.push(...L.createBoundaryMarkerTraces(n))}if(o.forEach(n=>{n.regionOverlay&&e[0]&&s.push(...C.createRegionOverlayTraces([n],0,e[0]))}),i.forEach(({peaks:n,seriesIndex:r})=>{n.some(T=>T.regionOverlay)&&e[r]&&s.push(...C.createRegionOverlayTraces(n,r,e[r]))}),a.length>0){const n={x:a.map(r=>r.peak.x),y:a.map(r=>r.peak.y),type:"scatter",mode:"markers",marker:{size:14,opacity:0},showlegend:!1,name:"",customdata:a.map(r=>({id:r.peak.id,peak:r.peak,seriesIndex:r.seriesIndex,seriesName:r.seriesName,isAutoDetected:r.isAutoDetected})),hoverinfo:"none"};s.push(n)}return s}const M=t=>typeof t=="number"?t.toFixed(2):String(t);function y(t){if(!t)return[];const e=t.replace(/<br\s*\/?>/gi,`
|
|
2
|
+
`);return(typeof DOMParser>"u"?e:new DOMParser().parseFromString(e,"text/html").body.textContent??"").split(`
|
|
3
|
+
`).map(i=>i.trim()).filter(Boolean)}function O(t){const e=t.trim(),o={label:e,unit:""};if(!e.endsWith(")"))return o;const i=e.lastIndexOf("(");if(i<=0)return o;const a=e.slice(i+1,-1).trim(),c=e.slice(0,i).trim();return!a||!c||a.includes("(")||a.includes(")")?o:{label:c,unit:a}}const k=(t,e)=>e?`${t} ${e}`:t;function b(t,e,o){const i=x.buildHoverExtraContent(t.name,t.metadata).split("<br>").slice(1);return[`${t.name}: ${k(M(e),o)}`,...i]}function I(t){const e=t.customdata?.peak;return e?y(e.hoverText??e.text):typeof t.text=="string"?y(t.text):[]}function N(t,e){const{series:o,xAxisTitle:i,yAxisTitle:a}=e,c=O(i),{unit:p}=O(a),u=[],s=t.find(r=>r.x!==void 0);s?.x!==void 0&&u.push(`${c.label}: ${k(M(s.x),c.unit)}`);const n=new Set;for(const r of t){const T=r.curveNumber===void 0?void 0:o[r.curveNumber];if(T){r.y!==void 0&&u.push(...b(T,r.y,p));continue}const f=I(r),A=f.join(`
|
|
4
|
+
`);f.length>0&&!n.has(A)&&(n.add(A),u.push(...f))}return u}function w(t){const{title:e,titleFontSize:o,titleTopMargin:i,width:a,height:c,xAxisTitle:p,yAxisTitle:u,xRange:s,yRange:n,showLegend:r,seriesCount:T,showGridX:f,showGridY:A,showCrosshairs:h,theme:l,peakAnnotations:R}=t;return{title:e?{text:e,font:{size:o,family:m.CHART_FONT_FAMILY,color:l.textColor}}:void 0,width:a,height:c,margin:{l:d.CHROMATOGRAM_LAYOUT.MARGIN_LEFT,r:d.CHROMATOGRAM_LAYOUT.MARGIN_RIGHT,b:d.CHROMATOGRAM_LAYOUT.MARGIN_BOTTOM,t:e?i??d.CHROMATOGRAM_LAYOUT.MARGIN_TOP_WITH_TITLE:d.CHROMATOGRAM_LAYOUT.MARGIN_TOP_NO_TITLE,pad:d.CHROMATOGRAM_LAYOUT.MARGIN_PAD},paper_bgcolor:l.paperBg,plot_bgcolor:l.plotBg,font:{family:m.CHART_FONT_FAMILY},hovermode:h?"x":"x unified",dragmode:"zoom",xaxis:{title:{text:p,font:{size:14,color:l.textSecondary,family:m.CHART_FONT_FAMILY},standoff:15},showgrid:f,gridcolor:l.gridColor,linecolor:l.lineColor,linewidth:1,range:s,autorange:!s,zeroline:!1,tickfont:{size:12,color:l.textColor,family:m.CHART_FONT_FAMILY},showspikes:h,spikemode:"across",spikesnap:"cursor",spikecolor:l.spikeColor,spikethickness:1,spikedash:"dot"},yaxis:{title:{text:u,font:{size:14,color:l.textSecondary,family:m.CHART_FONT_FAMILY},standoff:10},showgrid:A,gridcolor:l.gridColor,linecolor:l.lineColor,linewidth:1,range:n,autorange:!n,zeroline:!1,tickfont:{size:12,color:l.textColor,family:m.CHART_FONT_FAMILY},showspikes:h,spikemode:"across",spikesnap:"cursor",spikecolor:l.spikeColor,spikethickness:1,spikedash:"dot"},legend:{x:.5,y:-.15,xanchor:"center",yanchor:"top",orientation:"h",font:{size:12,color:l.textColor,family:m.CHART_FONT_FAMILY}},showlegend:r&&T>1,annotations:R}}function v(t){const{showExportButton:e,width:o,height:i}=t;return{responsive:!0,displayModeBar:!0,displaylogo:!1,modeBarButtonsToRemove:["lasso2d","select2d",...e?[]:["toImage"]],...e&&{toImageButtonOptions:{format:"png",filename:"chromatogram",width:o,height:i}}}}function F(t,e,o,i,a){return c=>{const p=c.points[0];if(p&&p.curveNumber<e){const s=p.curveNumber;o.current!==s&&(o.current!==null&&g.getLoadedPlotly().restyle(t,{"line.width":d.CHROMATOGRAM_TRACE.BASE_LINE_WIDTH},[o.current]),g.getLoadedPlotly().restyle(t,{"line.width":d.CHROMATOGRAM_TRACE.BASE_LINE_WIDTH*a},[s]),o.current=s)}const u=c.points.find(s=>s.customdata!=null);u&&i.current?.(u.customdata)}}function G(t){return e=>{if(!t.current)return;const o=e.points.find(i=>i.customdata!=null);o&&t.current(o.customdata)}}function B(t,e,o){return()=>{o.current?.(null),e.current!==null&&(g.getLoadedPlotly().restyle(t,{"line.width":d.CHROMATOGRAM_TRACE.BASE_LINE_WIDTH},[e.current]),e.current=null)}}exports.buildChromatogramTooltipLines=N;exports.buildConfig=v;exports.buildLayout=w;exports.buildTraceData=H;exports.createClickHandler=G;exports.createHoverHandler=F;exports.createUnhoverHandler=B;exports.htmlToTooltipLines=y;exports.splitAxisTitle=O;
|
|
2
5
|
//# sourceMappingURL=plotBuilder.cjs.map
|