Skip to content

Commit

Permalink
feat(D3 plugin): negative Y values for bar-x chart (#483)
Browse files Browse the repository at this point in the history
* feat(D3): negative Y values for bar-x chart

* Add bar-y and area chart examples with negative values

* fix area chart

* Fix bar-y

* fix types
  • Loading branch information
kuzmadom authored May 22, 2024
1 parent e1602e1 commit 7180e39
Show file tree
Hide file tree
Showing 11 changed files with 217 additions and 26 deletions.
27 changes: 21 additions & 6 deletions src/plugins/d3/__stories__/Showcase.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,20 @@ import {StoryObj} from '@storybook/react';
import {Loader} from '../../../components/Loader/Loader';
import {settings} from '../../../libs';
import {Basic as BasicArea} from '../examples/area/Basic';
import {NegativeValues as AreaNegativeValues} from '../examples/area/NegativeValues';
import {PercentStackingArea} from '../examples/area/PercentStacking';
import {StackedArea} from '../examples/area/StackedArea';
import {TwoYAxis as AreaTwoYAxis} from '../examples/area/TwoYAxis';
import {BasicBarXChart} from '../examples/bar-x/Basic';
import {DataLabels as BarXDataLabels} from '../examples/bar-x/DataLabels';
import {GroupedColumns} from '../examples/bar-x/GroupedColumns';
import {NegativeValues as BarXNegativeValues} from '../examples/bar-x/NegativeValues';
import {PercentStackColumns} from '../examples/bar-x/PercentStack';
import {StackedColumns} from '../examples/bar-x/StackedColumns';
import {TwoYAxis as BarXTwoYAxis} from '../examples/bar-x/TwoYAxis';
import {Basic as BasicBarY} from '../examples/bar-y/Basic';
import {GroupedColumns as GroupedColumnsBarY} from '../examples/bar-y/GroupedColumns';
import {NegativeValues as BarYNegativeValues} from '../examples/bar-y/NegativeValues';
import {PercentStackingBars} from '../examples/bar-y/PercentStacking';
import {StackedColumns as StackedColumnsBarY} from '../examples/bar-y/StackedColumns';
import {LineAndBarXCombinedChart} from '../examples/combined/LineAndBarX';
Expand Down Expand Up @@ -62,11 +65,11 @@ const ShowcaseStory = () => {
<Text variant="subheader-1">With data labels</Text>
<LineWithDataLabels />
</Col>
<Col s={12} m={12} l={6}>
<Col s={12} m={6} l={6}>
<Text variant="subheader-1">Lines with different shapes</Text>
<LinesWithShapes />
</Col>
<Col s={12} m={12} l={6}>
<Col s={12} m={6} l={6}>
<Text variant="subheader-1">Line with two Y axis</Text>
<LineTwoYAxis />
</Col>
Expand All @@ -87,10 +90,14 @@ const ShowcaseStory = () => {
<Text variant="subheader-1">Stacked percentage areas</Text>
<PercentStackingArea />
</Col>
<Col s={12} m={12}>
<Col s={12} m={6}>
<Text variant="subheader-1">Dual Y axis</Text>
<AreaTwoYAxis />
</Col>
<Col s={12} m={6}>
<Text variant="subheader-1">With negative values</Text>
<AreaNegativeValues />
</Col>
</Row>
<Row space={1}>
<Text variant="header-2">Bar-x charts</Text>
Expand All @@ -116,10 +123,14 @@ const ShowcaseStory = () => {
<Text variant="subheader-1">Bar-x chart with data labels</Text>
<BarXDataLabels />
</Col>
<Col s={12} m={12}>
<Col s={12} m={6}>
<Text variant="subheader-1">Dual Y axis</Text>
<BarXTwoYAxis />
</Col>
<Col s={12} m={6}>
<Text variant="subheader-1">Bar-x chart with negative values</Text>
<BarXNegativeValues />
</Col>
</Row>
<Row space={1}>
<Text variant="header-2">Bar-y charts</Text>
Expand All @@ -141,6 +152,10 @@ const ShowcaseStory = () => {
<Text variant="subheader-1">Stacked percentage bars</Text>
<PercentStackingBars />
</Col>
<Col s={12} m={6}>
<Text variant="subheader-1">Bar-y chart with negative values</Text>
<BarYNegativeValues />
</Col>
</Row>
<Row space={1}>
<Text variant="header-2">Pie charts</Text>
Expand All @@ -159,11 +174,11 @@ const ShowcaseStory = () => {
<Text variant="header-2">Scatter charts</Text>
</Row>
<Row space={3}>
<Col s={12} m={12} l={6}>
<Col s={12} m={6} l={6}>
<Text variant="subheader-1">Basic scatter</Text>
<BasicScatter />
</Col>
<Col s={12} m={12} l={6}>
<Col s={12} m={6} l={6}>
<Text variant="subheader-1">Scatter chart with two Y axis</Text>
<ScatterTwoYAxis />
</Col>
Expand Down
32 changes: 32 additions & 0 deletions src/plugins/d3/examples/area/NegativeValues.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import React from 'react';

import {ChartKit} from '../../../../components/ChartKit';
import type {ChartKitWidgetData} from '../../../../types';
import {ExampleWrapper} from '../ExampleWrapper';

export const NegativeValues = () => {
const data = [
{x: 0, y: 10},
{x: 1, y: 20},
{x: 2, y: -30},
{x: 3, y: 100},
];

const widgetData: ChartKitWidgetData = {
series: {
data: [
{
type: 'area',
data: data,
name: 'Min temperature',
},
],
},
};

return (
<ExampleWrapper>
<ChartKit type="d3" data={widgetData} />
</ExampleWrapper>
);
};
50 changes: 50 additions & 0 deletions src/plugins/d3/examples/bar-x/NegativeValues.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import React from 'react';

import {dateTime} from '@gravity-ui/date-utils';

import {ChartKit} from '../../../../components/ChartKit';
import type {ChartKitWidgetData} from '../../../../types';
import {ExampleWrapper} from '../ExampleWrapper';
import marsWeatherData from '../mars-weather';

export const NegativeValues = () => {
const data = marsWeatherData.map((d) => ({
x: dateTime({input: d.terrestrial_date, format: 'YYYY-MM-DD'}).valueOf(),
y: d.min_temp,
}));

const widgetData: ChartKitWidgetData = {
series: {
data: [
{
type: 'bar-x',
data: data,
name: 'Min temperature',
},
],
},
yAxis: [
{
title: {
text: 'Min temperature',
},
},
],
xAxis: {
type: 'datetime',
title: {
text: 'Terrestrial date',
},
ticks: {pixelInterval: 200},
},
title: {
text: 'Mars weather',
},
};

return (
<ExampleWrapper>
<ChartKit type="d3" data={widgetData} />
</ExampleWrapper>
);
};
49 changes: 49 additions & 0 deletions src/plugins/d3/examples/bar-y/NegativeValues.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import React from 'react';

import {dateTime} from '@gravity-ui/date-utils';

import {ChartKit} from '../../../../components/ChartKit';
import type {ChartKitWidgetData} from '../../../../types';
import {ExampleWrapper} from '../ExampleWrapper';
import marsWeatherData from '../mars-weather';

export const NegativeValues = () => {
const data = marsWeatherData.map((d) => ({
y: dateTime({input: d.terrestrial_date, format: 'YYYY-MM-DD'}).valueOf(),
x: d.min_temp,
}));

const widgetData: ChartKitWidgetData = {
series: {
data: [
{
type: 'bar-y',
data: data,
name: 'Min temperature',
},
],
},
xAxis: {
title: {
text: 'Min temperature',
},
},
yAxis: [
{
type: 'datetime',
title: {
text: 'Terrestrial date',
},
},
],
title: {
text: 'Mars weather',
},
};

return (
<ExampleWrapper>
<ChartKit type="d3" data={widgetData} />
</ExampleWrapper>
);
};
18 changes: 14 additions & 4 deletions src/plugins/d3/renderer/hooks/useAxisScales/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@ import get from 'lodash/get';
import {ChartKitWidgetAxis, ChartKitWidgetSeries} from '../../../../../types';
import {DEFAULT_AXIS_TYPE} from '../../constants';
import {
CHART_SERIES_WITH_VOLUME,
getDataCategoryValue,
getDefaultMaxXAxisValue,
getDomainDataXBySeries,
getDomainDataYBySeries,
getOnlyVisibleSeries,
Expand Down Expand Up @@ -71,9 +73,14 @@ export function createYScale(axis: PreparedAxis, series: PreparedSeries[], bound
const range = [boundsHeight, boundsHeight * axis.maxPadding];

if (isNumericalArrayData(domain)) {
const [domainYMin, yMax] = extent(domain) as [number, number];
const [domainYMin, domainMax] = extent(domain) as [number, number];
const yMinValue = typeof yMin === 'number' ? yMin : domainYMin;
return scaleLinear().domain([yMinValue, yMax]).range(range).nice();
let yMaxValue = domainMax;
if (series.some((s) => CHART_SERIES_WITH_VOLUME.includes(s.type))) {
yMaxValue = Math.max(yMaxValue, 0);
}

return scaleLinear().domain([yMinValue, yMaxValue]).range(range).nice();
}

break;
Expand Down Expand Up @@ -135,6 +142,7 @@ export function createXScale(
boundsWidth: number,
) {
const xMin = get(axis, 'min');
const xMax = getDefaultMaxXAxisValue(series);
const xType = get(axis, 'type', DEFAULT_AXIS_TYPE);
const xCategories = get(axis, 'categories');
const xTimestamps = get(axis, 'timestamps');
Expand All @@ -148,9 +156,11 @@ export function createXScale(
const domain = getDomainDataXBySeries(series);

if (isNumericalArrayData(domain)) {
const [domainXMin, xMax] = extent(domain) as [number, number];
const [domainXMin, domainXMax] = extent(domain) as [number, number];
const xMinValue = typeof xMin === 'number' ? xMin : domainXMin;
return scaleLinear().domain([xMinValue, xMax]).range(xRange).nice();
const xMaxValue =
typeof xMax === 'number' ? Math.max(xMax, domainXMax) : domainXMax;
return scaleLinear().domain([xMinValue, xMaxValue]).range(xRange).nice();
}

break;
Expand Down
10 changes: 7 additions & 3 deletions src/plugins/d3/renderer/hooks/useChartOptions/y-axis.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
yAxisTitleDefaults,
} from '../../constants';
import {
CHART_SERIES_WITH_VOLUME,
formatAxisTickLabel,
getClosestPointsRange,
getHorisontalSvgTextHeight,
Expand Down Expand Up @@ -51,9 +52,11 @@ const getAxisLabelMaxWidth = (args: {axis: PreparedAxis; series: ChartKitWidgetS

function getAxisMin(axis?: ChartKitWidgetAxis, series?: ChartKitWidgetSeries[]) {
const min = axis?.min;
const seriesWithVolume = ['bar-x', 'area', 'waterfall'];

if (typeof min === 'undefined' && series?.some((s) => seriesWithVolume.includes(s.type))) {
if (
typeof min === 'undefined' &&
series?.some((s) => CHART_SERIES_WITH_VOLUME.includes(s.type))
) {
return series.reduce((minValue, s) => {
switch (s.type) {
case 'waterfall': {
Expand All @@ -68,7 +71,8 @@ function getAxisMin(axis?: ChartKitWidgetAxis, series?: ChartKitWidgetSeries[])
return Math.min(minValue, minSubTotal);
}
default: {
return minValue;
const minYValue = s.data.reduce((res, d) => Math.min(res, get(d, 'y', 0)), 0);
return Math.min(minValue, minYValue);
}
}
}, 0);
Expand Down
2 changes: 1 addition & 1 deletion src/plugins/d3/renderer/hooks/useShapes/area/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ type Args = {
export const AreaSeriesShapes = (args: Args) => {
const {dispatcher, preparedData, seriesOptions} = args;

const ref = React.useRef<SVGGElement>(null);
const ref = React.useRef<SVGGElement | null>(null);

React.useEffect(() => {
if (!ref.current) {
Expand Down
24 changes: 19 additions & 5 deletions src/plugins/d3/renderer/hooks/useShapes/area/prepare-data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import {group, sort} from 'd3';

import type {AreaSeriesData} from '../../../../../../types';
import type {LabelData} from '../../../types';
import {getLabelsSize, getLeftPosition} from '../../../utils';
import {getDataCategoryValue, getLabelsSize, getLeftPosition} from '../../../utils';
import type {ChartScale} from '../../useAxisScales';
import type {PreparedAxis} from '../../useChartOptions/types';
import type {PreparedAreaSeries} from '../../useSeries/types';
Expand Down Expand Up @@ -40,9 +40,14 @@ function getLabelData(point: PointData, series: PreparedAreaSeries, xMax: number
}

function getXValues(series: PreparedAreaSeries[], xAxis: PreparedAxis, xScale: ChartScale) {
const categories = xAxis.categories || [];
const xValues = series.reduce<Map<string, number>>((acc, s) => {
s.data.forEach((d) => {
const key = String(d.x);
const key = String(
xAxis.type === 'category'
? getDataCategoryValue({axisDirection: 'x', categories, data: d})
: d.x,
);
if (!acc.has(key)) {
acc.set(key, getXValue({point: d, xAxis, xScale}));
}
Expand All @@ -51,7 +56,7 @@ function getXValues(series: PreparedAreaSeries[], xAxis: PreparedAxis, xScale: C
}, new Map());

if (xAxis.type === 'category') {
return (xAxis.categories || []).reduce<[string, number][]>((acc, category) => {
return categories.reduce<[string, number][]>((acc, category) => {
const xValue = xValues.get(category);
if (typeof xValue === 'number') {
acc.push([category, xValue]);
Expand Down Expand Up @@ -89,9 +94,18 @@ export const prepareAreaData = (args: {
const yAxisIndex = s.yAxis;
const seriesYAxis = yAxis[yAxisIndex];
const seriesYScale = yScale[yAxisIndex];
const [yMin, _yMax] = seriesYScale.range();
const yMin = getYValue({point: {y: 0}, yAxis: seriesYAxis, yScale: seriesYScale});
const seriesData = s.data.reduce<Map<string, AreaSeriesData>>((m, d) => {
return m.set(String(d.x), d);
const key = String(
xAxis.type === 'category'
? getDataCategoryValue({
axisDirection: 'x',
categories: xAxis.categories || [],
data: d,
})
: d.x,
);
return m.set(key, d);
}, new Map());
const points = xValues.reduce<PointData[]>((pointsAcc, [x, xValue]) => {
const accumulatedYValue = accumulatedYValues.get(x) || 0;
Expand Down
9 changes: 5 additions & 4 deletions src/plugins/d3/renderer/hooks/useShapes/bar-x/prepare-data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -153,12 +153,13 @@ export const prepareBarXData = (args: {
}

const x = xCenter - currentGroupWidth / 2 + (rectWidth + rectGap) * groupItemIndex;
const y = seriesYScale(yValue.data.y as number);
const height = plotHeight - y;

const yDataValue = yValue.data.y as number;
const y = seriesYScale(yDataValue);
const base = seriesYScale(0);
const height = yDataValue > 0 ? base - y : y - base;
const barData: PreparedBarXData = {
x,
y: y - stackHeight,
y: yDataValue > 0 ? y - stackHeight : seriesYScale(0),
width: rectWidth,
height,
opacity: get(yValue.data, 'opacity', null),
Expand Down
Loading

0 comments on commit 7180e39

Please sign in to comment.