Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .nx/version-plans/version-plan-1778163738042.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@ledgerhq/lumen-ui-react-visualization': patch
---

feat(Scrubber): Introduce Scrubber component for chart interactions
Original file line number Diff line number Diff line change
Expand Up @@ -68,26 +68,25 @@ describe('XAxis', () => {
ticks: [1, 3],
});
const axis = getByTestId('x-axis');
// 2 grid lines + 1 axis line + 2 tick marks = 5
expect(axis.querySelectorAll('line')).toHaveLength(5);
});

it('uses dashed grid lines by default', () => {
it('uses solid grid lines by default', () => {
const { getByTestId } = renderXAxis({ showGrid: true, ticks: [2] });
const axis = getByTestId('x-axis');
const line = axis.querySelector('line');
expect(line?.getAttribute('stroke-dasharray')).toBe('3 3');
expect(line?.getAttribute('stroke-dasharray')).toBeNull();
});

it('uses solid grid lines when gridLineStyle is solid', () => {
it('uses dashed grid lines when gridLineStyle is dashed', () => {
const { getByTestId } = renderXAxis({
showGrid: true,
gridLineStyle: 'solid',
gridLineStyle: 'dashed',
ticks: [2],
});
const axis = getByTestId('x-axis');
const line = axis.querySelector('line');
expect(line?.getAttribute('stroke-dasharray')).toBeNull();
expect(line?.getAttribute('stroke-dasharray')).toBe('3 3');
});

it('applies tickLabelFormatter', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ const TICK_LABEL_OFFSET = 6;
export const DEFAULT_AXIS_HEIGHT = 28;

export function XAxis({
gridLineStyle = 'dashed',
gridLineStyle = 'solid',
position = 'bottom',
showGrid = false,
showLine = false,
Expand Down Expand Up @@ -56,7 +56,9 @@ export function XAxis({
y1={drawingArea.y}
x2={tick.position}
y2={drawingArea.y + drawingArea.height}
style={{ stroke: cssVar('var(--border-muted-subtle)') }}
style={{
stroke: cssVar('var(--border-muted-subtle-transparent)'),
}}
strokeWidth={cssVar('var(--stroke-1)')}
strokeDasharray={gridLineStyle === 'dashed' ? '3 3' : undefined}
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,26 +71,25 @@ describe('YAxis', () => {
ticks: [20, 40],
});
const axis = getByTestId('y-axis');
// 2 grid lines + 1 axis line + 2 tick marks = 5
expect(axis.querySelectorAll('line')).toHaveLength(5);
});

it('uses dashed grid lines by default', () => {
it('uses solid grid lines by default', () => {
const { getByTestId } = renderYAxis({ showGrid: true, ticks: [30] });
const axis = getByTestId('y-axis');
const line = axis.querySelector('line');
expect(line?.getAttribute('stroke-dasharray')).toBe('3 3');
expect(line?.getAttribute('stroke-dasharray')).toBeNull();
});

it('uses solid grid lines when gridLineStyle is solid', () => {
it('uses dashed grid lines when gridLineStyle is dashed', () => {
const { getByTestId } = renderYAxis({
showGrid: true,
gridLineStyle: 'solid',
gridLineStyle: 'dashed',
ticks: [30],
});
const axis = getByTestId('y-axis');
const line = axis.querySelector('line');
expect(line?.getAttribute('stroke-dasharray')).toBeNull();
expect(line?.getAttribute('stroke-dasharray')).toBe('3 3');
});

it('applies tickLabelFormatter', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ const TICK_LABEL_OFFSET = 6;
export const DEFAULT_AXIS_WIDTH = 40;

export function YAxis({
gridLineStyle = 'dashed',
gridLineStyle = 'solid',
position = 'start',
showGrid = false,
showLine = false,
Expand Down Expand Up @@ -56,7 +56,9 @@ export function YAxis({
y1={tick.position}
x2={drawingArea.x + drawingArea.width}
y2={tick.position}
style={{ stroke: cssVar('var(--border-muted-subtle)') }}
style={{
stroke: cssVar('var(--border-muted-subtle-transparent)'),
}}
Comment thread
zel-kass marked this conversation as resolved.
strokeWidth={cssVar('var(--stroke-1)')}
strokeDasharray={gridLineStyle === 'dashed' ? '3 3' : undefined}
/>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';

import { ScrubberProvider } from '../Scrubber/ScrubberProvider';
import { CartesianChartProvider, useBuildChartContext } from './context';
import type { CartesianChartProps } from './types';
import {
Expand All @@ -18,9 +19,12 @@ export function CartesianChart({
inset,
axisPadding,
ariaLabel = 'Chart',
enableScrubbing = false,
onScrubberPositionChange,
children,
}: CartesianChartProps) {
const containerRef = useRef<HTMLDivElement>(null);
const svgRef = useRef<SVGSVGElement>(null);
const [measuredWidth, setMeasuredWidth] = useState<number | undefined>(
typeof width === 'number' ? width : undefined,
);
Expand Down Expand Up @@ -69,15 +73,31 @@ export function CartesianChart({

const svgContent = (
<svg
ref={svgRef}
data-testid='chart-svg'
width={resolvedWidth}
height={height}
role='img'
aria-label={ariaLabel || 'Chart'}
style={{ display: 'block', overflow: 'visible' }}
tabIndex={enableScrubbing ? 0 : undefined}
style={{
display: 'block',
overflow: 'visible',
outline: enableScrubbing ? 'none' : undefined,
Comment thread
zel-kass marked this conversation as resolved.
}}
>
<CartesianChartProvider value={contextValue}>
{children}
{enableScrubbing ? (
<ScrubberProvider
svgRef={svgRef}
enableScrubbing={enableScrubbing}
onScrubberPositionChange={onScrubberPositionChange}
>
{children}
</ScrubberProvider>
) : (
children
)}
</CartesianChartProvider>
</svg>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,4 +48,15 @@ export type CartesianChartProps = {
* SVG content rendered inside the chart's context provider.
*/
children?: ReactNode;
/**
* Enables scrubbing (hover/touch/keyboard) interactions on the chart.
* When true, the SVG becomes focusable and captures pointer/keyboard events.
* @default false
*/
enableScrubbing?: boolean;
/**
* Callback fired whenever the scrubber moves to a new data index or is cleared.
* Receives `undefined` when the scrubber leaves the chart.
*/
onScrubberPositionChange?: (index: number | undefined) => void;
};
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { useCartesianChartContext } from '../CartesianChart/context';
import type { LineProps } from './types';
import { toScaledPoints, buildLinePath, buildAreaPath } from './utils';

const AREA_GRADIENT_OPACITY = 0.2;
const AREA_GRADIENT_OPACITY = 0.15;

export function Line({
seriesId,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ export function LineChart({
width = '100%',
height = 160,
inset,
enableScrubbing,
onScrubberPositionChange,
children,
}: LineChartProps) {
const {
Expand Down Expand Up @@ -78,6 +80,8 @@ export function LineChart({
height={height}
inset={inset}
axisPadding={axisPadding}
enableScrubbing={enableScrubbing}
onScrubberPositionChange={onScrubberPositionChange}
>
{showXAxis && <XAxis {...xAxisVisualProps} />}
{showYAxis && <YAxis {...yAxisVisualProps} />}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,4 +61,15 @@ export type LineChartProps = {
* Additional SVG content rendered inside the chart after lines and axes.
*/
children?: ReactNode;
/**
* Enables scrubbing (hover/touch/keyboard) interactions on the chart.
* When true, add a `<Scrubber>` as a child to visualise the interaction.
* @default false
*/
enableScrubbing?: boolean;
/**
* Callback fired whenever the scrubber moves to a new data index or is cleared.
* Receives `undefined` when the scrubber leaves the chart.
*/
onScrubberPositionChange?: (index: number | undefined) => void;
};
Loading
Loading