diff --git a/packages/core/index.d.ts b/packages/core/index.d.ts index 6d9186410..96427cd4c 100644 --- a/packages/core/index.d.ts +++ b/packages/core/index.d.ts @@ -77,6 +77,13 @@ declare module '@nivo/core' { line: Partial } legends: { + hidden: { + symbol: Partial<{ + fill: string + opacity: number + }> + text: Partial + } text: Partial } labels: { @@ -129,6 +136,10 @@ declare module '@nivo/core' { line: Partial }> legends: Partial<{ + hidden: Partial<{ + symbol: CompleteTheme['legends']['hidden']['symbol'] + text: CompleteTheme['legends']['hidden']['text'] + }> text: Partial }> labels: Partial<{ diff --git a/packages/core/src/theming/defaultTheme.js b/packages/core/src/theming/defaultTheme.js index 49930fa1c..6fab13331 100644 --- a/packages/core/src/theming/defaultTheme.js +++ b/packages/core/src/theming/defaultTheme.js @@ -38,6 +38,16 @@ export const defaultTheme = { }, }, legends: { + hidden: { + symbol: { + fill: '#333333', + opacity: 0.6, + }, + text: { + fill: '#333333', + opacity: 0.6, + }, + }, text: {}, }, labels: { diff --git a/packages/core/src/theming/propTypes.js b/packages/core/src/theming/propTypes.js index c960e59d2..631a35090 100644 --- a/packages/core/src/theming/propTypes.js +++ b/packages/core/src/theming/propTypes.js @@ -44,6 +44,13 @@ export const gridThemePropType = PropTypes.shape({ }) export const legendsThemePropType = PropTypes.shape({ + hidden: PropTypes.shape({ + symbol: PropTypes.shape({ + fill: PropTypes.string.isRequired, + opacity: PropTypes.number, + }).isRequired, + text: PropTypes.shape({ ...textProps, opacity: PropTypes.number }).isRequired, + }).isRequired, text: PropTypes.shape({ ...textProps }).isRequired, }) diff --git a/packages/legends/src/svg/BoxLegendSvg.tsx b/packages/legends/src/svg/BoxLegendSvg.tsx index 8561b852b..58a15ce45 100644 --- a/packages/legends/src/svg/BoxLegendSvg.tsx +++ b/packages/legends/src/svg/BoxLegendSvg.tsx @@ -32,6 +32,7 @@ export const BoxLegendSvg = ({ onClick, onMouseEnter, onMouseLeave, + toggleSerie, effects, }: BoxLegendSvgProps) => { @@ -78,6 +79,7 @@ export const BoxLegendSvg = ({ onClick={onClick} onMouseEnter={onMouseEnter} onMouseLeave={onMouseLeave} + toggleSerie={typeof toggleSerie === 'boolean' ? undefined : toggleSerie} /> ) } diff --git a/packages/legends/src/svg/LegendSvg.tsx b/packages/legends/src/svg/LegendSvg.tsx index e8def96e9..e9a37a6dd 100644 --- a/packages/legends/src/svg/LegendSvg.tsx +++ b/packages/legends/src/svg/LegendSvg.tsx @@ -30,6 +30,7 @@ export const LegendSvg = ({ onClick, onMouseEnter, onMouseLeave, + toggleSerie, }: LegendSvgProps) => { const { padding } = computeDimensions({ itemCount: data.length, @@ -67,6 +68,7 @@ export const LegendSvg = ({ onClick={onClick} onMouseEnter={onMouseEnter} onMouseLeave={onMouseLeave} + toggleSerie={toggleSerie} /> ))} diff --git a/packages/legends/src/svg/LegendSvgItem.tsx b/packages/legends/src/svg/LegendSvgItem.tsx index ec46a74ed..804cc5ef3 100644 --- a/packages/legends/src/svg/LegendSvgItem.tsx +++ b/packages/legends/src/svg/LegendSvgItem.tsx @@ -41,6 +41,7 @@ export const LegendSvgItem = ({ onClick, onMouseEnter, onMouseLeave, + toggleSerie, effects, }: LegendSvgItemProps) => { @@ -93,7 +94,7 @@ export const LegendSvgItem = ({ height, }) - const isInteractive = [onClick, onMouseEnter, onMouseLeave].some( + const isInteractive = [onClick, onMouseEnter, onMouseLeave, toggleSerie].some( handler => handler !== undefined ) @@ -115,6 +116,7 @@ export const LegendSvgItem = ({ }} onClick={event => { onClick?.(data, event) + toggleSerie?.(data.id) }} onMouseEnter={handleMouseEnter} onMouseLeave={handleMouseLeave} @@ -127,6 +129,7 @@ export const LegendSvgItem = ({ fill: data.fill ?? data.color ?? 'black', borderWidth: style.symbolBorderWidth ?? symbolBorderWidth, borderColor: style.symbolBorderColor ?? symbolBorderColor, + ...(data.hidden ? theme.legends.hidden.symbol : undefined), })} { @@ -15,6 +16,7 @@ export const SymbolCircle = ({ cx={x + size / 2} cy={y + size / 2} fill={fill} + opacity={opacity} strokeWidth={borderWidth} stroke={borderColor} style={{ diff --git a/packages/legends/src/svg/symbols/SymbolDiamond.tsx b/packages/legends/src/svg/symbols/SymbolDiamond.tsx index 805c0825f..ce32ce455 100644 --- a/packages/legends/src/svg/symbols/SymbolDiamond.tsx +++ b/packages/legends/src/svg/symbols/SymbolDiamond.tsx @@ -6,6 +6,7 @@ export const SymbolDiamond = ({ y, size, fill, + opacity = 1, borderWidth = 0, borderColor = 'transparent', }: SymbolProps) => { @@ -20,6 +21,7 @@ export const SymbolDiamond = ({ L${size / 2} 0 `} fill={fill} + opacity={opacity} strokeWidth={borderWidth} stroke={borderColor} style={{ diff --git a/packages/legends/src/svg/symbols/SymbolSquare.tsx b/packages/legends/src/svg/symbols/SymbolSquare.tsx index d06b2e77e..69e51a7bf 100644 --- a/packages/legends/src/svg/symbols/SymbolSquare.tsx +++ b/packages/legends/src/svg/symbols/SymbolSquare.tsx @@ -6,6 +6,7 @@ export const SymbolSquare = ({ y, size, fill, + opacity = 1, borderWidth = 0, borderColor = 'transparent', }: SymbolProps) => { @@ -14,6 +15,7 @@ export const SymbolSquare = ({ x={x} y={y} fill={fill} + opacity={opacity} strokeWidth={borderWidth} stroke={borderColor} width={size} diff --git a/packages/legends/src/svg/symbols/SymbolTriangle.tsx b/packages/legends/src/svg/symbols/SymbolTriangle.tsx index 963741104..cd590ebaf 100644 --- a/packages/legends/src/svg/symbols/SymbolTriangle.tsx +++ b/packages/legends/src/svg/symbols/SymbolTriangle.tsx @@ -6,6 +6,7 @@ export const SymbolTriangle = ({ y, size, fill, + opacity = 1, borderWidth = 0, borderColor = 'transparent', }: SymbolProps) => { @@ -19,6 +20,7 @@ export const SymbolTriangle = ({ L${size / 2} 0 `} fill={fill} + opacity={opacity} strokeWidth={borderWidth} stroke={borderColor} style={{ diff --git a/packages/legends/src/svg/symbols/types.ts b/packages/legends/src/svg/symbols/types.ts index 7c7d47e99..2be0d06e5 100644 --- a/packages/legends/src/svg/symbols/types.ts +++ b/packages/legends/src/svg/symbols/types.ts @@ -4,6 +4,7 @@ export type SymbolProps = { y: number size: number fill: string + opacity?: number borderWidth?: number borderColor?: string } diff --git a/packages/legends/src/types.ts b/packages/legends/src/types.ts index ed02d37a5..703aa4892 100644 --- a/packages/legends/src/types.ts +++ b/packages/legends/src/types.ts @@ -31,7 +31,9 @@ type InteractivityProps = Partial< Record< 'onClick' | 'onMouseEnter' | 'onMouseLeave', (datum: Datum, event: React.MouseEvent) => void - > + > & { + toggleSerie: (id: Datum['id']) => void + } > export type LegendAnchor = @@ -56,6 +58,7 @@ export type LegendItemDirection = export type Datum = { id: string | number label: string | number + hidden?: boolean color?: string fill?: string } @@ -81,15 +84,20 @@ export type LegendProps = { translateX?: number translateY?: number anchor: LegendAnchor + toggleSerie?: boolean } & CommonLegendProps & BoxLegendSymbolProps & - InteractivityProps + Omit export type BoxLegendSvgProps = { containerWidth: number containerHeight: number -} & LegendProps & - Required> +} & Omit & + Required> & + Omit & + Partial<{ + toggleSerie: boolean | InteractivityProps['toggleSerie'] + }> export type LegendSvgProps = { x: number diff --git a/packages/legends/tests/svg/__snapshots__/LegendSvgItem.test.tsx.snap b/packages/legends/tests/svg/__snapshots__/LegendSvgItem.test.tsx.snap index 67bf363d9..da764b6ec 100644 --- a/packages/legends/tests/svg/__snapshots__/LegendSvgItem.test.tsx.snap +++ b/packages/legends/tests/svg/__snapshots__/LegendSvgItem.test.tsx.snap @@ -25,6 +25,7 @@ exports[`should support bottom-to-top direction 1`] = ` { partialMargin ) - const { lineGenerator, areaGenerator, series, xScale, yScale, slices, points } = useLine({ + const { + legendData, + toggleSerie, + lineGenerator, + areaGenerator, + series, + xScale, + yScale, + slices, + points, + } = useLine({ data, xScale: xScaleSpec, xFormat, @@ -126,18 +136,6 @@ const Line = props => { const [currentPoint, setCurrentPoint] = useState(null) const [currentSlice, setCurrentSlice] = useState(null) - const legendData = useMemo( - () => - series - .map(line => ({ - id: line.id, - label: line.id, - color: line.color, - })) - .reverse(), - [series] - ) - const layerById = { grid: ( { containerHeight={innerHeight} data={legend.data || legendData} theme={theme} + toggleSerie={legend.toggleSerie ? toggleSerie : undefined} /> )), } diff --git a/packages/line/src/hooks.js b/packages/line/src/hooks.js index 9bdd13922..664ecb8f2 100644 --- a/packages/line/src/hooks.js +++ b/packages/line/src/hooks.js @@ -6,7 +6,7 @@ * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ -import { useMemo } from 'react' +import { useCallback, useMemo, useState } from 'react' import { area, line } from 'd3-shape' import { curveFromProp, useTheme, useValueFormatter } from '@nivo/core' import { useOrdinalColorScale, useInheritedColor } from '@nivo/colors' @@ -160,21 +160,45 @@ export const useLine = ({ const theme = useTheme() const getPointColor = useInheritedColor(pointColor, theme) const getPointBorderColor = useInheritedColor(pointBorderColor, theme) + const [hiddenIds, setHiddenIds] = useState([]) const { xScale, yScale, series: rawSeries } = useMemo( - () => computeXYScalesForSeries(data, xScaleSpec, yScaleSpec, width, height), - [data, xScaleSpec, yScaleSpec, width, height] - ) - - const series = useMemo( () => - rawSeries.map(serie => ({ - ...serie, - color: getColor(serie), - })), - [rawSeries, getColor] + computeXYScalesForSeries( + data.filter(item => hiddenIds.indexOf(item.id) === -1), + xScaleSpec, + yScaleSpec, + width, + height + ), + [data, hiddenIds, xScaleSpec, yScaleSpec, width, height] ) + const { legendData, series } = useMemo(() => { + const dataWithColor = data.map(line => ({ + id: line.id, + label: line.id, + color: getColor(line), + })) + const series = dataWithColor + .map(datum => ({ + ...rawSeries.find(serie => serie.id === datum.id), + color: datum.color, + })) + .filter(item => Boolean(item.id)) + const legendData = dataWithColor + .map(item => ({ ...item, hidden: !series.find(serie => serie.id === item.id) })) + .reverse() + + return { legendData, series } + }, [data, rawSeries, getColor]) + + const toggleSerie = useCallback(id => { + setHiddenIds(state => + state.indexOf(id) > -1 ? state.filter(item => item !== id) : [...state, id] + ) + }, []) + const points = usePoints({ series, getPointColor, @@ -198,6 +222,8 @@ export const useLine = ({ }) return { + legendData, + toggleSerie, lineGenerator, areaGenerator, getColor,