Skip to content

Commit

Permalink
Calculate offset types (#3843)
Browse files Browse the repository at this point in the history
  • Loading branch information
PavelVanecek authored Oct 12, 2023
1 parent ffb6b4d commit 4152b56
Show file tree
Hide file tree
Showing 6 changed files with 336 additions and 88 deletions.
39 changes: 26 additions & 13 deletions src/chart/generateCategoricalChart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,8 @@ import { AccessibilityManager } from './AccessibilityManager';
import { isDomainSpecifiedByUser } from '../util/isDomainSpecifiedByUser';
import { deferer, CancelFunction } from '../util/deferer';
import { getActiveShapeIndexForTooltip, isFunnel, isPie, isScatter } from '../util/ActiveShapeUtils';
import { Props as YAxisProps } from '../cartesian/YAxis';
import { Props as XAxisProps } from '../cartesian/XAxis';

export type GraphicalItem<Props = Record<string, any>> = ReactElement<
Props,
Expand Down Expand Up @@ -654,24 +656,34 @@ const getAxisNameByLayout = (layout: LayoutType) => {

/**
* Calculate the offset of main part in the svg element
* @param {Object} props Latest props
* graphicalItems The instances of item
* xAxisMap The configuration of x-axis
* yAxisMap The configuration of y-axis
* @param {Object} prevLegendBBox the boundary box of legend
* @param {Object} params.props Latest props
* @param {Array} params.graphicalItems The instances of item
* @param {Object} params.xAxisMap The configuration of x-axis
* @param {Object} params.yAxisMap The configuration of y-axis
* @param {Object} prevLegendBBox The boundary box of legend
* @return {Object} The offset of main part in the svg element
*/
const calculateOffset = (
{ props, graphicalItems, xAxisMap = {} as BaseAxisProps, yAxisMap = {} as BaseAxisProps }: any,
prevLegendBBox?: any,
) => {
{
props,
graphicalItems,
xAxisMap = {},
yAxisMap = {},
}: {
props: CategoricalChartProps;
graphicalItems: Array<ReactElement>;
xAxisMap?: { [axisId: string]: XAxisProps };
yAxisMap?: { [axisId: string]: YAxisProps };
},
prevLegendBBox?: DOMRect | null,
): ChartOffset => {
const { width, height, children } = props;
const margin = props.margin || {};
const brushItem = findChildByType(children, Brush);
const legendItem = findChildByType(children, Legend);

const offsetH = Object.keys(yAxisMap).reduce(
(result: any, id: any) => {
(result, id) => {
const entry = yAxisMap[id];
const { orientation } = entry;

Expand All @@ -698,7 +710,7 @@ const calculateOffset = (
{ top: margin.top || 0, bottom: margin.bottom || 0 },
);

let offset = { ...offsetV, ...offsetH };
let offset: ChartOffset = { ...offsetV, ...offsetH };

const brushBottom = offset.bottom;

Expand All @@ -707,6 +719,7 @@ const calculateOffset = (
}

if (legendItem && prevLegendBBox) {
// @ts-expect-error margin is optional in props but required in appendOffsetOfLegend
offset = appendOffsetOfLegend(offset, graphicalItems, props, prevLegendBBox);
}

Expand Down Expand Up @@ -780,7 +793,7 @@ export interface CategoricalChartState {

yValue?: number;

legendBBox?: any;
legendBBox?: DOMRect | null;

prevData?: any[];
prevWidth?: number;
Expand Down Expand Up @@ -999,7 +1012,7 @@ export const generateCategoricalChart = ({
};
}, {});

const offset = calculateOffset({ ...axisObj, props, graphicalItems }, prevState?.legendBBox);
const offset: ChartOffset = calculateOffset({ ...axisObj, props, graphicalItems }, prevState?.legendBBox);

Object.keys(axisObj).forEach(key => {
axisObj[key] = formatAxisMap(props, axisObj[key], offset, key.replace('Map', ''), chartName);
Expand Down Expand Up @@ -1447,7 +1460,7 @@ export const generateCategoricalChart = ({
}
};

handleLegendBBoxUpdate = (box: any) => {
handleLegendBBoxUpdate = (box: DOMRect | null) => {
if (box) {
const { dataStartIndex, dataEndIndex, updateId } = this.state;

Expand Down
105 changes: 31 additions & 74 deletions src/util/ChartUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,8 @@ import { ReactElement, ReactNode } from 'react';
import { getNiceTickValues, getTickValuesFixedDomain } from 'recharts-scale';

import { ErrorBar } from '../cartesian/ErrorBar';
import { Legend, Props as LegendProps } from '../component/Legend';
import { findEntryInArray, getPercentValue, isNumber, isNumOrStr, mathSign, uniqueId } from './DataUtils';
import { filterProps, findAllByType, findChildByType, getDisplayName } from './ReactUtils';
import { filterProps, findAllByType, getDisplayName } from './ReactUtils';
// TODO: Cause of circular dependency. Needs refactor.
// import { RadiusAxisProps, AngleAxisProps } from '../polar/types';
import {
Expand All @@ -29,8 +28,13 @@ import {
TickItem,
CategoricalDomain,
StackOffsetType,
Margin,
ChartOffset,
} from './types';
import { Payload as LegendPayload } from '../component/DefaultLegendContent';
import { getLegendProps } from './getLegendProps';

// Exported for backwards compatibility
export { getLegendProps };

export function getValueByDataKey<T>(obj: T, dataKey: DataKey<T>, defaultValue?: any) {
if (_.isNil(obj) || _.isNil(dataKey)) {
Expand Down Expand Up @@ -193,67 +197,6 @@ export interface FormattedGraphicalItem {
item: ReactElement<{ legendType?: LegendType; hide: boolean; name?: string; dataKey: unknown }>;
}

interface SectorOrDataEntry {
name: any;
fill: any;
}

export const getLegendProps = ({
children,
formattedGraphicalItems,
legendWidth,
legendContent,
}: {
children: ReactNode[];
formattedGraphicalItems?: Array<FormattedGraphicalItem>;
legendWidth: number;
legendContent?: 'children';
}): LegendProps & { item: ReactElement } => {
const legendItem = findChildByType(children, Legend);
if (!legendItem) {
return null;
}

let legendData: LegendPayload[];
if (legendItem.props && legendItem.props.payload) {
legendData = legendItem.props && legendItem.props.payload;
} else if (legendContent === 'children') {
legendData = (formattedGraphicalItems || []).reduce((result, { item, props }) => {
const data: ReadonlyArray<SectorOrDataEntry> = props.sectors || props.data || [];

return result.concat(
data.map((entry: SectorOrDataEntry) => ({
type: legendItem.props.iconType || item.props.legendType,
value: entry.name,
color: entry.fill,
payload: entry,
})),
);
}, []);
} else {
legendData = (formattedGraphicalItems || []).map(({ item }): LegendPayload => {
const { dataKey, name, legendType, hide } = item.props;

return {
inactive: hide,
dataKey,
type: legendItem.props.iconType || legendType || 'square',
color: getMainColorOfGraphicItem(item),
value: name || dataKey,
// @ts-expect-error property strokeDasharray is required in Payload but optional in props
payload: item.props,
};
});
}

return {
...legendItem.props,
...Legend.getWithHeight(legendItem, legendWidth),
payload: legendData,
item: legendItem,
};
};

export type BarSetup = {
barSize: number | string;
stackList: ReadonlyArray<ReactElement>;
Expand Down Expand Up @@ -437,27 +380,41 @@ export const getBarPosition = ({
return result;
};

export const appendOffsetOfLegend = (offset: any, items: Array<FormattedGraphicalItem>, props: any, legendBox: any) => {
export const appendOffsetOfLegend = (
offset: ChartOffset,
_unused: unknown,
props: {
width?: number;
margin: Margin;
children?: ReactNode[];
},
legendBox: DOMRect | null,
): ChartOffset => {
const { children, width, margin } = props;
const legendWidth = width - (margin.left || 0) - (margin.right || 0);
// const legendHeight = height - (margin.top || 0) - (margin.bottom || 0);
const legendProps = getLegendProps({ children, legendWidth });
let newOffset = offset;

if (legendProps) {
const box = legendBox || {};
const { width: boxWidth, height: boxHeight } = legendBox || {};
const { align, verticalAlign, layout } = legendProps;

if ((layout === 'vertical' || (layout === 'horizontal' && verticalAlign === 'middle')) && isNumber(offset[align])) {
newOffset = { ...offset, [align]: newOffset[align] + (box.width || 0) };
if (
(layout === 'vertical' || (layout === 'horizontal' && verticalAlign === 'middle')) &&
align !== 'center' &&
isNumber(offset[align])
) {
return { ...offset, [align]: offset[align] + (boxWidth || 0) };
}

if ((layout === 'horizontal' || (layout === 'vertical' && align === 'center')) && isNumber(offset[verticalAlign])) {
newOffset = { ...offset, [verticalAlign]: newOffset[verticalAlign] + (box.height || 0) };
if (
(layout === 'horizontal' || (layout === 'vertical' && align === 'center')) &&
verticalAlign !== 'middle' &&
isNumber(offset[verticalAlign])
) {
return { ...offset, [verticalAlign]: offset[verticalAlign] + (boxHeight || 0) };
}
}

return newOffset;
return offset;
};

const isErrorBarRelevantForAxis = (layout?: LayoutType, axisType?: AxisType, direction?: 'x' | 'y'): boolean => {
Expand Down
66 changes: 66 additions & 0 deletions src/util/getLegendProps.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { ReactNode, ReactElement } from 'react';
import { Legend, Props as LegendProps } from '../component/Legend';
import { FormattedGraphicalItem, getMainColorOfGraphicItem } from './ChartUtils';
import { findChildByType } from './ReactUtils';
import { Payload as LegendPayload } from '../component/DefaultLegendContent';

interface SectorOrDataEntry {
name: any;
fill: any;
}

export const getLegendProps = ({
children,
formattedGraphicalItems,
legendWidth,
legendContent,
}: {
children: ReactNode[];
formattedGraphicalItems?: Array<FormattedGraphicalItem>;
legendWidth: number;
legendContent?: 'children';
}): null | (LegendProps & { item: ReactElement }) => {
const legendItem = findChildByType(children, Legend);
if (!legendItem) {
return null;
}

let legendData: LegendPayload[];
if (legendItem.props && legendItem.props.payload) {
legendData = legendItem.props && legendItem.props.payload;
} else if (legendContent === 'children') {
legendData = (formattedGraphicalItems || []).reduce((result, { item, props }) => {
const data: ReadonlyArray<SectorOrDataEntry> = props.sectors || props.data || [];

return result.concat(
data.map((entry: SectorOrDataEntry) => ({
type: legendItem.props.iconType || item.props.legendType,
value: entry.name,
color: entry.fill,
payload: entry,
})),
);
}, []);
} else {
legendData = (formattedGraphicalItems || []).map(({ item }): LegendPayload => {
const { dataKey, name, legendType, hide } = item.props;

return {
inactive: hide,
dataKey,
type: legendItem.props.iconType || legendType || 'square',
color: getMainColorOfGraphicItem(item),
value: name || dataKey,
// @ts-expect-error property strokeDasharray is required in Payload but optional in props
payload: item.props,
};
});
}

return {
...legendItem.props,
...Legend.getWithHeight(legendItem, legendWidth),
payload: legendData,
item: legendItem,
};
};
7 changes: 7 additions & 0 deletions test/util/ChartUtils.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
MIN_VALUE_REG,
parseSpecifiedDomain,
getTicksOfAxis,
getLegendProps,
} from '../../src/util/ChartUtils';
import { BaseAxisProps, DataKey } from '../../src/util/types';

Expand Down Expand Up @@ -613,3 +614,9 @@ describe('getDomainOfErrorBars', () => {
});
});
});

describe('exports for backwards-compatibility', () => {
test('getLegendProps should be exported', () => {
expect(getLegendProps).toBeInstanceOf(Function);
});
});
Loading

0 comments on commit 4152b56

Please sign in to comment.