Skip to content

Commit 1ac28b2

Browse files
committed
feat: added onMouseEnter and onMouseLeave callbacks to handle bar hover states, refactored waterfallchart component, util and hook files, fixed an issue where component was getting stuck when value 0 is passed for yAxisPixelsPerUnit
1 parent d5a6468 commit 1ac28b2

File tree

4 files changed

+87
-76
lines changed

4 files changed

+87
-76
lines changed

src/types/types.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { CSSProperties } from 'react';
22

33
export type IWaterfallGraphProps = {
4-
dataPoints: Array<ITransaction>;
4+
dataPoints: Array<IDataPoint>;
55
barWidth?: number;
66
showBridgeLines?: boolean;
77
showYAxisScaleLines?: boolean;
@@ -10,6 +10,8 @@ export type IWaterfallGraphProps = {
1010
summaryXLabel?: string;
1111
styles?: ICustomizationStyles;
1212
onChartClick?: IOnChartClick;
13+
onMouseEnter?: onMouseEnter;
14+
onMouseLeave?: onMouseLeave;
1315
};
1416

1517
export type ICustomizationStyles = {
@@ -18,7 +20,7 @@ export type ICustomizationStyles = {
1820
negativeBar?: CSSProperties;
1921
};
2022

21-
export type ITransaction = {
23+
export type IDataPoint = {
2224
label: string;
2325
value: number;
2426
};
@@ -48,6 +50,9 @@ export type ICalculateBarWidth = (graphWidth: number) => number;
4850

4951
export type IOnChartClick = (chartElement: IChartElement) => void;
5052

53+
export type onMouseEnter = (e: React.MouseEvent<SVGRectElement, MouseEvent>, chartElement: IChartElement) => void;
54+
export type onMouseLeave = (e: React.MouseEvent<SVGRectElement, MouseEvent>, chartElement: IChartElement) => void;
55+
5156
export enum chartTypes {
5257
transaction,
5358
summary

src/waterfall-chart/WaterFallChart.tsx

Lines changed: 42 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React, { FC, useEffect, useRef, useState } from 'react';
1+
import React, { FC, Fragment, useEffect, useRef, useState } from 'react';
22
import { IWaterfallGraphProps } from '../types/types';
33
import useWaterfallChart from './useWaterFallChart';
44
import classes from './styles.module.scss';
@@ -14,21 +14,30 @@ import {
1414
const WaterFallChart: FC<IWaterfallGraphProps> = (props) => {
1515
const {
1616
dataPoints,
17-
barWidth,
17+
barWidth: initialBarWidth = DEFAULT_BAR_WIDTH,
1818
showBridgeLines = true,
1919
showYAxisScaleLines = false,
20-
yAxisPixelsPerUnit = DEFAULT_PIXELS_PER_Y_UNIT,
20+
yAxisPixelsPerUnit: initialYAxisPixelsPerUnit,
2121
showFinalSummary = true,
2222
summaryXLabel = DEFAULT_SUMMARY_LABEL,
2323
styles = {},
24-
onChartClick
24+
onChartClick,
25+
onMouseEnter,
26+
onMouseLeave
2527
} = props;
2628

2729
const wrapperRef = useRef<HTMLDivElement | null>(null);
2830
const [wrapperHeight, setWrapperHeight] = useState(0);
29-
const [barWidthVal, setBarWidthVal] = useState(barWidth ?? DEFAULT_BAR_WIDTH);
31+
const [barWidth, setBarWidth] = useState(initialBarWidth);
32+
const yAxisPixelsPerUnit = initialYAxisPixelsPerUnit ? initialYAxisPixelsPerUnit : DEFAULT_PIXELS_PER_Y_UNIT;
3033

31-
const { chartElements, yValueForZeroLine, yAxisPoints, yAxisScale, calculateBarWidth } = useWaterfallChart(
34+
const {
35+
chartElements,
36+
yValueForZeroLine,
37+
yAxisPoints,
38+
yAxisScale,
39+
calculateBarWidth
40+
} = useWaterfallChart(
3241
dataPoints,
3342
wrapperHeight,
3443
yAxisPixelsPerUnit,
@@ -39,7 +48,9 @@ const WaterFallChart: FC<IWaterfallGraphProps> = (props) => {
3948
const onWrapperDimensionsChange = (): void => {
4049
if (wrapperRef.current) {
4150
setWrapperHeight(wrapperRef?.current?.offsetHeight);
42-
if (!barWidth || barWidth <= 0) setBarWidthVal(calculateBarWidth(wrapperRef?.current?.offsetWidth));
51+
if (!initialBarWidth || initialBarWidth <= 0) {
52+
setBarWidth(calculateBarWidth(wrapperRef?.current?.offsetWidth));
53+
}
4354
}
4455
};
4556

@@ -48,7 +59,8 @@ const WaterFallChart: FC<IWaterfallGraphProps> = (props) => {
4859
window.addEventListener('resize', onWrapperDimensionsChange);
4960

5061
return () => window.removeEventListener('resize', onWrapperDimensionsChange);
51-
}, [barWidth, calculateBarWidth]);
62+
// eslint-disable-next-line react-hooks/exhaustive-deps
63+
}, [initialBarWidth]);
5264

5365
const summaryValue = Math.abs(chartElements[chartElements?.length - 1]?.cumulativeSum);
5466
const summaryBarHeight = Math.abs((summaryValue / yAxisScale) * yAxisPixelsPerUnit);
@@ -61,7 +73,7 @@ const WaterFallChart: FC<IWaterfallGraphProps> = (props) => {
6173
};
6274

6375
return (
64-
<div ref={wrapperRef} className={classes.chartWrapper}>
76+
<div ref={wrapperRef} className={classes.chartWrapper} id='graph-svg-wrapper'>
6577
<svg className={classes.svgContainer}>
6678
{/* y-axis */}
6779
<line x1='0' y1='0' x2='0' y2='100%' className={classes.axisLines} id='yAxisLine' />
@@ -80,51 +92,54 @@ const WaterFallChart: FC<IWaterfallGraphProps> = (props) => {
8092
/>
8193
))}
8294
{chartElements?.map((chartElement, index) => (
83-
<>
95+
<Fragment key={`${chartElement?.name}-bar-graph`}>
8496
<rect
85-
key={`${chartElement?.name}-bar-graph`}
86-
width={barWidthVal}
97+
width={barWidth}
8798
height={chartElement?.barHeight}
8899
y={chartElement?.yVal}
89-
x={(2 * index + 1) * barWidthVal}
90-
className={`${classes.graphBar} ${chartElement?.value >= 0 ? classes.positiveGraph : classes.negativeGraph}`}
100+
x={(2 * index + 1) * barWidth}
101+
className={`${classes.graphBar} ${
102+
chartElement?.value >= 0 ? classes.positiveGraph : classes.negativeGraph
103+
}`}
91104
style={chartElement?.value >= 0 ? styles?.positiveBar : styles?.negativeBar}
92105
onClick={(): void => onChartClick && onChartClick(chartElement)}
106+
onMouseEnter={(e: React.MouseEvent<SVGRectElement, MouseEvent>):void => onMouseEnter && onMouseEnter(e, chartElement)}
107+
onMouseLeave={(e: React.MouseEvent<SVGRectElement, MouseEvent>):void => onMouseLeave && onMouseLeave(e, chartElement)}
93108
id={`chartBar-${index}`}
109+
data-testid={`data-point`}
94110
/>
95-
{showBridgeLines && (showFinalSummary || index !== chartElements?.length - 1) && (
111+
{showBridgeLines &&
112+
(showFinalSummary || index !== chartElements?.length - 1) && (
96113
<line
97114
key={`${chartElement?.name}-bridge-line`}
98115
className={classes.bridgeLine}
99-
x1={(2 * index + 2) * barWidthVal}
116+
x1={(2 * index + 2) * barWidth}
100117
y1={yValueForZeroLine - (chartElement?.cumulativeSum / yAxisScale) * yAxisPixelsPerUnit}
101-
x2={(2 * index + 3) * barWidthVal}
118+
x2={(2 * index + 3) * barWidth}
102119
y2={yValueForZeroLine - (chartElement?.cumulativeSum / yAxisScale) * yAxisPixelsPerUnit}
103120
id={`chartBarBridgeLine-${index}`}
104121
/>
105122
)}
106-
</>
123+
</Fragment>
107124
))}
108125
{showFinalSummary && summaryBarHeight > 0 && (
109126
<rect
110127
key={FINAL_SUMMARY_GRAPH_KEY}
111-
width={barWidthVal}
128+
width={barWidth}
112129
height={summaryChartElement?.barHeight}
113130
y={summaryChartElement?.yVal}
114-
x={(2 * chartElements?.length + 1) * barWidthVal}
131+
x={(2 * chartElements?.length + 1) * barWidth}
115132
className={`${classes.graphBar} ${classes.summaryGraphBar}`}
116133
onClick={(): void => onChartClick && onChartClick(summaryChartElement)}
117134
id='summaryBar'
135+
onMouseEnter={(e: React.MouseEvent<SVGRectElement, MouseEvent>):void => onMouseEnter && onMouseEnter(e, summaryChartElement)}
136+
onMouseLeave={(e: React.MouseEvent<SVGRectElement, MouseEvent>):void => onMouseLeave && onMouseLeave(e, summaryChartElement)}
118137
/>
119138
)}
120139
</svg>
121140
<div className={classes.yPoints}>
122141
{yAxisPoints?.map((yAxisPoint, index) => (
123-
<div
124-
key={yAxisPoint}
125-
className={classes.yPoint}
126-
style={{ bottom: index * yAxisPixelsPerUnit - 7 }}
127-
>
142+
<div key={yAxisPoint} className={classes.yPoint} style={{ bottom: index * yAxisPixelsPerUnit - 7 }}>
128143
{yAxisPoint}
129144
</div>
130145
))}
@@ -134,7 +149,7 @@ const WaterFallChart: FC<IWaterfallGraphProps> = (props) => {
134149
<div
135150
key={transaction?.label}
136151
className={classes.xPoint}
137-
style={{ left: (2 * index + 1.25) * barWidthVal }}
152+
style={{ left: (2 * index + 1.25) * barWidth }}
138153
// the 1.25 is to reduce chances for the label to overflow to right
139154
>
140155
{transaction?.label}
@@ -144,7 +159,7 @@ const WaterFallChart: FC<IWaterfallGraphProps> = (props) => {
144159
<div
145160
key={FINAL_SUMMARY_X_LABEL_KEY}
146161
className={classes.xPoint}
147-
style={{ ...styles?.summaryBar, left: (2 * chartElements?.length + 1.25) * barWidthVal }}
162+
style={{ ...styles?.summaryBar, left: (2 * chartElements?.length + 1.25) * barWidth }}
148163
>
149164
{summaryXLabel}
150165
</div>
Lines changed: 35 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,60 +1,51 @@
1-
import { IChartElement, ITransaction, IUseWaterfallChartReturnType } from '../types/types';
1+
import { IChartElement, IDataPoint, IUseWaterfallChartReturnType } from '../types/types';
22
import { getIntervalAndYPoints, getLargestCumulativeSum, getSmallestCumulativeSum } from './utils';
33

44
const useWaterfallChart = (
5-
transactions: Array<ITransaction>,
5+
dataPoints: Array<IDataPoint>,
66
chartHeight: number,
77
yAxisPixelsPerUnit: number,
88
showFinalSummary: boolean
99
): IUseWaterfallChartReturnType => {
10-
const largestCumulativeVal = getLargestCumulativeSum(transactions); // this will be the highest y point in the graph
11-
const smallestCumulativeVal = getSmallestCumulativeSum(transactions);
12-
let chartElements: Array<IChartElement> = [];
13-
14-
const maxLabelsCount = Math.ceil(chartHeight / yAxisPixelsPerUnit);
15-
16-
let yAxisPoints: Array<number> = [];
17-
let yAxisScale = 0;
18-
let lowestYAxisValue = 0;
19-
let yValueForZeroLine = 0;
20-
21-
if (chartHeight && chartHeight > 0) {
22-
const InterValAndYPoints = getIntervalAndYPoints(smallestCumulativeVal, largestCumulativeVal, maxLabelsCount);
23-
yAxisPoints = InterValAndYPoints?.yAxisPoints;
24-
yAxisScale = InterValAndYPoints?.yAxisScale;
25-
lowestYAxisValue = InterValAndYPoints?.yAxisPoints[0];
26-
// yAxisScale is the number of Y units per 30px
27-
// lowestYAxisValue is the yAxisValue for origin (0, 0)
28-
29-
yValueForZeroLine = chartHeight - (Math.abs(lowestYAxisValue) / yAxisScale) * yAxisPixelsPerUnit;
30-
let cumulativeSum = 0;
31-
32-
chartElements = transactions.map((transaction) => {
33-
const { label, value } = transaction;
34-
let yVal = 0;
35-
const barHeight = (value / yAxisScale) * yAxisPixelsPerUnit;
36-
const offsetHeight = (cumulativeSum / yAxisScale) * yAxisPixelsPerUnit;
37-
// minimum distance from zero line to the floating bar for the transaction
38-
if (value < 0) {
39-
yVal = yValueForZeroLine - offsetHeight;
40-
} else yVal = yValueForZeroLine - (offsetHeight + barHeight);
41-
42-
cumulativeSum += value;
43-
44-
return { name: label, value, yVal, cumulativeSum, barHeight: Math.abs(barHeight) };
45-
});
10+
if (chartHeight <= 0) {
11+
return {
12+
chartElements: [],
13+
yValueForZeroLine: 0,
14+
yAxisPoints: [],
15+
yAxisScale: 0,
16+
calculateBarWidth: () => 0
17+
};
4618
}
4719

20+
const largestCumulativeVal = getLargestCumulativeSum(dataPoints);
21+
const smallestCumulativeVal = getSmallestCumulativeSum(dataPoints);
22+
23+
const { yAxisPoints, yAxisScale } = getIntervalAndYPoints(smallestCumulativeVal, largestCumulativeVal, Math.ceil(chartHeight / yAxisPixelsPerUnit));
24+
const lowestYAxisValue = yAxisPoints[0];
25+
const yValueForZeroLine = chartHeight - (Math.abs(lowestYAxisValue) / yAxisScale) * yAxisPixelsPerUnit;
26+
27+
let cumulativeSum = 0;
28+
const chartElements: Array<IChartElement> = dataPoints.map((dataPoint) => {
29+
const { label, value } = dataPoint;
30+
const barHeight = (value / yAxisScale) * yAxisPixelsPerUnit;
31+
const offsetHeight = (cumulativeSum / yAxisScale) * yAxisPixelsPerUnit;
32+
const yVal = value < 0 ? yValueForZeroLine - offsetHeight : yValueForZeroLine - (offsetHeight + barHeight);
33+
34+
cumulativeSum += value;
35+
36+
return { name: label, value, yVal, cumulativeSum, barHeight: Math.abs(barHeight) };
37+
});
38+
4839
const calculateBarWidth = (chartWidth: number): number => {
49-
let barWidth = 0;
50-
if (chartWidth && transactions?.length > 0) {
51-
if (showFinalSummary) barWidth = chartWidth / (2 * transactions?.length + 2);
52-
else barWidth = chartWidth / (2 * transactions?.length + 1);
40+
if (chartWidth <= 0 || dataPoints.length === 0) {
41+
return 0;
5342
}
54-
return barWidth;
43+
44+
const divisor = showFinalSummary ? 2 * dataPoints.length + 2 : 2 * dataPoints.length + 1;
45+
return chartWidth / divisor;
5546
};
5647

5748
return { chartElements, yValueForZeroLine, yAxisPoints, yAxisScale, calculateBarWidth };
5849
};
5950

60-
export default useWaterfallChart;
51+
export default useWaterfallChart;

src/waterfall-chart/utils.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
import { IGetIntervalAndYPointsReturnType, ITransaction } from '../types/types';
1+
import { IGetIntervalAndYPointsReturnType, IDataPoint } from '../types/types';
22

3-
export function getLargestCumulativeSum(arr: Array<ITransaction>): number {
3+
export function getLargestCumulativeSum(arr: Array<IDataPoint>): number {
44
let maxSum = arr[0]?.value; // Initialize maxSum and currentSum with the first element of the array
55
let currentSum = arr[0]?.value;
66

@@ -13,7 +13,7 @@ export function getLargestCumulativeSum(arr: Array<ITransaction>): number {
1313
return maxSum;
1414
}
1515

16-
export function getSmallestCumulativeSum(arr: Array<ITransaction>): number {
16+
export function getSmallestCumulativeSum(arr: Array<IDataPoint>): number {
1717
let minSum = arr[0]?.value; // Initialize minSum and currentSum with the first element of the array
1818
let currentSum = arr[0]?.value;
1919

0 commit comments

Comments
 (0)