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
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.
32 changes: 32 additions & 0 deletions src/__tests__/x-axis.visual.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import React from 'react';

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

import {ChartTestStory} from '../../playwright/components/ChartTestStory';
import {barYBasicData} from '../__stories__/__data__';

test.describe('X-axis', () => {
test('min', async ({mount}) => {
const data = cloneDeep(barYBasicData);
set(data, 'xAxis.min', 60);
const component = await mount(<ChartTestStory data={data} />);
await expect(component.locator('svg')).toHaveScreenshot();
});

test('max', async ({mount}) => {
const data = cloneDeep(barYBasicData);
set(data, 'xAxis.max', 120);
const component = await mount(<ChartTestStory data={data} />);
await expect(component.locator('svg')).toHaveScreenshot();
});

test('min-max', async ({mount}) => {
const data = cloneDeep(barYBasicData);
set(data, 'xAxis.min', 60);
set(data, 'xAxis.max', 120);
const component = await mount(<ChartTestStory data={data} />);
await expect(component.locator('svg')).toHaveScreenshot();
});
});
32 changes: 32 additions & 0 deletions src/__tests__/y-axis.visual.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import React from 'react';

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

import {ChartTestStory} from '../../playwright/components/ChartTestStory';
import {scatterBasicData} from '../__stories__/__data__';

test.describe('Y-axis', () => {
test('min', async ({mount}) => {
const data = cloneDeep(scatterBasicData);
set(data, 'yAxis[0].min', 5);
const component = await mount(<ChartTestStory data={data} />);
await expect(component.locator('svg')).toHaveScreenshot();
});

test('max', async ({mount}) => {
const data = cloneDeep(scatterBasicData);
set(data, 'yAxis[0].max', 8);
const component = await mount(<ChartTestStory data={data} />);
await expect(component.locator('svg')).toHaveScreenshot();
});

test('min-max', async ({mount}) => {
const data = cloneDeep(scatterBasicData);
set(data, 'yAxis[0].min', 5);
set(data, 'yAxis[0].max', 8);
const component = await mount(<ChartTestStory data={data} />);
await expect(component.locator('svg')).toHaveScreenshot();
});
});
10 changes: 9 additions & 1 deletion src/components/ChartInner/index.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import React from 'react';

import {useUniqId} from '@gravity-ui/uikit';

import {useCrosshair} from '../../hooks';
import {EventType, block, getDispatcher} from '../../utils';
import {AxisX, AxisY} from '../Axis';
Expand All @@ -23,6 +25,7 @@ export const ChartInner = (props: ChartInnerProps) => {
const htmlLayerRef = React.useRef<HTMLDivElement | null>(null);
const plotRef = React.useRef<SVGGElement | null>(null);
const dispatcher = React.useMemo(() => getDispatcher(), []);
const clipPathId = useUniqId();
const {
boundsHeight,
boundsOffsetLeft,
Expand Down Expand Up @@ -125,6 +128,11 @@ export const ChartInner = (props: ChartInnerProps) => {
onTouchMove={throttledHandleTouchMove}
onClick={handleChartClick}
>
<defs>
<clipPath id={clipPathId}>
<rect x={0} y={0} width={boundsWidth} height={boundsHeight} />
</clipPath>
</defs>
{title && <Title {...title} chartWidth={width} />}
<g transform={`translate(0, ${boundsOffsetTop})`}>
{preparedSplit.plots.map((plot, index) => {
Expand Down Expand Up @@ -161,7 +169,7 @@ export const ChartInner = (props: ChartInnerProps) => {
</g>
</React.Fragment>
)}
{shapes}
<g clipPath={`url(#${clipPathId})`}>{shapes}</g>
</g>
{preparedLegend.enabled && (
<Legend
Expand Down
69 changes: 50 additions & 19 deletions src/hooks/useAxisScales/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
getAxisHeight,
getDataCategoryValue,
getDefaultMaxXAxisValue,
getDefaultMinXAxisValue,
getDomainDataXBySeries,
getDomainDataYBySeries,
getOnlyVisibleSeries,
Expand Down Expand Up @@ -66,7 +67,8 @@ const filterCategoriesByVisibleSeries = (args: {

export function createYScale(axis: PreparedAxis, series: PreparedSeries[], boundsHeight: number) {
const yType: ChartAxisType = get(axis, 'type', DEFAULT_AXIS_TYPE);
const yMin = get(axis, 'min');
const yMinProps = get(axis, 'min');
const yMaxProps = get(axis, 'max');
const yCategories = get(axis, 'categories');
const yTimestamps = get(axis, 'timestamps');

Expand All @@ -77,15 +79,21 @@ export function createYScale(axis: PreparedAxis, series: PreparedSeries[], bound
const range = [boundsHeight, boundsHeight * axis.maxPadding];

if (isNumericalArrayData(domain)) {
const [domainYMin, domainMax] = extent(domain) as [number, number];
const yMinValue = typeof yMin === 'number' ? yMin : domainYMin;
let yMaxValue = domainMax;
if (series.some((s) => CHART_SERIES_WITH_VOLUME_ON_Y_AXIS.includes(s.type))) {
yMaxValue = Math.max(yMaxValue, 0);
const [yMinDomain, yMaxDomain] = extent(domain) as [number, number];
const yMin = typeof yMinProps === 'number' ? yMinProps : yMinDomain;
let yMax: number;

if (typeof yMaxProps === 'number') {
yMax = yMaxProps;
} else {
const hasSeriesWithVolumeOnYAxis = series.some((s) =>
CHART_SERIES_WITH_VOLUME_ON_Y_AXIS.includes(s.type),
);
yMax = hasSeriesWithVolumeOnYAxis ? Math.max(yMaxDomain, 0) : yMaxDomain;
}

const scaleFn = yType === 'logarithmic' ? scaleLog : scaleLinear;
return scaleFn().domain([yMinValue, yMaxValue]).range(range).nice();
return scaleFn().domain([yMin, yMax]).range(range).nice();
}

break;
Expand All @@ -106,13 +114,17 @@ export function createYScale(axis: PreparedAxis, series: PreparedSeries[], bound
const range = [boundsHeight, boundsHeight * axis.maxPadding];

if (yTimestamps) {
const [yMin, yMax] = extent(yTimestamps) as [number, number];
const [yMinTimestamp, yMaxTimestamp] = extent(yTimestamps) as [number, number];
const yMin = typeof yMinProps === 'number' ? yMinProps : yMinTimestamp;
const yMax = typeof yMaxProps === 'number' ? yMaxProps : yMaxTimestamp;
return scaleUtc().domain([yMin, yMax]).range(range).nice();
} else {
const domain = getDomainDataYBySeries(series);

if (isNumericalArrayData(domain)) {
const [yMin, yMax] = extent(domain) as [number, number];
const [yMinTimestamp, yMaxTimestamp] = extent(domain) as [number, number];
const yMin = typeof yMinProps === 'number' ? yMinProps : yMinTimestamp;
const yMax = typeof yMaxProps === 'number' ? yMaxProps : yMaxTimestamp;
return scaleUtc().domain([yMin, yMax]).range(range).nice();
}
}
Expand Down Expand Up @@ -150,13 +162,12 @@ export function createXScale(
series: (PreparedSeries | ChartSeries)[],
boundsWidth: number,
) {
const xMin = get(axis, 'min');
const xMax = getDefaultMaxXAxisValue(series);
const xMinProps = get(axis, 'min');
const xMaxProps = get(axis, 'max');
const xType: ChartAxisType = get(axis, 'type', DEFAULT_AXIS_TYPE);
const xCategories = get(axis, 'categories');
const xTimestamps = get(axis, 'timestamps');
const maxPadding = get(axis, 'maxPadding', 0);

const xAxisMinPadding = boundsWidth * maxPadding + calculateXAxisPadding(series);
const xRange = [0, boundsWidth - xAxisMinPadding];

Expand All @@ -166,13 +177,29 @@ export function createXScale(
const domain = getDomainDataXBySeries(series);

if (isNumericalArrayData(domain)) {
const [domainXMin, domainXMax] = extent(domain) as [number, number];
const xMinValue = typeof xMin === 'number' ? xMin : domainXMin;
const xMaxValue =
typeof xMax === 'number' ? Math.max(xMax, domainXMax) : domainXMax;
const [xMinDomain, xMaxDomain] = extent(domain) as [number, number];
let xMin: number;
let xMax: number;

if (typeof xMinProps === 'number') {
xMin = xMinProps;
} else {
const xMinDefault = getDefaultMinXAxisValue(series);
xMin = xMinDefault ?? xMinDomain;
}

if (typeof xMaxProps === 'number') {
xMax = xMaxProps;
} else {
const xMaxDefault = getDefaultMaxXAxisValue(series);
xMax =
typeof xMaxDefault === 'number'
? Math.max(xMaxDefault, xMaxDomain)
: xMaxDomain;
}

const scaleFn = xType === 'logarithmic' ? scaleLog : scaleLinear;
return scaleFn().domain([xMinValue, xMaxValue]).range(xRange).nice();
return scaleFn().domain([xMin, xMax]).range(xRange).nice();
}

break;
Expand All @@ -197,13 +224,17 @@ export function createXScale(
}
case 'datetime': {
if (xTimestamps) {
const [xMin, xMax] = extent(xTimestamps) as [number, number];
const [xMinTimestamp, xMaxTimestamp] = extent(xTimestamps) as [number, number];
const xMin = typeof xMinProps === 'number' ? xMinProps : xMinTimestamp;
const xMax = typeof xMaxProps === 'number' ? xMaxProps : xMaxTimestamp;
return scaleUtc().domain([xMin, xMax]).range(xRange).nice();
} else {
const domain = getDomainDataXBySeries(series);

if (isNumericalArrayData(domain)) {
const [xMin, xMax] = extent(domain) as [number, number];
const [xMinTimestamp, xMaxTimestamp] = extent(domain) as [number, number];
const xMin = typeof xMinProps === 'number' ? xMinProps : xMinTimestamp;
const xMax = typeof xMaxProps === 'number' ? xMaxProps : xMaxTimestamp;
return scaleUtc().domain([xMin, xMax]).range(xRange).nice();
}
}
Expand Down
1 change: 0 additions & 1 deletion src/hooks/useChartOptions/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,6 @@ export type PreparedAxis = Omit<ChartAxis, 'type' | 'labels' | 'plotLines' | 'pl
align: ChartAxisTitleAlignment;
maxRowCount: number;
};
min?: number;
grid: {
enabled: boolean;
};
Expand Down
21 changes: 2 additions & 19 deletions src/hooks/useChartOptions/x-axis.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import {
} from '../../constants';
import type {BaseTextStyle, ChartSeries, ChartXAxis} from '../../types';
import {
CHART_SERIES_WITH_VOLUME_ON_X_AXIS,
calculateCos,
formatAxisTickLabel,
getClosestPointsRange,
Expand Down Expand Up @@ -73,23 +72,6 @@ function getLabelSettings({
return {height: Math.min(maxHeight, labelsHeight), rotation};
}

function getAxisMin(axis?: ChartXAxis, series?: ChartSeries[]) {
const min = axis?.min;

if (
typeof min === 'undefined' &&
series?.some((s) => CHART_SERIES_WITH_VOLUME_ON_X_AXIS.includes(s.type))
) {
return series.reduce((minValue, s) => {
// @ts-expect-error
const minYValue = s.data.reduce((res, d) => Math.min(res, get(d, 'x', 0)), 0);
return Math.min(minValue, minYValue);
}, 0);
}

return min;
}

export const getPreparedXAxis = ({
xAxis,
series,
Expand Down Expand Up @@ -145,7 +127,8 @@ export const getPreparedXAxis = ({
align: get(xAxis, 'title.align', xAxisTitleDefaults.align),
maxRowCount: get(xAxis, 'title.maxRowCount', xAxisTitleDefaults.maxRowCount),
},
min: getAxisMin(xAxis, series),
min: get(xAxis, 'min'),
max: get(xAxis, 'max'),
maxPadding: get(xAxis, 'maxPadding', 0.01),
grid: {
enabled: get(xAxis, 'grid.enabled', true),
Expand Down
47 changes: 4 additions & 43 deletions src/hooks/useChartOptions/y-axis.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,22 +11,17 @@ import {
} from '../../constants';
import type {BaseTextStyle, ChartSeries, ChartYAxis} from '../../types';
import {
CHART_SERIES_WITH_VOLUME_ON_Y_AXIS,
formatAxisTickLabel,
getClosestPointsRange,
getDefaultMinYAxisValue,
getHorisontalSvgTextHeight,
getLabelsSize,
getScaleTicks,
getWaterfallPointSubtotal,
isAxisRelatedSeries,
wrapText,
} from '../../utils';
import {createYScale} from '../useAxisScales';
import type {
PreparedSeries,
PreparedWaterfallSeries,
PreparedWaterfallSeriesData,
} from '../useSeries/types';
import type {PreparedSeries} from '../useSeries/types';

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

Expand Down Expand Up @@ -58,41 +53,6 @@ const getAxisLabelMaxWidth = (args: {axis: PreparedAxis; series: ChartSeries[]})
}).maxWidth;
};

function getAxisMin(axis?: ChartYAxis, series?: ChartSeries[]) {
const min = axis?.min;

if (
typeof min === 'undefined' &&
series?.some((s) => CHART_SERIES_WITH_VOLUME_ON_Y_AXIS.includes(s.type))
) {
return series.reduce((minValue, s) => {
switch (s.type) {
case 'waterfall': {
const minSubTotal = s.data.reduce(
(res, d) =>
Math.min(
res,
getWaterfallPointSubtotal(
d as PreparedWaterfallSeriesData,
s as PreparedWaterfallSeries,
) || 0,
),
0,
);
return Math.min(minValue, minSubTotal);
}
default: {
// @ts-expect-error
const minYValue = s.data.reduce((res, d) => Math.min(res, get(d, 'y', 0)), 0);
return Math.min(minValue, minYValue);
}
}
}, 0);
}

return min;
}

export const getPreparedYAxis = ({
series,
yAxis,
Expand Down Expand Up @@ -172,7 +132,8 @@ export const getPreparedYAxis = ({
align: get(axisItem, 'title.align', yAxisTitleDefaults.align),
maxRowCount: titleMaxRowsCount,
},
min: getAxisMin(axisItem, series),
min: get(axisItem, 'min') ?? getDefaultMinYAxisValue(series),
max: get(axisItem, 'max'),
maxPadding: get(axisItem, 'maxPadding', 0.05),
grid: {
enabled: get(
Expand Down
9 changes: 4 additions & 5 deletions src/types/chart/axis.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,8 @@ export interface ChartAxis {
};
/** The minimum value of the axis. If undefined the min value is automatically calculate. */
min?: number;
/** The maximum value of the axis. If undefined the max value is automatically calculate. */
max?: number;
/** The grid lines settings. */
grid?: {
/** Enable or disable the grid lines.
Expand Down Expand Up @@ -140,11 +142,8 @@ export interface AxisCrosshair extends Omit<AxisPlotLine, 'value'> {
}

export interface ChartYAxis extends ChartAxis {
/** Axis location.
* Possible values - 'left' and 'right'.
* */
/** Axis location. Possible values - `'left'` and `'right'`. */
position?: 'left' | 'right';
/** Property for splitting charts. Determines which area the axis is located in.
* */
/** Property for splitting charts. Determines which area the axis is located in. */
plotIndex?: number;
}
Loading
Loading