Skip to content

Commit 0a513e5

Browse files
committed
created svg graph component
1 parent afbd05b commit 0a513e5

File tree

9 files changed

+233
-2
lines changed

9 files changed

+233
-2
lines changed

src/constants.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export const DEFAULT_BAR_WIDTH = 20;

src/index.css

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
html,
2-
body {
2+
body, #root {
33
margin: 0;
44
padding: 0;
55
font-family: sans-serif;
66
height: 100%;
7+
width: 100%;
78
}
89

910
h1 {

src/stories/Component.stories.tsx

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import React from 'react';
2+
import { ComponentStory, ComponentMeta } from '@storybook/react';
3+
import Component from '../waterfall-chart';
4+
5+
export default {
6+
title: 'Example/WaterFallChart',
7+
component: Component,
8+
parameters: {
9+
// More on Story layout: https://storybook.js.org/docs/react/configure/story-layout
10+
layout: 'fullscreen'
11+
}
12+
} as ComponentMeta<typeof Component>;
13+
14+
const Template: ComponentStory<typeof Component> = (args) => (
15+
<Component {...args} />
16+
);
17+
18+
export const WaterFallChart = Template.bind({});
19+
WaterFallChart.args = {
20+
transactions: [{
21+
label: 'Income',
22+
value: 300
23+
},{
24+
label: 'Expense1',
25+
value: -60
26+
},{
27+
label: 'Gain1',
28+
value: 200
29+
},{
30+
label: 'Expense2',
31+
value: -100
32+
},{
33+
label: 'Gain2',
34+
value: 200
35+
}
36+
]
37+
};

src/types/types.d.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
export type IWaterfallGraphProps = {
2+
transactions: Array<ITransaction>;
3+
}
4+
5+
export type ITransaction = {
6+
label: string;
7+
value: number;
8+
color?: string;
9+
}
10+
11+
export type IChartElement = {
12+
name: string;
13+
value: number;
14+
yVal: number;
15+
cumulativeSum: number;
16+
}
17+
export type IUseWaterfallChartReturnType = {
18+
chartElements: Array<IChartElement>;
19+
yAxisUnitsPerPixel: number;
20+
yValueForZeroLine: number
21+
}
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
import React, { FC, useEffect, useRef, useState } from "react";
2+
import { IWaterfallGraphProps } from "../types/types";
3+
import { useWaterfallChart } from './utils';
4+
import styles from './styles.module.scss';
5+
import '../index.css';
6+
import { DEFAULT_BAR_WIDTH } from "../constants";
7+
8+
const WaterFallChart:FC<IWaterfallGraphProps> = (props) => {
9+
const { transactions } = props;
10+
11+
const wrapperRef = useRef<HTMLDivElement | null>(null);
12+
const [wrapperHeight, setWrapperHeight] = useState(0);
13+
14+
useEffect(() => {
15+
// Function to calculate wrapper height
16+
const calculateWrapperHeight = (): void => {
17+
if (wrapperRef.current) {
18+
const height = wrapperRef?.current?.offsetHeight; // Use offsetHeight to get the height
19+
setWrapperHeight(height); // Update state with calculated height
20+
}
21+
};
22+
23+
calculateWrapperHeight(); // Call the function initially
24+
25+
// Add event listener for window resize to recalculate wrapper height
26+
window.addEventListener('resize', calculateWrapperHeight);
27+
28+
return () => {
29+
// Cleanup: remove event listener on component unmount
30+
window.removeEventListener('resize', calculateWrapperHeight);
31+
};
32+
}, []);
33+
const { chartElements, yAxisUnitsPerPixel, yValueForZeroLine } = useWaterfallChart(transactions, wrapperHeight);
34+
35+
return (
36+
<div
37+
ref={wrapperRef}
38+
className={styles.chartWrapper}
39+
>
40+
<svg className={styles.svgContainer}>
41+
{/* y-axis */}
42+
<line x1="0" y1="0" x2="0" y2="100%" style={{ strokeDasharray: 1, stroke: 'rgb(158, 158, 158)', strokeWidth: 2}} />
43+
{/* x-axis */}
44+
<line x1="0" y1="100%" x2="100%" y2="100%" style={{ strokeDasharray: 1, stroke: 'rgb(158, 158, 158)', strokeWidth: 2}} />
45+
{yValueForZeroLine > 0 && (
46+
// zero line if negative values present
47+
<line
48+
x1="0"
49+
y1={yValueForZeroLine}
50+
x2="100%"
51+
y2={yValueForZeroLine}
52+
style={{ strokeDasharray: 1, stroke: 'rgb(158, 158, 158)', strokeWidth: 2}}
53+
/>
54+
)}
55+
{chartElements?.map((chartElement, index) => (
56+
<>
57+
<rect
58+
key={chartElement?.name}
59+
width={DEFAULT_BAR_WIDTH}
60+
height={Math.abs(chartElement?.value / yAxisUnitsPerPixel)}
61+
y={chartElement?.yVal}
62+
x={((2*index)+1) * DEFAULT_BAR_WIDTH}
63+
style={{
64+
fill: chartElement?.value > 0 ? '#00B050' : '#FF0000',
65+
strokeWidth: 2,
66+
stroke: chartElement?.value > 0 ? '#00B050' : '#FF0000',
67+
zIndex: 1,
68+
transition: 'all 0.3s ease-in-out'
69+
}} />
70+
<line
71+
key={chartElement?.name}
72+
x1={((2*index)+2) * DEFAULT_BAR_WIDTH}
73+
y1={wrapperHeight - (chartElement?.cumulativeSum / yAxisUnitsPerPixel) - (wrapperHeight - yValueForZeroLine)}
74+
x2={((2*index)+3) * DEFAULT_BAR_WIDTH}
75+
y2={wrapperHeight - (chartElement?.cumulativeSum / yAxisUnitsPerPixel) - (wrapperHeight - yValueForZeroLine)}
76+
style={{
77+
stroke: '#545453',
78+
strokeWidth: 2,
79+
zIndex: 1
80+
}} />
81+
</>
82+
))}
83+
</svg>
84+
</div>
85+
);
86+
};
87+
88+
export default WaterFallChart;

src/waterfall-chart/index.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import WaterFallChart from "./WaterFallChart";
2+
3+
export default WaterFallChart;
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
#root {
2+
width: 100%;
3+
height: 100%;
4+
}
5+
* {
6+
margin: 0;
7+
padding: 0;
8+
box-sizing: border-box;
9+
}
10+
.chartWrapper {
11+
height: calc(100% - 40px);
12+
width: calc(100% - 40px);
13+
margin: 20px;
14+
}
15+
16+
.svgContainer {
17+
height: 100%;
18+
width: 100%;
19+
display: block;
20+
}

src/waterfall-chart/utils.ts

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import { IChartElement, ITransaction, IUseWaterfallChartReturnType } from "../types/types";
2+
3+
export const useWaterfallChart = (transactions: Array<ITransaction>, chartHeight: number): IUseWaterfallChartReturnType => {
4+
const largestCumulativeVal = getLargestCumulativeSum(transactions); // this will be the highest y point in the graph
5+
const smallestCumulativeVal = getSmallestCumulativeSum(transactions);
6+
// const pixelPerYUnit = 20;
7+
let cumulativeSum = transactions[0]?.value || 0;
8+
let chartElements: Array<IChartElement> = [];
9+
let yAxisUnitsPerPixel = 0;
10+
let yValueForZeroLine = 0;
11+
if (chartHeight && chartHeight > 0) {
12+
yAxisUnitsPerPixel = (largestCumulativeVal - smallestCumulativeVal) / chartHeight;
13+
yValueForZeroLine = largestCumulativeVal / yAxisUnitsPerPixel;
14+
15+
chartElements = transactions.map((transaction, index) => {
16+
const {label, value} = transaction;
17+
let yVal=0;
18+
if (value < 0 || index === 0) yVal = chartHeight - ((cumulativeSum) / yAxisUnitsPerPixel) - (chartHeight - yValueForZeroLine);
19+
else yVal = chartHeight - ((cumulativeSum + value) / yAxisUnitsPerPixel) - (chartHeight - yValueForZeroLine);
20+
if (index > 0) cumulativeSum += value;
21+
return ({
22+
name: label,
23+
value,
24+
yVal,
25+
cumulativeSum
26+
});
27+
});
28+
}
29+
return({ chartElements, yAxisUnitsPerPixel, yValueForZeroLine});
30+
};
31+
32+
function getLargestCumulativeSum(arr: Array<ITransaction>): number {
33+
let maxSum = arr[0]?.value; // Initialize maxSum and currentSum with the first element of the array
34+
let currentSum = arr[0]?.value;
35+
36+
for (let i = 1; i < arr.length; i++) {
37+
currentSum += arr[i]?.value;
38+
39+
if (currentSum > maxSum) {
40+
maxSum = currentSum;
41+
}
42+
}
43+
44+
return maxSum;
45+
}
46+
47+
48+
function getSmallestCumulativeSum(arr: Array<ITransaction>): number {
49+
let minSum = arr[0]?.value; // Initialize minSum and currentSum with the first element of the array
50+
let currentSum = arr[0]?.value;
51+
52+
for (let i = 1; i < arr.length; i++) {
53+
currentSum += arr[i]?.value;
54+
55+
if (currentSum < minSum) {
56+
minSum = currentSum;
57+
}
58+
}
59+
if(minSum > 0) return 0; // if chart never goes below zero then smallest value will be zero
60+
return minSum;
61+
}

tsconfig.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@
99
"sourceMap": true,
1010
"noEmit": false,
1111
"declaration": true,
12-
"suppressImplicitAnyIndexErrors": true,
1312
"allowSyntheticDefaultImports": true,
1413
"lib": ["es2018", "dom"],
1514
"moduleResolution": "node",

0 commit comments

Comments
 (0)