Skip to content
Merged
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
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
21 changes: 21 additions & 0 deletions src/__stories__/Other/RangeSlider/RangeSlider.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,15 @@ export default meta;
type Story = StoryObj<typeof ChartStory>;

const dateTimeData = cloneDeep(lineTwoYAxisData);
set(dateTimeData, 'xAxis.maxPadding', 0);
set(dateTimeData, 'xAxis.rangeSlider', {enabled: true});
set(dateTimeData, 'legend', {enabled: true});
set(dateTimeData, 'title', {
text: 'Without default range',
});

const dateTimeDataWithRange = cloneDeep(lineTwoYAxisData);
set(dateTimeDataWithRange, 'xAxis.maxPadding', 0);
set(dateTimeDataWithRange, 'xAxis.rangeSlider', {enabled: true, defaultRange: {size: 'P1M'}});
set(dateTimeDataWithRange, 'legend', {enabled: true});
set(dateTimeDataWithRange, 'title', {
Expand All @@ -48,13 +50,15 @@ export const RangeSliderDateTime = {
} satisfies Story;

const linearData = cloneDeep(scatterLinearXAxisData);
set(linearData, 'xAxis.maxPadding', 0);
set(linearData, 'xAxis.rangeSlider', {enabled: true});
set(linearData, 'legend', {enabled: true});
set(linearData, 'title', {
text: 'Without default range',
});

const linearDataWithRange = cloneDeep(scatterLinearXAxisData);
set(linearDataWithRange, 'xAxis.maxPadding', 0);
set(linearDataWithRange, 'xAxis.rangeSlider', {enabled: true, defaultRange: {size: 1000}});
set(linearDataWithRange, 'legend', {enabled: true});
set(linearDataWithRange, 'title', {
Expand All @@ -74,3 +78,20 @@ export const RangeSliderLinear = {
</div>
),
} satisfies Story;

const dateTimeDataWithPadding = cloneDeep(lineTwoYAxisData);
set(dateTimeDataWithPadding, 'xAxis.maxPadding', 0.2);
set(dateTimeDataWithPadding, 'xAxis.rangeSlider', {enabled: true});
set(dateTimeDataWithPadding, 'legend', {enabled: true});
set(dateTimeDataWithPadding, 'title', {
text: 'With maxPadding 0.2',
});

export const RangeSliderWithPadding = {
name: 'With maxPadding',
render: () => (
<div style={{height: 350}}>
<Chart data={dateTimeDataWithPadding} />
</div>
),
} satisfies Story;
157 changes: 157 additions & 0 deletions src/__tests__/range-slider.visual.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
import React from 'react';

import {expect, test} from '@playwright/experimental-ct-react';
import cloneDeep from 'lodash/cloneDeep';
import merge from 'lodash/merge';

import {ChartTestStory} from '../../playwright/components/ChartTestStory';
import {lineTwoYAxisData, scatterLinearXAxisData} from '../__stories__/__data__';
import type {ChartData, DeepPartial} from '../types';

import {dragElementByCalculatedPosition, getLocator, getLocatorBoundingBox} from './utils';

function getData(args: {basicData: ChartData; extraData?: DeepPartial<ChartData>}): ChartData {
const {basicData, extraData} = args;
const data = cloneDeep(basicData);
const defaults: DeepPartial<ChartData> = {
chart: {margin: {top: 10, right: 10, bottom: 10, left: 10}},
legend: {enabled: false},
title: {text: ''},
xAxis: {
labels: {enabled: false},
maxPadding: 0,
rangeSlider: {enabled: true},
title: {text: ''},
type: 'datetime',
},
yAxis: [{title: {text: ''}}, {title: {text: ''}}],
};
merge(data, defaults, extraData);

return data;
}

test.describe('Range slider', () => {
test('Basic actions', async ({mount, page}) => {
const data = getData({basicData: lineTwoYAxisData});
const component = await mount(<ChartTestStory data={data} />);
await expect(component.locator('svg')).toHaveScreenshot();

// Drag left brush handle to the right
await dragElementByCalculatedPosition({
component,
page,
selector: '.gcharts-brush .handle--w',
getDragOptions: ({boundingBox}) => {
const startX = boundingBox.x + boundingBox.width / 2;
const endX = startX + 100;
const y = boundingBox.y + boundingBox.height / 2;
return {from: [startX, y], to: [endX, y]};
},
});
await expect(component.locator('svg')).toHaveScreenshot();

// Drag right brush handle to the left
await dragElementByCalculatedPosition({
component,
page,
selector: '.gcharts-brush .handle--e',
getDragOptions: ({boundingBox}) => {
const startX = boundingBox.x + boundingBox.width / 2;
const endX = startX - 100;
const y = boundingBox.y + boundingBox.height / 2;
return {from: [startX, y], to: [endX, y]};
},
});
await expect(component.locator('svg')).toHaveScreenshot();

// Drag selection to the right
await dragElementByCalculatedPosition({
component,
page,
selector: '.gcharts-brush .selection',
getDragOptions: ({boundingBox}) => {
const startX = boundingBox.x + boundingBox.width / 2;
const endX = startX + 50;
const y = boundingBox.y + boundingBox.height / 2;
return {from: [startX, y], to: [endX, y]};
},
});
await expect(component.locator('svg')).toHaveScreenshot();
});

test('Default range size duration', async ({mount}) => {
const data = getData({
basicData: lineTwoYAxisData,
extraData: {xAxis: {rangeSlider: {defaultRange: {size: 'P1M'}}}},
});
const component = await mount(<ChartTestStory data={data} />);
await expect(component.locator('svg')).toHaveScreenshot();
});

test('Default range size number', async ({mount}) => {
const data = getData({
basicData: scatterLinearXAxisData,
extraData: {xAxis: {rangeSlider: {defaultRange: {size: 1000}}, type: 'linear'}},
});
const component = await mount(<ChartTestStory data={data} />);
await expect(component.locator('svg')).toHaveScreenshot();
});

test('Hide series', async ({mount}) => {
const data = getData({
basicData: lineTwoYAxisData,
extraData: {
series: {
data: [{rangeSlider: {visible: false}}],
},
},
});
const component = await mount(<ChartTestStory data={data} />);
await expect(component.locator('svg')).toHaveScreenshot();
});

test('Height option', async ({mount}) => {
const data = getData({
basicData: lineTwoYAxisData,
extraData: {xAxis: {rangeSlider: {height: 80}}},
});
const component = await mount(<ChartTestStory data={data} />);
await expect(component.locator('svg')).toHaveScreenshot();
});

test('Margin option', async ({mount}) => {
const data = getData({
basicData: lineTwoYAxisData,
extraData: {xAxis: {rangeSlider: {margin: 30}}},
});
const component = await mount(<ChartTestStory data={data} />);
await expect(component.locator('svg')).toHaveScreenshot();
});

test('Overlay click', async ({mount, page}) => {
const data = getData({
basicData: lineTwoYAxisData,
extraData: {xAxis: {rangeSlider: {defaultRange: {size: 'P1M'}}}},
});
const component = await mount(<ChartTestStory data={data} />);

// Click on the overlay center
const overlay = await getLocator({component, selector: '.gcharts-brush .overlay'});
const boundingBox = await getLocatorBoundingBox(overlay);
let x = boundingBox.x + boundingBox.width / 2;
const y = boundingBox.y + boundingBox.height / 2;
await page.mouse.click(x, y);
await expect(component.locator('svg')).toHaveScreenshot();

// Click on the overlay left edge
x = boundingBox.x + 5;
await page.mouse.click(x, y);
await expect(component.locator('svg')).toHaveScreenshot();

// Click on the overlay right edge
x = boundingBox.x + boundingBox.width - 5;
await page.mouse.click(x, y);
await expect(component.locator('svg')).toHaveScreenshot();
});
});
49 changes: 49 additions & 0 deletions src/__tests__/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import {expect} from '@playwright/experimental-ct-react';
import type {MountResult} from '@playwright/experimental-ct-react';
import type {Locator, Page} from '@playwright/test';

export async function getLocator(args: {component: MountResult; selector: string}) {
const {component, selector} = args;
const locator = component.locator(selector);
await expect(locator).toBeVisible();

return locator;
}

export async function getLocatorBoundingBox(locator: Locator) {
const boundingBox = await locator.evaluate((el) => el.getBoundingClientRect());

if (!boundingBox) {
throw new Error('Bounding box not found');
}

return boundingBox;
}

async function simulateDrag(args: {from: [number, number]; page: Page; to: [number, number]}) {
const {from, page, to} = args;
const [fromX, fromY] = from;
const [toX, toY] = to;

await page.mouse.move(fromX, fromY);
await page.mouse.down();
await page.mouse.move(toX, toY);
await page.mouse.up();
}

export async function dragElementByCalculatedPosition(args: {
component: MountResult;
getDragOptions: (args: {boundingBox: DOMRect}) => {
from: [number, number];
to: [number, number];
};
page: Page;
selector: string;
}) {
const {component, getDragOptions, page, selector} = args;
const locator = await getLocator({component, selector});
const boundingBox = await getLocatorBoundingBox(locator);
const {from, to} = getDragOptions({boundingBox});

await simulateDrag({page, from, to});
}
2 changes: 1 addition & 1 deletion src/hooks/useAxisScales/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -353,7 +353,7 @@ export function createXScale(args: {
order: axis.order,
});
}
const maxPadding = get(axis, 'maxPadding', 0);
const maxPadding = rangeSliderState ? 0 : get(axis, 'maxPadding', 0);
const xAxisMaxPadding = boundsWidth * maxPadding + calculateXAxisPadding(series);

const range = getXScaleRange({
Expand Down
1 change: 1 addition & 0 deletions src/hooks/useRangeSlider/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ export function useRangeSlider(props: UseRangeSliderProps): PreparedRangeSliderP
}, [rangeSliderState, xScale]);
const offsetTop = getRangeSliderOffsetTop({
height,
preparedChart,
preparedLegend,
preparedRangeSlider,
});
Expand Down
12 changes: 10 additions & 2 deletions src/hooks/useRangeSlider/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,28 @@ import {isBandScale} from '../../utils';
import type {PreparedRangeSlider} from '../useAxis/types';
import type {ChartScale} from '../useAxisScales';
import type {BrushSelection} from '../useBrush/types';
import type {PreparedChart} from '../useChartOptions/types';
import type {PreparedLegend} from '../useSeries/types';

import type {RangeSliderState} from './types';

export function getRangeSliderOffsetTop(args: {
height: number;
preparedChart: PreparedChart;
preparedLegend: PreparedLegend | null;
preparedRangeSlider: PreparedRangeSlider;
}) {
const {height, preparedLegend, preparedRangeSlider} = args;
const {height, preparedChart, preparedLegend, preparedRangeSlider} = args;
const legendHeight = preparedLegend?.enabled ? (preparedLegend?.height ?? 0) : 0;
const legendMargin = preparedLegend?.enabled ? (preparedLegend?.margin ?? 0) : 0;

return height - preparedRangeSlider.height - legendHeight - legendMargin;
return (
height -
preparedRangeSlider.height -
legendHeight -
legendMargin -
preparedChart.margin.bottom
);
}

export function getRangeSliderSelection(args: {
Expand Down
7 changes: 7 additions & 0 deletions src/types/misc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,10 @@ export type PointPosition = [number, number];
export type DeepRequired<T> = Required<{
[K in keyof T]: T[K] extends Required<T[K]> ? T[K] : DeepRequired<T[K]>;
}>;

/**
* Makes all properties in T optional, including nested objects.
*/
export type DeepPartial<T> = {
[K in keyof T]?: T[K] extends object ? DeepPartial<T[K]> : T[K];
};
Loading