Skip to content

Commit

Permalink
feat: replace Recharts with Echarts (#80)
Browse files Browse the repository at this point in the history
  • Loading branch information
Gumichocopengin8 authored Feb 1, 2023
1 parent 12bb2dd commit 87a9478
Show file tree
Hide file tree
Showing 7 changed files with 211 additions and 363 deletions.
4 changes: 4 additions & 0 deletions web/next.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ module.exports = {
GOOGLE_ANALYTICS_TAG_ID: process.env.GOOGLE_ANALYTICS_TAG_ID ?? '',
},

transpilePackages: ['echarts', 'zrender'],

reactStrictMode: true,

webpack(config, options) {
config.plugins = config.plugins || [];

Expand Down
7 changes: 2 additions & 5 deletions web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,18 +17,15 @@
"@mui/material": "^5.11.6",
"@mui/styles": "^5.11.2",
"axios": "^1.2.6",
"lodash.zip": "^4.2.0",
"moment": "^2.29.4",
"dayjs": "^1.11.7",
"echarts": "^5.4.1",
"next": "^13.1.6",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-hook-form": "^7.43.0",
"react-is": "^18.2.0",
"recharts": "^2.3.2",
"recoil": "^0.7.6"
},
"devDependencies": {
"@types/lodash.zip": "^4.2.7",
"@types/node": "^18.11.18",
"@types/react": "^18.0.27",
"@typescript-eslint/eslint-plugin": "^5.49.0",
Expand Down
4 changes: 2 additions & 2 deletions web/src/components/[crate_names]/CrateTableRow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@
*/

import { useState } from 'react';
import dayjs from 'dayjs';
import { useRouter } from 'next/router';
import { css } from '@emotion/react';
import moment from 'moment';
import {
Table,
TableBody,
Expand Down Expand Up @@ -37,7 +37,7 @@ const CrateTableRow = ({ crateData }: Props): JSX.Element => {
const router = useRouter();
const { crate_names } = router.query;
const [open, setOpen] = useState<boolean>(false);
const dateFormat = (date: Date): string => moment(date).format('MMM DD, YYYY');
const dateFormat = (date: Date): string => dayjs(date).format('MMM DD, YYYY');
const onOpen = () => setOpen((state) => !state);

const onRemoveCrate = () => {
Expand Down
144 changes: 61 additions & 83 deletions web/src/components/[crate_names]/DownloadChart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,90 +5,86 @@
* LICENSE file in the root directory of this source tree.
*/

import React, { useMemo } from 'react';
import { useMemo } from 'react';
import { useRouter } from 'next/router';
import { css } from '@emotion/react';
import moment from 'moment';
import _zip from 'lodash.zip';
import dayjs from 'dayjs';
import { Typography } from '@mui/material';
import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer } from 'recharts';
import { Downloads } from 'interfaces/downloads';
import ReactECharts from 'components/echarts/ReactEChart';
import type { EChartsOption } from 'echarts';

interface Props {
downloadsData: Downloads[];
}

interface CustomUniformedData {
date: string;
downloads: number;
}

// date and downloads array length must be equal
interface ChartData {
date: string;
[x: string]: number | string;
dates: string[];
data: {
name: string;
downloads: number[];
}[];
}

const DownloadChart = ({ downloadsData }: Props): JSX.Element => {
const router = useRouter();
const { crate_names } = router.query;
const cratesNames = String(crate_names).split('+');

const uniformedData: ChartData[][] = useMemo(() => {
return downloadsData
.map((d) => {
const data: Downloads = { version_downloads: [...d.version_downloads], meta: { ...d.meta } };
const start = moment().subtract(89, 'days'); // for 90 days
const end = moment();
while (start.unix() < end.unix()) {
// fill missing date data
data.version_downloads.push({ date: start.format('YYYY-MM-DD'), downloads: 0, version: 0 });
start.add(1, 'days');
}
const uniformedData: ChartData = useMemo(() => {
const dates: string[] = [];
let start = dayjs().subtract(89, 'day'); // for 90 days
const end = dayjs();
while (start.unix() <= end.unix()) {
dates.push(start.format('YYYY-MM-DD'));
start = start.add(1, 'day');
}

// Map<crateName, Map<date, downlowd num>>
const map: Map<string, Map<string, number>> = new Map();
downloadsData.forEach((d, index) => {
const vMap: Map<string, number> = new Map();
for (const date of dates) {
vMap.set(date, 0);
}
for (const v of d.version_downloads) {
vMap.set(v.date, (vMap.has(v.date) ? vMap.get(v.date) : 0) + v.downloads);
}
map.set(cratesNames[index], vMap);
});

const dates = data.version_downloads.map((v) => v.date);
return Array.from(new Set(dates))
.map((date) =>
data.version_downloads
.map((download) => {
if (date === download.date) return download;
else return;
})
.filter((v) => v)
.reduce((uniformedDateData, currentValue) => {
uniformedDateData =
uniformedDateData.date === currentValue.date
? {
date: currentValue.date,
downloads: uniformedDateData.downloads + currentValue.downloads,
}
: { date: currentValue.date, downloads: currentValue.downloads };
return uniformedDateData;
}, {} as CustomUniformedData)
)
.sort((a: CustomUniformedData, b: CustomUniformedData) => {
if (a.date < b.date) return -1;
if (a.date > b.date) return 1;
return 0;
});
})
.map((data, i) => {
// modifiy data type for chart
return data.map((nd) => {
return { date: nd.date, [cratesNames[i]]: nd.downloads };
});
});
return {
dates: dates,
data: cratesNames.map((name) => {
const m = map.get(name);
return { name, downloads: map.has(name) ? Array.from(m.values()) : [] };
}),
};
}, [cratesNames, downloadsData]);

// zip to have all crates in an object
const chartData = _zip(...uniformedData).map((v) => Object.assign({}, ...v));

const generateRandomColor = () => {
const letters = '0123456789ABCDEF';
let color = '#';
for (let i = 0; i < 6; i++) {
color += letters[Math.floor(Math.random() * 16)];
}
return color;
const option: EChartsOption = {
dataZoom: [
{ realtime: true, show: true, type: 'slider' },
{ realtime: true, show: true, type: 'inside', zoomLock: true },
],
tooltip: { trigger: 'axis' },
legend: { data: cratesNames, type: 'scroll' },
toolbox: {
feature: {
dataZoom: { yAxisIndex: 'none' },
restore: {},
dataView: { readOnly: true },
magicType: { type: ['line', 'bar'] },
saveAsImage: {},
},
},
grid: { left: 80, right: 80 },
xAxis: { type: 'category', boundaryGap: false, data: uniformedData.dates },
yAxis: { type: 'value' },
series: uniformedData.data.map((d) => {
return { data: d.downloads, name: d.name, type: 'line' };
}),
};

return (
Expand All @@ -97,25 +93,7 @@ const DownloadChart = ({ downloadsData }: Props): JSX.Element => {
Recent Daily Downloads (90days)
</Typography>
<div css={CrateDownloadChart}>
<ResponsiveContainer>
<LineChart data={chartData} margin={{ top: 15, right: 30, left: 20, bottom: 5 }}>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="date" />
<YAxis />
<Tooltip />
<Legend />
{cratesNames.map((crateName) => (
<Line
key={crateName}
type="monotone"
dataKey={crateName}
name={crateName}
stroke={generateRandomColor()}
activeDot={{ r: 8 }}
/>
))}
</LineChart>
</ResponsiveContainer>
<ReactECharts option={option} />
</div>
</section>
);
Expand Down
81 changes: 81 additions & 0 deletions web/src/components/echarts/ReactEChart.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import type { EChartsOption, SetOptionOpts } from 'echarts';
import { BarChart, LineChart } from 'echarts/charts';
import {
DatasetComponent,
DataZoomComponent,
GridComponent,
LegendComponent,
TitleComponent,
ToolboxComponent,
TooltipComponent,
TransformComponent,
} from 'echarts/components';
import * as echarts from 'echarts/core';
import { LabelLayout, UniversalTransition } from 'echarts/features';
import { CanvasRenderer } from 'echarts/renderers';
import React, { useEffect, useRef, useState } from 'react';
import type { CSSProperties } from 'react';
import useResize from 'hooks/useResize';

echarts.use([
BarChart,
LineChart,
DataZoomComponent,
TitleComponent,
TooltipComponent,
ToolboxComponent,
LegendComponent,
GridComponent,
DatasetComponent,
TransformComponent,
LabelLayout,
UniversalTransition,
CanvasRenderer,
]);

interface Props {
onClick?: (param: echarts.ECElementEvent) => void;
option: EChartsOption;
settings?: SetOptionOpts;
style?: CSSProperties;
}

const ReactECharts: React.FC<Props> = ({ option, style, settings = {}, onClick }: Props) => {
const chartRef = useRef<HTMLDivElement>(null);
const [width, height] = useResize(chartRef);
const [echart, setEchart] = useState<echarts.ECharts | undefined>(undefined);

useEffect(() => {
if (chartRef.current && !echart) {
const chart = echarts.init(chartRef.current, null, { renderer: 'canvas', useDirtyRect: false });
chart.on('click', onClick ?? (() => undefined));
setEchart(chart);
}

return () => {
echart?.dispose();
};
}, [echart, onClick]);

useEffect(() => {
echart?.resize({ width, height });
}, [echart, height, width]);

useEffect(() => {
echart?.setOption(option, { ...settings, notMerge: false });
}, [echart, option, settings]);

return (
<div
ref={chartRef}
style={{
height: '100%',
position: 'relative',
width: '100%',
...style,
}}
/>
);
};

export default ReactECharts;
34 changes: 34 additions & 0 deletions web/src/hooks/useResize.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { RefObject, useCallback, useEffect, useState } from 'react';

const useResize = (ref: RefObject<HTMLElement>): [width: number, height: number] => {
const [height, setHeight] = useState<number>(0);
const [width, setWidth] = useState<number>(0);

const handleResize = useCallback((entries: ResizeObserverEntry[]) => {
if (!Array.isArray(entries)) {
return;
}

const entry = entries[0];
setWidth(entry.contentRect.width);
setHeight(entry.contentRect.height);
}, []);

useEffect(() => {
if (!ref.current) {
return;
}

let resizeObserver = new ResizeObserver((entries: ResizeObserverEntry[]) => handleResize(entries));
resizeObserver.observe(ref.current);

return () => {
resizeObserver.disconnect();
resizeObserver = null;
};
}, [handleResize, ref]);

return [width, height];
};

export default useResize;
Loading

0 comments on commit 87a9478

Please sign in to comment.