Skip to content

Commit b433224

Browse files
committed
Added basic customization props
1 parent c06eb0f commit b433224

File tree

6 files changed

+151
-38
lines changed

6 files changed

+151
-38
lines changed

src/constants.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,9 @@
11
export const DEFAULT_BAR_WIDTH = 50;
22

33
export const DEFAULT_PIXELS_PER_Y_UNIT = 30;
4+
5+
export const DEFAULT_SUMMARY_LABEL = 'Total';
6+
7+
export const FINAL_SUMMARY_GRAPH_KEY = 'final-summary-bar';
8+
9+
export const FINAL_SUMMARY_X_LABEL_KEY = 'final-summary-x-point'

src/stories/Component.stories.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ export default {
1212
} as ComponentMeta<typeof Component>;
1313

1414
const Template: ComponentStory<typeof Component> = (args) => (
15-
<div style={{ width: '100%', height: '100%', padding: '10% 20%' }}>
15+
<div style={{ width: '100%', height: '100%', padding: '100px 200px' }}>
1616
<Component {...args} />
1717
</div>
1818
);

src/types/types.d.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,17 @@
1+
import { CSSProperties } from "react";
2+
13
export type IWaterfallGraphProps = {
24
transactions: Array<ITransaction>;
35
barWidth?: number;
6+
showBridgeLines? : boolean;
7+
showYAxisScaleLines?: boolean;
8+
yAxisPixelsPerUnit?: number;
9+
showFinalSummary?: boolean;
10+
summaryXLabel?: string;
11+
summaryBarStyles?: CSSProperties;
12+
positiveBarStyles?: CSSProperties;
13+
negativeBarStyles?: CSSProperties;
14+
onChartClick?: IOnChartClick;
415
};
516

617
export type ITransaction = {
@@ -14,6 +25,7 @@ export type IChartElement = {
1425
value: number;
1526
yVal: number;
1627
cumulativeSum: number;
28+
barHeight: number;
1729
};
1830

1931
export type IUseWaterfallChartReturnType = {
@@ -30,3 +42,10 @@ export type IGetIntervalAndYPointsReturnType = {
3042
};
3143

3244
export type ICalculateBarWidth = (graphWidth: number) => number;
45+
46+
export type IOnChartClick = (chartElement: IChartElement | { name: 'summary'}) => void;
47+
48+
export enum chartTypes {
49+
transaction,
50+
summary
51+
}

src/waterfall-chart/WaterFallChart.tsx

Lines changed: 93 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -3,25 +3,44 @@ import { IWaterfallGraphProps } from '../types/types';
33
import { useWaterfallChart } from './utils';
44
import styles from './styles.module.scss';
55
import '../index.css';
6-
import { DEFAULT_BAR_WIDTH, DEFAULT_PIXELS_PER_Y_UNIT } from '../constants';
6+
import { DEFAULT_BAR_WIDTH,
7+
DEFAULT_PIXELS_PER_Y_UNIT,
8+
DEFAULT_SUMMARY_LABEL,
9+
FINAL_SUMMARY_GRAPH_KEY,
10+
FINAL_SUMMARY_X_LABEL_KEY
11+
} from '../constants';
712

813
const WaterFallChart: FC<IWaterfallGraphProps> = (props) => {
9-
const { transactions, barWidth } = props;
14+
const {
15+
transactions,
16+
barWidth,
17+
showBridgeLines = true,
18+
showYAxisScaleLines = false,
19+
yAxisPixelsPerUnit = DEFAULT_PIXELS_PER_Y_UNIT,
20+
showFinalSummary = true,
21+
summaryXLabel = DEFAULT_SUMMARY_LABEL,
22+
summaryBarStyles = {},
23+
positiveBarStyles = {},
24+
negativeBarStyles = {},
25+
onChartClick
26+
} = props;
1027

1128
const wrapperRef = useRef<HTMLDivElement | null>(null);
1229
const [wrapperHeight, setWrapperHeight] = useState(0);
1330
const [barWidthVal, setBarWidthVal] = useState(barWidth ?? DEFAULT_BAR_WIDTH);
1431

1532
const { chartElements, yValueForZeroLine, yAxisPoints, yAxisScale, calculateBarWidth } = useWaterfallChart(
1633
transactions,
17-
wrapperHeight
34+
wrapperHeight,
35+
yAxisPixelsPerUnit,
36+
showFinalSummary
1837
);
1938

2039
useEffect(() => {
2140
const onWrapperDimensionsChange = (): void => {
2241
if (wrapperRef.current) {
2342
setWrapperHeight(wrapperRef?.current?.offsetHeight);
24-
if (!barWidth) setBarWidthVal(calculateBarWidth(wrapperRef?.current?.offsetWidth));
43+
if (!barWidth || barWidth <= 0 ) setBarWidthVal(calculateBarWidth(wrapperRef?.current?.offsetWidth));
2544
}
2645
};
2746

@@ -32,21 +51,45 @@ const WaterFallChart: FC<IWaterfallGraphProps> = (props) => {
3251
return () => window.removeEventListener('resize', onWrapperDimensionsChange);
3352
}, [barWidth, calculateBarWidth]);
3453

54+
const renderSummaryBar = (): JSX.Element => {
55+
const value = Math.abs(chartElements[chartElements?.length - 1]?.cumulativeSum);
56+
const barHeight = Math.abs((value/ yAxisScale) * yAxisPixelsPerUnit);
57+
const chartElement = {
58+
name: summaryXLabel,
59+
value,
60+
yVal: yValueForZeroLine - (value / yAxisScale) * yAxisPixelsPerUnit,
61+
cumulativeSum: 0,
62+
barHeight: barHeight
63+
}
64+
65+
return (
66+
<rect
67+
key={FINAL_SUMMARY_GRAPH_KEY}
68+
width={barWidthVal}
69+
height={chartElement?.barHeight}
70+
y={chartElement?.yVal}
71+
x={(2 * chartElements?.length + 1 ) * barWidthVal}
72+
className={`${styles.graphBar} ${styles.summaryGraphBar}`}
73+
onClick={(): void => onChartClick && onChartClick(chartElement)}
74+
/>
75+
)
76+
};
77+
3578
return (
3679
<div ref={wrapperRef} className={styles.chartWrapper}>
3780
<svg className={styles.svgContainer}>
3881
{/* y-axis */}
3982
<line x1='0' y1='0' x2='0' y2='100%' className={styles.axisLines} />
4083
{/* x-axis */}
4184
<line x1='0' y1='100%' x2='100%' y2='100%' className={styles.axisLines} />
42-
{/* zero line if negative values present */}
43-
{yAxisPoints?.map((yPoint, index) => (
85+
{/*y axis scale lines */}
86+
{showYAxisScaleLines && yAxisPoints?.map((yPoint, index) => (
4487
<line
4588
key={yPoint}
4689
x1='0'
47-
y1={wrapperHeight - index * DEFAULT_PIXELS_PER_Y_UNIT}
90+
y1={wrapperHeight - index * yAxisPixelsPerUnit}
4891
x2='100%'
49-
y2={wrapperHeight - index * DEFAULT_PIXELS_PER_Y_UNIT}
92+
y2={wrapperHeight - index * yAxisPixelsPerUnit}
5093
className={`${styles.axisLines}`}
5194
/>
5295
))}
@@ -55,37 +98,61 @@ const WaterFallChart: FC<IWaterfallGraphProps> = (props) => {
5598
<rect
5699
key={chartElement?.name}
57100
width={barWidthVal}
58-
height={(Math.abs(chartElement?.value) / yAxisScale) * DEFAULT_PIXELS_PER_Y_UNIT}
101+
height={chartElement?.barHeight}
59102
y={chartElement?.yVal}
60103
x={(2 * index + 1) * barWidthVal}
61104
className={`${styles.graphBar} ${chartElement?.value >= 0 ? styles.positiveGraph : styles.negativeGraph}`}
105+
style={chartElement?.value >= 0 ? positiveBarStyles : negativeBarStyles}
106+
onClick={(): void => onChartClick && onChartClick(chartElement)}
62107
/>
63-
<line
64-
key={chartElement?.name}
65-
x1={(2 * index + 2) * barWidthVal}
66-
y1={
67-
wrapperHeight -
68-
(chartElement?.cumulativeSum / yAxisScale) * DEFAULT_PIXELS_PER_Y_UNIT -
69-
(wrapperHeight - yValueForZeroLine)
70-
}
71-
x2={(2 * index + 3) * barWidthVal}
72-
y2={
73-
wrapperHeight -
74-
(chartElement?.cumulativeSum / yAxisScale) * DEFAULT_PIXELS_PER_Y_UNIT -
75-
(wrapperHeight - yValueForZeroLine)
76-
}
77-
className={styles.bridgeLine}
78-
/>
108+
{showBridgeLines
109+
&& (showFinalSummary
110+
||index !== chartElements?.length - 1)
111+
&& (
112+
<line
113+
key={chartElement?.name}
114+
className={styles.bridgeLine}
115+
x1={(2 * index + 2) * barWidthVal}
116+
y1={yValueForZeroLine - (chartElement?.cumulativeSum / yAxisScale) * yAxisPixelsPerUnit}
117+
x2={(2 * index + 3) * barWidthVal}
118+
y2={yValueForZeroLine - (chartElement?.cumulativeSum / yAxisScale) * yAxisPixelsPerUnit}
119+
/>
120+
)}
79121
</>
80122
))}
123+
{showFinalSummary && renderSummaryBar()}
81124
</svg>
82125
<div className={styles.yPoints}>
83126
{yAxisPoints?.map((yAxisPoint, index) => (
84-
<div key={yAxisPoint} className={styles.yPoint} style={{ bottom: index * DEFAULT_PIXELS_PER_Y_UNIT - 8 }}>
127+
<div
128+
key={yAxisPoint}
129+
className={styles.yPoint}
130+
style={{ bottom: index * yAxisPixelsPerUnit - 8 }}
131+
>
85132
{yAxisPoint}
86133
</div>
87134
))}
88135
</div>
136+
<div className={styles.xPoints}>
137+
{transactions?.map((transaction, index) => (
138+
<div
139+
key={transaction?.label}
140+
className={styles.xPoint}
141+
style={{ left: (2 * index + 1.25) * barWidthVal }}
142+
>
143+
{transaction?.label}
144+
</div>
145+
))}
146+
{showFinalSummary && (
147+
<div
148+
key={FINAL_SUMMARY_X_LABEL_KEY}
149+
className={styles.xPoint}
150+
style={{ ...summaryBarStyles, left: (2 * chartElements?.length + 1.25) * barWidthVal }}
151+
>
152+
{summaryXLabel}
153+
</div>
154+
)}
155+
</div>
89156
</div>
90157
);
91158
};

src/waterfall-chart/styles.module.scss

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,10 @@
3232
z-index: 1;
3333
transition: all 0.3s ease-in-out;
3434
}
35+
.summaryGraphBar {
36+
fill: #076aff;
37+
stroke: #076aff;
38+
}
3539
.positiveGraph {
3640
fill: #00b050;
3741
stroke: #00b050;
@@ -58,3 +62,16 @@
5862
text-align: right;
5963
}
6064
}
65+
66+
.xPoints {
67+
position: relative;
68+
height: 0px;
69+
.xPoint {
70+
position: absolute;
71+
font-size: 11px;
72+
line-height: 14px;
73+
color: #545453;
74+
top: 5px;
75+
text-align: center;
76+
}
77+
}

src/waterfall-chart/utils.ts

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import { DEFAULT_BAR_WIDTH, DEFAULT_PIXELS_PER_Y_UNIT } from '../constants';
21
import {
32
IChartElement,
43
IGetIntervalAndYPointsReturnType,
@@ -8,13 +7,15 @@ import {
87

98
export const useWaterfallChart = (
109
transactions: Array<ITransaction>,
11-
chartHeight: number
10+
chartHeight: number,
11+
yAxisPixelsPerUnit: number,
12+
showFinalSummary: boolean
1213
): IUseWaterfallChartReturnType => {
1314
const largestCumulativeVal = getLargestCumulativeSum(transactions); // this will be the highest y point in the graph
1415
const smallestCumulativeVal = getSmallestCumulativeSum(transactions);
1516
let chartElements: Array<IChartElement> = [];
1617

17-
const maxLabelsCount = Math.ceil(chartHeight / DEFAULT_PIXELS_PER_Y_UNIT);
18+
const maxLabelsCount = Math.ceil(chartHeight / yAxisPixelsPerUnit);
1819

1920
let yAxisPoints: Array<number> = [];
2021
let yAxisScale = 0;
@@ -29,28 +30,31 @@ export const useWaterfallChart = (
2930
// yAxisScale is the number of Y units per 30px
3031
// lowestYAxisValue is the yAxisValue for origin (0, 0)
3132

32-
yValueForZeroLine = chartHeight - (Math.abs(lowestYAxisValue) / yAxisScale) * DEFAULT_PIXELS_PER_Y_UNIT;
33+
yValueForZeroLine = chartHeight - (Math.abs(lowestYAxisValue) / yAxisScale) * yAxisPixelsPerUnit;
3334
let cumulativeSum = 0;
3435

3536
chartElements = transactions.map((transaction) => {
3637
const { label, value } = transaction;
3738
let yVal = 0;
38-
const barHeight = (value / yAxisScale) * DEFAULT_PIXELS_PER_Y_UNIT;
39-
const offsetHeight = (cumulativeSum / yAxisScale) * DEFAULT_PIXELS_PER_Y_UNIT;
39+
const barHeight = (value / yAxisScale) * yAxisPixelsPerUnit;
40+
const offsetHeight = (cumulativeSum / yAxisScale) * yAxisPixelsPerUnit;
4041
// minimum distance from zero line to the floating bar for the transaction
4142
if (value < 0) {
4243
yVal = yValueForZeroLine - offsetHeight;
4344
} else yVal = yValueForZeroLine - (offsetHeight + barHeight);
4445

4546
cumulativeSum += value;
4647

47-
return { name: label, value, yVal, cumulativeSum };
48+
return { name: label, value, yVal, cumulativeSum, barHeight: Math.abs(barHeight) };
4849
});
4950
}
5051

5152
const calculateBarWidth = (chartWidth: number): number => {
52-
let barWidth = DEFAULT_BAR_WIDTH;
53-
if (chartWidth && transactions?.length > 0) barWidth = chartWidth / (2 * transactions?.length + 1);
53+
let barWidth = 0;
54+
if (chartWidth && transactions?.length > 0) {
55+
if (showFinalSummary) barWidth = chartWidth / (2 * transactions?.length + 2);
56+
else barWidth = chartWidth / (2 * transactions?.length + 1);
57+
}
5458
return barWidth;
5559
};
5660

@@ -100,7 +104,7 @@ function getIntervalAndYPoints(
100104
): IGetIntervalAndYPointsReturnType {
101105
let yAxisScale = Math.pow(10, Math.ceil(Math.log10((maxVal - minVal) / maxLabelsCount)) - 1);
102106
let roundedMinVal = roundMinVal(minVal, yAxisScale);
103-
let roundedMaxVal = roundMaxVal(maxVal, yAxisScale);
107+
const roundedMaxVal = roundMaxVal(maxVal, yAxisScale);
104108
let isScaleSufficient = checkIfScaleSufficient(yAxisScale, maxLabelsCount, roundedMaxVal - roundedMinVal);
105109

106110
let isMultipleOfFiveChecked = false;
@@ -113,7 +117,6 @@ function getIntervalAndYPoints(
113117
isMultipleOfFiveChecked = true;
114118
}
115119
roundedMinVal = roundMinVal(minVal, yAxisScale);
116-
roundedMaxVal = roundMaxVal(maxVal, yAxisScale);
117120
isScaleSufficient = checkIfScaleSufficient(yAxisScale, maxLabelsCount, maxVal - roundedMinVal);
118121
}
119122

@@ -127,6 +130,7 @@ function getIntervalAndYPoints(
127130
}
128131

129132
function checkIfScaleSufficient(scale: number, maxLabelsCount: number, valueRange: number): boolean {
133+
if (maxLabelsCount === 0) return true; // to stop the while loop from checking for sufficient scale with zero maxLabelsCount
130134
if (scale * maxLabelsCount >= valueRange) return true;
131135
return false;
132136
}

0 commit comments

Comments
 (0)