Created PieChart component. Refactored charts.

This commit is contained in:
Mike Cao 2024-03-14 20:26:52 -07:00
parent f277580722
commit f6524392e2
15 changed files with 403 additions and 336 deletions

View file

@ -1,11 +0,0 @@
.chart {
position: relative;
height: 400px;
overflow: hidden;
}
.tooltip {
display: flex;
flex-direction: column;
gap: 10px;
}

View file

@ -1,192 +0,0 @@
import { useState, useRef, useEffect, useCallback } from 'react';
import { Loading } from 'react-basics';
import classNames from 'classnames';
import Chart from 'chart.js/auto';
import HoverTooltip from 'components/common/HoverTooltip';
import Legend from 'components/metrics/Legend';
import { useLocale } from 'components/hooks';
import { useTheme } from 'components/hooks';
import { DEFAULT_ANIMATION_DURATION } from 'lib/constants';
import { renderNumberLabels } from 'lib/charts';
import styles from './BarChart.module.css';
export interface BarChartProps {
datasets?: any[];
unit?: string;
animationDuration?: number;
stacked?: boolean;
isLoading?: boolean;
renderXLabel?: (label: string, index: number, values: any[]) => string;
renderYLabel?: (label: string, index: number, values: any[]) => string;
XAxisType?: string;
YAxisType?: string;
renderTooltipPopup?: (setTooltipPopup: (data: any) => void, model: any) => void;
updateMode?: string;
onCreate?: (chart: any) => void;
onUpdate?: (chart: any) => void;
className?: string;
}
export function BarChart({
datasets = [],
unit,
animationDuration = DEFAULT_ANIMATION_DURATION,
stacked = false,
isLoading = false,
renderXLabel,
renderYLabel,
XAxisType = 'time',
YAxisType = 'linear',
renderTooltipPopup,
updateMode,
onCreate,
onUpdate,
className,
}: BarChartProps) {
const canvas = useRef();
const chart = useRef(null);
const [tooltip, setTooltipPopup] = useState(null);
const { locale } = useLocale();
const { theme, colors } = useTheme();
const [legendItems, setLegendItems] = useState([]);
const getOptions = useCallback(() => {
return {
responsive: true,
maintainAspectRatio: false,
animation: {
duration: animationDuration,
resize: {
duration: 0,
},
active: {
duration: 0,
},
},
plugins: {
legend: {
display: false,
},
tooltip: {
enabled: false,
external: renderTooltipPopup ? renderTooltipPopup.bind(null, setTooltipPopup) : undefined,
},
},
scales: {
x: {
type: XAxisType,
stacked: true,
time: {
unit,
},
grid: {
display: false,
},
border: {
color: colors.chart.line,
},
ticks: {
color: colors.chart.text,
autoSkip: false,
maxRotation: 0,
callback: renderXLabel,
},
},
y: {
type: YAxisType,
min: 0,
beginAtZero: true,
stacked,
grid: {
color: colors.chart.line,
},
border: {
color: colors.chart.line,
},
ticks: {
color: colors.chart.text,
callback: renderYLabel || renderNumberLabels,
},
},
},
};
}, [
animationDuration,
renderTooltipPopup,
renderXLabel,
XAxisType,
YAxisType,
stacked,
colors,
unit,
locale,
]);
const createChart = (datasets: any[]) => {
Chart.defaults.font.family = 'Inter';
chart.current = new Chart(canvas.current, {
type: 'bar',
data: {
datasets,
},
options: getOptions() as any,
});
onCreate?.(chart.current);
setLegendItems(chart.current.legend.legendItems);
};
const updateChart = (datasets: any[]) => {
setTooltipPopup(null);
chart.current.data.datasets.forEach((dataset: { data: any }, index: string | number) => {
dataset.data = datasets[index]?.data;
});
chart.current.options = getOptions();
// Allow config changes before update
onUpdate?.(chart.current);
chart.current.update(updateMode);
setLegendItems(chart.current.legend.legendItems);
};
useEffect(() => {
if (datasets) {
if (!chart.current) {
createChart(datasets);
} else {
updateChart(datasets);
}
}
}, [datasets, unit, theme, animationDuration, locale]);
const handleLegendClick = (index: number) => {
const meta = chart.current.getDatasetMeta(index);
meta.hidden = meta.hidden === null ? !chart.current.data.datasets[index].hidden : null;
chart.current.update();
};
return (
<>
<div className={classNames(styles.chart, className)}>
{isLoading && <Loading position="page" icon="dots" />}
<canvas ref={canvas} />
</div>
<Legend items={legendItems} onClick={handleLegendClick} />
{tooltip && (
<HoverTooltip>
<div className={styles.tooltip}>{tooltip}</div>
</HoverTooltip>
)}
</>
);
}
export default BarChart;

View file

@ -1,7 +1,7 @@
import { useMemo } from 'react';
import { Loading } from 'react-basics';
import { colord } from 'colord';
import BarChart from './BarChart';
import BarChart from 'components/charts/BarChart';
import { getDateArray } from 'lib/date';
import {
useLocale,
@ -10,8 +10,8 @@ import {
useNavigation,
useWebsiteEvents,
} from 'components/hooks';
import { EVENT_COLORS } from 'lib/constants';
import { renderDateLabels, renderStatusTooltipPopup } from 'lib/charts';
import { CHART_COLORS } from 'lib/constants';
import { renderDateLabels } from 'lib/charts';
export interface EventsChartProps {
websiteId: string;
@ -26,7 +26,6 @@ export function EventsChart({ websiteId, className, token }: EventsChartProps) {
const {
query: { url, event },
} = useNavigation();
const { data, isLoading } = useWebsiteEvents(websiteId, {
startAt: +startDate,
endAt: +endDate,
@ -38,9 +37,8 @@ export function EventsChart({ websiteId, className, token }: EventsChartProps) {
offset,
});
const datasets = useMemo(() => {
const chartData = useMemo(() => {
if (!data) return [];
if (isLoading) return data;
const map = (data as any[]).reduce((obj, { x, t, y }) => {
if (!obj[x]) {
@ -56,18 +54,20 @@ export function EventsChart({ websiteId, className, token }: EventsChartProps) {
map[key] = getDateArray(map[key], startDate, endDate, unit);
});
return Object.keys(map).map((key, index) => {
const color = colord(EVENT_COLORS[index % EVENT_COLORS.length]);
return {
label: key,
data: map[key],
lineTension: 0,
backgroundColor: color.alpha(0.6).toRgbString(),
borderColor: color.alpha(0.7).toRgbString(),
borderWidth: 1,
};
});
}, [data, isLoading, startDate, endDate, unit]);
return {
datasets: Object.keys(map).map((key, index) => {
const color = colord(CHART_COLORS[index % CHART_COLORS.length]);
return {
label: key,
data: map[key],
lineTension: 0,
backgroundColor: color.alpha(0.6).toRgbString(),
borderColor: color.alpha(0.7).toRgbString(),
borderWidth: 1,
};
}),
};
}, [data, startDate, endDate, unit]);
if (isLoading) {
return <Loading icon="dots" />;
@ -76,11 +76,10 @@ export function EventsChart({ websiteId, className, token }: EventsChartProps) {
return (
<BarChart
className={className}
datasets={datasets as any[]}
data={chartData}
unit={unit}
stacked={true}
renderXLabel={renderDateLabels(unit, locale)}
renderTooltipPopup={renderStatusTooltipPopup(unit, locale)}
isLoading={isLoading}
/>
);

View file

@ -1,9 +1,8 @@
import { useEffect } from 'react';
import { StatusLight } from 'react-basics';
import { colord } from 'colord';
import classNames from 'classnames';
import { LegendItem } from 'chart.js/auto';
import { useLocale } from 'components/hooks';
import { useForceUpdate } from 'components/hooks';
import styles from './Legend.module.css';
export function Legend({
@ -11,14 +10,9 @@ export function Legend({
onClick,
}: {
items: any[];
onClick: (index: number) => void;
onClick: (index: LegendItem) => void;
}) {
const { locale } = useLocale();
const forceUpdate = useForceUpdate();
useEffect(() => {
forceUpdate();
}, [locale, forceUpdate]);
if (!items.find(({ text }) => text)) {
return null;
@ -26,14 +20,15 @@ export function Legend({
return (
<div className={styles.legend}>
{items.map(({ text, fillStyle, datasetIndex, hidden }) => {
{items.map(item => {
const { text, fillStyle, hidden } = item;
const color = colord(fillStyle);
return (
<div
key={text}
className={classNames(styles.label, { [styles.hidden]: hidden })}
onClick={() => onClick(datasetIndex)}
onClick={() => onClick(item)}
>
<StatusLight color={color.alpha(color.alpha() + 0.2).toHex()}>
<span className={locale}>{text}</span>

View file

@ -1,7 +1,7 @@
import { useMemo } from 'react';
import BarChart, { BarChartProps } from './BarChart';
import BarChart, { BarChartProps } from 'components/charts/BarChart';
import { useLocale, useTheme, useMessages } from 'components/hooks';
import { renderDateLabels, renderStatusTooltipPopup } from 'lib/charts';
import { renderDateLabels } from 'lib/charts';
export interface PageviewsChartProps extends BarChartProps {
data: {
@ -17,33 +17,36 @@ export function PageviewsChart({ data, unit, isLoading, ...props }: PageviewsCha
const { colors } = useTheme();
const { locale } = useLocale();
const datasets = useMemo(() => {
if (!data) return [];
const chartData = useMemo(() => {
if (!data) {
return {};
}
return [
{
label: formatMessage(labels.visitors),
data: data.sessions,
borderWidth: 1,
...colors.chart.visitors,
},
{
label: formatMessage(labels.views),
data: data.pageviews,
borderWidth: 1,
...colors.chart.views,
},
];
}, [data, colors, formatMessage, labels]);
return {
datasets: [
{
label: formatMessage(labels.visitors),
data: data.sessions,
borderWidth: 1,
...colors.chart.visitors,
},
{
label: formatMessage(labels.views),
data: data.pageviews,
borderWidth: 1,
...colors.chart.views,
},
],
};
}, [data]);
return (
<BarChart
{...props}
datasets={datasets}
data={chartData}
unit={unit}
isLoading={isLoading}
renderXLabel={renderDateLabels(unit, locale)}
renderTooltipPopup={renderStatusTooltipPopup(unit, locale)}
/>
);
}