BarChart component.

This commit is contained in:
Mike Cao 2020-08-26 09:58:24 -07:00
parent 5206622d5a
commit 5f47f328be
5 changed files with 373 additions and 166 deletions

View File

@ -0,0 +1,150 @@
import React, { useState, useRef, useEffect } from 'react';
import ReactTooltip from 'react-tooltip';
import classNames from 'classnames';
import ChartJS from 'chart.js';
import styles from './PageviewsChart.module.css';
import { format } from 'date-fns';
export default function BarChart({
chartId,
datasets,
unit,
records,
animationDuration = 300,
className,
onUpdate = () => {},
}) {
const canvas = useRef();
const chart = useRef();
const [tooltip, setTooltip] = useState({});
const renderLabel = (label, index, values) => {
const d = new Date(values[index].value);
const n = records;
switch (unit) {
case 'hour':
return format(d, 'ha');
case 'day':
if (n >= 15) {
return index % ~~(n / 15) === 0 ? format(d, 'MMM d') : '';
}
return format(d, 'EEE M/d');
case 'month':
return format(d, 'MMMM');
default:
return label;
}
};
const renderTooltip = model => {
const { opacity, title, body, labelColors } = model;
if (!opacity) {
setTooltip(null);
} else {
const [label, value] = body[0].lines[0].split(':');
setTooltip({
title: title[0],
value,
label,
labelColor: labelColors[0].backgroundColor,
});
}
};
function draw() {
if (!chart.current) {
chart.current = new ChartJS(canvas.current, {
type: 'bar',
data: {
datasets,
},
options: {
animation: {
duration: animationDuration,
},
tooltips: {
enabled: false,
custom: renderTooltip,
},
hover: {
animationDuration: 0,
},
responsiveAnimationDuration: 0,
scales: {
xAxes: [
{
type: 'time',
distribution: 'series',
time: {
unit,
tooltipFormat: 'ddd MMMM DD YYYY',
},
ticks: {
callback: renderLabel,
minRotation: 0,
maxRotation: 0,
},
gridLines: {
display: false,
},
offset: true,
stacked: true,
},
],
yAxes: [
{
ticks: {
beginAtZero: true,
},
},
],
},
},
});
} else {
const { options } = chart.current;
options.scales.xAxes[0].time.unit = unit;
options.scales.xAxes[0].ticks.callback = renderLabel;
onUpdate(chart.current);
}
}
useEffect(() => {
if (datasets) {
draw();
setTooltip(null);
}
}, [datasets]);
return (
<div
data-tip=""
data-for={`${chartId}-tooltip`}
className={classNames(styles.chart, className)}
>
<canvas ref={canvas} width={960} height={400} />
<ReactTooltip id={`${chartId}-tooltip`}>
{tooltip ? <Tooltip {...tooltip} /> : null}
</ReactTooltip>
</div>
);
}
const Tooltip = ({ title, value, label, labelColor }) => (
<div className={styles.tooltip}>
<div className={styles.content}>
<div className={styles.title}>{title}</div>
<div className={styles.metric}>
<div className={styles.dot}>
<div className={styles.color} style={{ backgroundColor: labelColor }} />
</div>
{value} {label}
</div>
</div>
</div>
);

View File

@ -0,0 +1,174 @@
import React, { useState, useRef, useEffect, useCallback } from 'react';
import ReactTooltip from 'react-tooltip';
import classNames from 'classnames';
import ChartJS from 'chart.js';
import { format } from 'date-fns';
import styles from './EventsChart.module.css';
export default function EventsChart({
websiteId,
data,
unit,
animationDuration = 300,
className,
children,
}) {
const canvas = useRef();
const chart = useRef();
const [tooltip, setTooltip] = useState({});
const renderLabel = useCallback(
(label, index, values) => {
const d = new Date(values[index].value);
const n = data.pageviews.length;
switch (unit) {
case 'day':
if (n >= 15) {
return index % ~~(n / 15) === 0 ? format(d, 'MMM d') : '';
}
return format(d, 'EEE M/d');
case 'month':
return format(d, 'MMMM');
default:
return label;
}
},
[unit, data],
);
const renderTooltip = model => {
const { opacity, title, body, labelColors } = model;
if (!opacity) {
setTooltip(null);
} else {
const [label, value] = body[0].lines[0].split(':');
setTooltip({
title: title[0],
value,
label,
labelColor: labelColors[0].backgroundColor,
});
}
};
function draw() {
if (!canvas.current) return;
if (!chart.current) {
chart.current = new ChartJS(canvas.current, {
type: 'bar',
data: {
datasets: [
{
label: 'unique visitors',
data: data.uniques,
lineTension: 0,
backgroundColor: 'rgb(38, 128, 235, 0.4)',
borderColor: 'rgb(13, 102, 208, 0.4)',
borderWidth: 1,
},
{
label: 'page views',
data: data.pageviews,
lineTension: 0,
backgroundColor: 'rgb(38, 128, 235, 0.2)',
borderColor: 'rgb(13, 102, 208, 0.2)',
borderWidth: 1,
},
],
},
options: {
animation: {
duration: animationDuration,
},
tooltips: {
enabled: false,
custom: renderTooltip,
},
hover: {
animationDuration: 0,
},
scales: {
xAxes: [
{
type: 'time',
distribution: 'series',
time: {
unit,
tooltipFormat: 'ddd MMMM DD YYYY',
},
ticks: {
callback: renderLabel,
maxRotation: 0,
},
gridLines: {
display: false,
},
offset: true,
stacked: true,
},
],
yAxes: [
{
ticks: {
beginAtZero: true,
},
},
],
},
},
});
} else {
const {
data: { datasets },
options,
} = chart.current;
datasets[0].data = data.uniques;
datasets[1].data = data.pageviews;
options.scales.xAxes[0].time.unit = unit;
options.scales.xAxes[0].ticks.callback = renderLabel;
options.animation.duration = animationDuration;
chart.current.update();
}
}
useEffect(() => {
if (data) {
draw();
setTooltip(null);
}
}, [data]);
return (
<div
data-tip=""
data-for={`${websiteId}-tooltip`}
className={classNames(styles.chart, className)}
>
<canvas ref={canvas} width={960} height={400} />
<ReactTooltip id={`${websiteId}-tooltip`}>
{tooltip ? <Tooltip {...tooltip} /> : null}
</ReactTooltip>
{children}
</div>
);
}
const Tooltip = ({ title, value, label, labelColor }) => (
<div className={styles.tooltip}>
<div className={styles.content}>
<div className={styles.title}>{title}</div>
<div className={styles.metric}>
<div className={styles.dot}>
<div className={styles.color} style={{ backgroundColor: labelColor }} />
</div>
{value} {label}
</div>
</div>
</div>
);

View File

@ -0,0 +1,3 @@
.chart {
display: flex;
}

View File

@ -1,67 +1,32 @@
import React, { useState, useRef, useEffect, useCallback } from 'react'; import React, { useState, useRef, useEffect, useCallback } from 'react';
import ReactTooltip from 'react-tooltip';
import classNames from 'classnames'; import classNames from 'classnames';
import ChartJS from 'chart.js'; import BarChart from './BarChart';
import { format } from 'date-fns'; import { format } from 'date-fns';
import styles from './PageviewsChart.module.css'; import styles from './PageviewsChart.module.css';
export default function PageviewsChart({ export default function PageviewsChart({ websiteId, data, unit, className, animationDuration }) {
websiteId, const handleUpdate = chart => {
data, const {
unit, data: { datasets },
animationDuration = 300, options,
className, } = chart;
children,
}) {
const canvas = useRef();
const chart = useRef();
const [tooltip, setTooltip] = useState({});
const renderLabel = useCallback( datasets[0].data = data.uniques;
(label, index, values) => { datasets[1].data = data.pageviews;
const d = new Date(values[index].value); options.animation.duration = animationDuration;
const n = data.pageviews.length;
switch (unit) { chart.update();
case 'day':
if (n >= 15) {
return index % ~~(n / 15) === 0 ? format(d, 'MMM d') : '';
}
return format(d, 'EEE M/d');
case 'month':
return format(d, 'MMMM');
default:
return label;
}
},
[unit, data],
);
const renderTooltip = model => {
const { opacity, title, body, labelColors } = model;
if (!opacity) {
setTooltip(null);
} else {
const [label, value] = body[0].lines[0].split(':');
setTooltip({
title: title[0],
value,
label,
labelColor: labelColors[0].backgroundColor,
});
}
}; };
function draw() { if (!data) {
if (!canvas.current) return; return null;
}
if (!chart.current) { return (
chart.current = new ChartJS(canvas.current, { <div className={classNames(styles.chart, className)}>
type: 'bar', <BarChart
data: { chartId={websiteId}
datasets: [ datasets={[
{ {
label: 'unique visitors', label: 'unique visitors',
data: data.uniques, data: data.uniques,
@ -78,97 +43,11 @@ export default function PageviewsChart({
borderColor: 'rgb(13, 102, 208, 0.2)', borderColor: 'rgb(13, 102, 208, 0.2)',
borderWidth: 1, borderWidth: 1,
}, },
], ]}
}, unit={unit}
options: { records={data.pageviews.length}
animation: { onUpdate={handleUpdate}
duration: animationDuration, />
},
tooltips: {
enabled: false,
custom: renderTooltip,
},
hover: {
animationDuration: 0,
},
scales: {
xAxes: [
{
type: 'time',
distribution: 'series',
time: {
unit,
tooltipFormat: 'ddd MMMM DD YYYY',
},
ticks: {
callback: renderLabel,
maxRotation: 0,
},
gridLines: {
display: false,
},
offset: true,
stacked: true,
},
],
yAxes: [
{
ticks: {
beginAtZero: true,
},
},
],
},
},
});
} else {
const {
data: { datasets },
options,
} = chart.current;
datasets[0].data = data.uniques;
datasets[1].data = data.pageviews;
options.scales.xAxes[0].time.unit = unit;
options.scales.xAxes[0].ticks.callback = renderLabel;
options.animation.duration = animationDuration;
chart.current.update();
}
}
useEffect(() => {
if (data) {
draw();
setTooltip(null);
}
}, [data]);
return (
<div
data-tip=""
data-for={`${websiteId}-tooltip`}
className={classNames(styles.chart, className)}
>
<canvas ref={canvas} width={960} height={400} />
<ReactTooltip id={`${websiteId}-tooltip`}>
{tooltip ? <Tooltip {...tooltip} /> : null}
</ReactTooltip>
{children}
</div> </div>
); );
} }
const Tooltip = ({ title, value, label, labelColor }) => (
<div className={styles.tooltip}>
<div className={styles.content}>
<div className={styles.title}>{title}</div>
<div className={styles.metric}>
<div className={styles.dot}>
<div className={styles.color} style={{ backgroundColor: labelColor }} />
</div>
{value} {label}
</div>
</div>
</div>
);

View File

@ -76,14 +76,15 @@ export default function WebsiteChart({
<div className="row"> <div className="row">
<CheckVisible className="col"> <CheckVisible className="col">
{visible => ( {visible => (
<>
<PageviewsChart <PageviewsChart
websiteId={websiteId} websiteId={websiteId}
data={{ pageviews, uniques }} data={{ pageviews, uniques }}
unit={unit} unit={unit}
animationDuration={visible ? 300 : 0} animationDuration={visible ? 300 : 0}
> />
<QuickButtons value={value} onChange={handleDateChange} /> <QuickButtons value={value} onChange={handleDateChange} />
</PageviewsChart> </>
)} )}
</CheckVisible> </CheckVisible>
</div> </div>