From 5f47f328bed02187c64ec0cf7d9ca7e99d6c55ea Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Wed, 26 Aug 2020 09:58:24 -0700 Subject: [PATCH] BarChart component. --- components/metrics/BarChart.js | 150 ++++++++++++++++ components/metrics/EventsChart.js | 174 +++++++++++++++++++ components/metrics/EventsChart.module.css | 3 + components/metrics/PageviewsChart.js | 197 +++++----------------- components/metrics/WebsiteChart.js | 15 +- 5 files changed, 373 insertions(+), 166 deletions(-) create mode 100644 components/metrics/BarChart.js create mode 100644 components/metrics/EventsChart.js create mode 100644 components/metrics/EventsChart.module.css diff --git a/components/metrics/BarChart.js b/components/metrics/BarChart.js new file mode 100644 index 00000000..96063fd6 --- /dev/null +++ b/components/metrics/BarChart.js @@ -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 ( +
+ + + {tooltip ? : null} + +
+ ); +} + +const Tooltip = ({ title, value, label, labelColor }) => ( +
+
+
{title}
+
+
+
+
+ {value} {label} +
+
+
+); diff --git a/components/metrics/EventsChart.js b/components/metrics/EventsChart.js new file mode 100644 index 00000000..56a4a1a9 --- /dev/null +++ b/components/metrics/EventsChart.js @@ -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 ( +
+ + + {tooltip ? : null} + + {children} +
+ ); +} + +const Tooltip = ({ title, value, label, labelColor }) => ( +
+
+
{title}
+
+
+
+
+ {value} {label} +
+
+
+); diff --git a/components/metrics/EventsChart.module.css b/components/metrics/EventsChart.module.css new file mode 100644 index 00000000..d586bead --- /dev/null +++ b/components/metrics/EventsChart.module.css @@ -0,0 +1,3 @@ +.chart { + display: flex; +} diff --git a/components/metrics/PageviewsChart.js b/components/metrics/PageviewsChart.js index fe8c44e4..f174d604 100644 --- a/components/metrics/PageviewsChart.js +++ b/components/metrics/PageviewsChart.js @@ -1,174 +1,53 @@ import React, { useState, useRef, useEffect, useCallback } from 'react'; -import ReactTooltip from 'react-tooltip'; import classNames from 'classnames'; -import ChartJS from 'chart.js'; +import BarChart from './BarChart'; import { format } from 'date-fns'; import styles from './PageviewsChart.module.css'; -export default function PageviewsChart({ - websiteId, - data, - unit, - animationDuration = 300, - className, - children, -}) { - const canvas = useRef(); - const chart = useRef(); - const [tooltip, setTooltip] = useState({}); +export default function PageviewsChart({ websiteId, data, unit, className, animationDuration }) { + const handleUpdate = chart => { + const { + data: { datasets }, + options, + } = chart; - const renderLabel = useCallback( - (label, index, values) => { - const d = new Date(values[index].value); - const n = data.pageviews.length; + datasets[0].data = data.uniques; + datasets[1].data = data.pageviews; + options.animation.duration = animationDuration; - 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, - }); - } + chart.update(); }; - 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(); - } + if (!data) { + return null; } - useEffect(() => { - if (data) { - draw(); - setTooltip(null); - } - }, [data]); - return ( -
- - - {tooltip ? : null} - - {children} +
+
); } - -const Tooltip = ({ title, value, label, labelColor }) => ( -
-
-
{title}
-
-
-
-
- {value} {label} -
-
-
-); diff --git a/components/metrics/WebsiteChart.js b/components/metrics/WebsiteChart.js index 4f066808..f93a985a 100644 --- a/components/metrics/WebsiteChart.js +++ b/components/metrics/WebsiteChart.js @@ -76,14 +76,15 @@ export default function WebsiteChart({
{visible => ( - + <> + - + )}