/* eslint-disable react/no-array-index-key */ import type { CSSProperties } from 'react'; import { useContext, useEffect, useMemo } from 'react'; import type { AlignGroupProps } from 'react-d3-utils'; import { AlignGroup } from 'react-d3-utils'; import { useLegend } from '../contexts/legendContext.js'; import { legendOffsetContext } from '../contexts/legendOffsetContext.js'; import { usePlotContext, usePlotDispatchContext, } from '../contexts/plotContext.js'; import type { CSSFuncProps, Position } from '../types.js'; import { functionalStyle } from '../utils.js'; import { markersMap } from './Markers.map.js'; type Positions = Partial>; interface ValidatedPosition { key?: Position; value?: number; } function exclusiveProps( offsets: Positions, a: keyof Positions, b: keyof Positions, position: string, ): ValidatedPosition { if (offsets[a] !== undefined) { if (offsets[b] !== undefined) { throw new Error( `${a} and ${b} should't be both defined for the position ${position}`, ); } return { key: a, value: offsets[a] }; } return offsets[b] !== undefined ? { key: b, value: offsets[b] } : {}; } function translation( position: LegendPosition, legendOffsets: Positions, plotWidth: number, plotHeight: number, legendOffset: number, ): Omit { switch (position) { case 'embedded': { const { key: verticalKey = 'top', value: verticalValue = 10 } = exclusiveProps(legendOffsets, 'top', 'bottom', position); const { key: horizontalKey = 'left', value: horizontalValue = 10 } = exclusiveProps(legendOffsets, 'left', 'right', position); const x = horizontalKey === 'right' ? plotWidth - horizontalValue : horizontalValue; const y = verticalKey === 'bottom' ? plotHeight - verticalValue : verticalValue; return { x, y, horizontalAlign: horizontalKey === 'left' ? 'start' : 'end', verticalAlign: verticalKey === 'top' ? 'start' : 'end', }; } case 'top': { const { value: horizontalValue = plotWidth / 2 } = exclusiveProps( legendOffsets, 'left', 'right', position, ); const x = horizontalValue; const y = -(legendOffsets.bottom || 0) - legendOffset; return { x, y, horizontalAlign: 'middle', verticalAlign: 'end' }; } case 'right': { const { key: verticalKey = 'top', value: verticalValue = plotHeight / 2, } = exclusiveProps(legendOffsets, 'top', 'bottom', position); const y = verticalKey === 'bottom' ? -verticalValue : verticalValue; const x = plotWidth + (legendOffsets.left || 0) + legendOffset; return { x, y, horizontalAlign: 'start', verticalAlign: 'middle' }; } case 'bottom': { const { value: horizontalValue = plotWidth / 2 } = exclusiveProps( legendOffsets, 'left', 'right', position, ); const x = horizontalValue; const y = plotHeight + (legendOffsets.top || 0) + legendOffset; return { x, y, horizontalAlign: 'middle', verticalAlign: 'start' }; } case 'left': { const { key: verticalKey = 'top', value: verticalValue = plotHeight / 2, } = exclusiveProps(legendOffsets, 'top', 'bottom', position); const y = verticalKey === 'bottom' ? -verticalValue : verticalValue; const x = -(legendOffsets.right || 0) - legendOffset; return { x, y, horizontalAlign: 'end', verticalAlign: 'middle' }; } default: { throw new Error(`Position ${JSON.stringify(position)} unknown`); } } } export type LegendPosition = Position | 'embedded'; export type LegendProps = { position?: LegendPosition; margin?: number; onClick?: (args: { event: React.MouseEvent; id: string; }) => void; labelStyle?: CSSFuncProps<{ id: string }>; lineStyle?: CSSFuncProps<{ id: string }>; showHide?: boolean; } & Partial>; export function Legend(options: LegendProps) { const { position = 'embedded', margin = 10, onClick, lineStyle: funcLineStyle = {}, showHide = false, labelStyle: funcLabelStyle = {}, ...legendOffsets } = options; const { plotWidth, plotHeight } = usePlotContext(); const plotDispatch = usePlotDispatchContext(); const [state, legendDispatch] = useLegend(); const legendOffset = useContext(legendOffsetContext); const alignGroupProps = useMemo(() => { return translation( position, legendOffsets, plotWidth, plotHeight, legendOffset, ); }, [position, legendOffsets, plotWidth, plotHeight, legendOffset]); useEffect(() => { plotDispatch({ type: 'addLegend', payload: { position, margin } }); return () => plotDispatch({ type: 'removeLegend' }); }, [plotDispatch, position, margin]); function onClickLegendItem(event: React.MouseEvent, id: string) { onClick?.({ event, id }); if (showHide) { legendDispatch({ type: 'TOGGLE_VISIBILITY', payload: { id }, }); } } return ( {state.labels.map((value, index) => { const labelStyle = functionalStyle( { fontSize: '16px' }, funcLabelStyle, { id: value.id }, ); const lineStyle = functionalStyle({}, funcLineStyle, { id: value.id }); // TODO: fix this as it's not guaranteed that `fontSize` can be parsed as a string. const height = Number.parseInt(String(labelStyle.fontSize), 10); const xPos = 10; const yPos = index * height + height / 2 + 3; if (value.range) { return ( onClickLegendItem(event, value.id)} key={index} transform={`translate(${xPos}, 0)`} style={{ opacity: value.isVisible ? '1' : '0.6' }} > {getRangeShape({ index, rangeColor: value.range.rangeColor, lineColor: value.colorLine, style: lineStyle, height, })} {value.label} ); } let Marker; if (value.shape) { Marker = markersMap[value.shape.figure]; } return ( onClickLegendItem(event, value.id)} key={index} transform={`translate(${xPos}, 0)`} style={{ opacity: value.isVisible ? '1' : '0.6' }} > {getLineShape({ index, color: value.colorLine, style: lineStyle, height, })} {value.shape && Marker && !value.shape.hidden && ( )} {value.label} ); })} ); } function getLineShape(config: { index: number; color?: string; style?: CSSProperties; height?: number; width?: number; }) { const { index, color, style = {}, height = 16 } = config; const x = 0; const { strokeWidth = '2px' } = style; const y = index * height + height / 2 + 3; return ( ); } function getRangeShape(config: { index: number; rangeColor?: string; lineColor?: string; style?: CSSProperties; height?: number; }) { const { index, rangeColor, lineColor, style = {}, height = 16 } = config; const { strokeWidth = '15px' } = style; const lineHeight = Number.parseInt(strokeWidth?.toString(), 10) / 5; const x = 0; const y = index * height + height / 2 - lineHeight; return ( ); }