import { extent } from 'd3-array'; import type { SVGAttributes } from 'react'; import { useEffect, useMemo } from 'react'; import { useLegend } from '../../contexts/legendContext.js'; import { usePlotContext, usePlotDispatchContext, } from '../../contexts/plotContext.js'; import { useIsSeriesVisible, useShift } from '../../hooks.js'; import type { BaseSeriesProps, CSSFuncProps, LabelFuncProps, SeriesPoint, ShapeFuncProps, } from '../../types.js'; import { functionalLabel, functionalShape, functionalStyle, middlePoint, useId, validateAxis, } from '../../utils.js'; import ErrorBars from '../ErrorBars.js'; import { markersMap } from '../Markers.map.js'; export interface ScatterSeriesProps extends BaseSeriesProps { data: readonly T[]; markerShape?: ShapeFuncProps; displayMarkers?: boolean; markerSize?: number; markerStyle?: CSSFuncProps; pointLabel?: LabelFuncProps; pointLabelStyle?: CSSFuncProps; displayErrorBars?: boolean; errorBarsStyle?: SVGAttributes; errorBarsCapStyle?: SVGAttributes; errorBarsCapSize?: number; lineStyle?: CSSFuncProps; displayLines?: boolean; } export function ScatterSeries( props: ScatterSeriesProps, ) { // Update plot context with data description const dispatch = usePlotDispatchContext(); const { colorScaler } = usePlotContext(); const [, legendDispatch] = useLegend(); const id = useId(props.id, 'series'); const { xAxis = 'x', yAxis = 'y', data, label, hidden, displayErrorBars = false, xShift: oldXShift = 0, yShift: oldYShift = 0, ...otherProps } = props; const { markerShape = 'circle', markerStyle = {}, errorBarsStyle, errorBarsCapStyle, errorBarsCapSize, } = otherProps; const isVisible = useIsSeriesVisible(id); const { xShift, yShift } = useShift({ xAxis, yAxis, xShift: oldXShift, yShift: oldYShift, }); const transform = `translate(${xShift},${yShift})`; useEffect(() => { if (!hidden) { legendDispatch({ type: 'ADD_LEGEND_LABEL', payload: { id, label, colorLine: 'white', shape: { color: markerStyle?.fill?.toString() || colorScaler(id), figure: typeof markerShape === 'string' ? markerShape : 'circle', hidden: false, }, }, }); return () => legendDispatch({ type: 'REMOVE_LEGEND_LABEL', payload: { id } }); } return undefined; }, [ colorScaler, hidden, id, label, legendDispatch, markerShape, markerStyle?.fill, ]); useEffect(() => { const [xMin, xMax] = extent(data, (d) => d.x) as [number, number]; const [yMin, yMax] = extent(data, (d) => d.y) as [number, number]; const x = { min: xMin, max: xMax, axisId: xAxis, shift: oldXShift }; const y = { min: yMin, max: yMax, axisId: yAxis, shift: oldYShift }; dispatch({ type: 'addSeries', payload: { id, x, y, label, data } }); // Delete information on unmount return () => dispatch({ type: 'removeSeries', payload: { id } }); }, [dispatch, id, data, xAxis, yAxis, label, oldXShift, oldYShift]); if (hidden) return null; // Render stateless plot component const inheritedProps = { data, xAxis, yAxis, }; const errorBarsProps = { hidden: !displayErrorBars, style: errorBarsStyle, capStyle: errorBarsCapStyle, capSize: errorBarsCapSize, transform, }; return isVisible ? ( ) : null; } interface ScatterSeriesRenderProps extends Omit, 'label'> { id: string; transform: string; xAxis: string; yAxis: string; } function ScatterSeriesRender({ id, data, xAxis, yAxis, markerShape = 'circle', markerSize = 8, markerStyle = {}, pointLabel = '', pointLabelStyle = {}, displayMarkers = true, lineStyle: pointLineStyle = {}, displayLines = false, transform, }: ScatterSeriesRenderProps) { // Get scales from context const { axisContext, colorScaler } = usePlotContext(); const [xScale, yScale] = validateAxis(axisContext, xAxis, yAxis); // calculates the path to display const markers = useMemo(() => { if (xScale === undefined || yScale === undefined) { return null; } const color = colorScaler(id); const defaultColor = { fill: color, stroke: color }; const markers = data.map((point, i) => { const style = functionalStyle(defaultColor, markerStyle, point, i, data); // Show marker const Marker = markersMap[functionalShape(markerShape, point, i, data)]; const label = functionalLabel(pointLabel, point, i, data); const labelStyle = functionalStyle({}, pointLabelStyle, point, i, data); const Lines = []; if (displayLines) { const lineStyle = functionalStyle({}, pointLineStyle, point, i, data); const prePoint = i > 0 ? middlePoint(point, data[i - 1]) : undefined; const nextPoint = data[i + 1] ? middlePoint(point, data[i + 1]) : undefined; const PreviousLine = prePoint ? ( ) : null; const NextLine = nextPoint ? ( ) : null; Lines.push(PreviousLine, NextLine); } return ( {Lines} {displayMarkers ? ( ) : null} {label ? {label} : null} ); }); return markers; }, [ xScale, yScale, colorScaler, id, data, markerStyle, markerShape, pointLabel, pointLabelStyle, pointLineStyle, displayLines, displayMarkers, markerSize, ]); if (!markers) return null; return ( {markers} ); }