Events chart.

This commit is contained in:
Mike Cao 2020-08-27 03:42:24 -07:00
parent 5f47f328be
commit 4618dc7f15
9 changed files with 220 additions and 226 deletions

View File

@ -17,6 +17,7 @@ import OSTable from './metrics/OSTable';
import DevicesTable from './metrics/DevicesTable';
import CountriesTable from './metrics/CountriesTable';
import EventsTable from './metrics/EventsTable';
import EventsChart from './metrics/EventsChart';
export default function WebsiteDetails({ websiteId, defaultDateRange = '7day' }) {
const [data, setData] = useState();
@ -25,7 +26,7 @@ export default function WebsiteDetails({ websiteId, defaultDateRange = '7day' })
const [dateRange, setDateRange] = useState(getDateRange(defaultDateRange));
const [expand, setExpand] = useState();
const [showEvents, setShowEvents] = useState(false);
const { startDate, endDate } = dateRange;
const { startDate, endDate, unit } = dateRange;
const BackButton = () => (
<Button
@ -55,10 +56,15 @@ export default function WebsiteDetails({ websiteId, defaultDateRange = '7day' })
{ label: 'Events', value: 'event', component: EventsTable },
];
const tableProps = {
const dataProps = {
websiteId,
startDate,
endDate,
unit,
};
const tableProps = {
...dataProps,
limit: 10,
onExpand: handleExpand,
websiteDomain: data?.domain,
@ -66,6 +72,10 @@ export default function WebsiteDetails({ websiteId, defaultDateRange = '7day' })
const DetailsComponent = expand?.component;
function getSelectedMenuOption(value) {
return menuOptions.find(e => e.value === value);
}
async function loadData() {
setData(await get(`/api/website/${websiteId}`));
}
@ -79,11 +89,11 @@ export default function WebsiteDetails({ websiteId, defaultDateRange = '7day' })
}
function handleExpand(value) {
setExpand(menuOptions.find(e => e.value === value));
setExpand(getSelectedMenuOption(value));
}
function handleMenuSelect(value) {
setExpand(menuOptions.find(e => e.value === value));
setExpand(getSelectedMenuOption(value));
}
useEffect(() => {
@ -142,7 +152,9 @@ export default function WebsiteDetails({ websiteId, defaultDateRange = '7day' })
<div className="col-12 col-md-12 col-lg-4">
<EventsTable {...tableProps} onDataLoad={data => setShowEvents(data.length > 0)} />
</div>
<div className="col-12 col-md-12 col-lg-8">events</div>
<div className="col-12 col-md-12 col-lg-8">
<EventsChart {...dataProps} />
</div>
</div>
</>
)}

View File

@ -12,6 +12,7 @@ export default function BarChart({
records,
animationDuration = 300,
className,
stacked = false,
onUpdate = () => {},
}) {
const canvas = useRef();
@ -54,8 +55,7 @@ export default function BarChart({
}
};
function draw() {
if (!chart.current) {
const createChart = () => {
chart.current = new ChartJS(canvas.current, {
type: 'bar',
data: {
@ -99,25 +99,31 @@ export default function BarChart({
ticks: {
beginAtZero: true,
},
stacked,
},
],
},
},
});
} else {
};
const updateChart = () => {
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();
if (!chart.current) {
createChart();
} else {
setTooltip(null);
updateChart();
}
}
}, [datasets]);

View File

@ -1,174 +1,84 @@
import React, { useState, useRef, useEffect, useCallback } from 'react';
import ReactTooltip from 'react-tooltip';
import React, { useState, useEffect } from 'react';
import classNames from 'classnames';
import ChartJS from 'chart.js';
import { format } from 'date-fns';
import styles from './EventsChart.module.css';
import BarChart from './BarChart';
import { get } from 'lib/web';
import { getTimezone, getDateArray } from 'lib/date';
import styles from './PageviewsChart.module.css';
export default function EventsChart({
websiteId,
data,
const COLORS = [
'rgba(38, 128, 235, 0.5)',
'rgba(227, 72, 80, 0.5)',
'rgba(45, 157, 120, 0.5)',
'rgba(103, 103, 236, 0.5)',
'rgba(68, 181, 86, 0.5)',
'rgba(146, 86, 217, 0.5)',
];
export default function EventsChart({ websiteId, startDate, endDate, unit, className }) {
const [data, setData] = useState();
async function loadData() {
const data = await get(`/api/website/${websiteId}/events`, {
start_at: +startDate,
end_at: +endDate,
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,
tz: getTimezone(),
});
console.log({ data });
const map = data.reduce((obj, { x, t, y }) => {
if (!obj[x]) {
obj[x] = [];
}
};
function draw() {
if (!canvas.current) return;
obj[x].push({ t, y });
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,
},
},
],
},
},
return obj;
}, {});
Object.keys(map).forEach(key => {
map[key] = getDateArray(map[key], startDate, endDate, unit);
});
} else {
setData(map);
}
function handleUpdate(chart) {
const {
data: { datasets },
options,
} = chart.current;
} = chart;
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();
}
chart.update();
}
useEffect(() => {
if (data) {
draw();
setTooltip(null);
loadData();
}, [websiteId, startDate, endDate]);
if (!data) {
return 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 className={classNames(styles.chart, className)}>
<BarChart
chartId={websiteId}
datasets={Object.keys(data).map((key, index) => ({
label: key,
data: data[key],
lineTension: 0,
backgroundColor: COLORS[index],
borderColor: COLORS[index],
borderWidth: 1,
}))}
unit={unit}
records={7}
onUpdate={handleUpdate}
stacked
/>
</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

@ -1,7 +1,6 @@
import React, { useState, useRef, useEffect, useCallback } from 'react';
import React from 'react';
import classNames from 'classnames';
import BarChart from './BarChart';
import { format } from 'date-fns';
import styles from './PageviewsChart.module.css';
export default function PageviewsChart({ websiteId, data, unit, className, animationDuration }) {

View File

@ -108,7 +108,7 @@ export function getDateArray(data, startDate, endDate, unit) {
const t = add(startDate, i);
const y = findData(t);
arr.push({ t, y });
arr.push({ ...data[i], t, y });
}
return arr;

View File

@ -401,7 +401,7 @@ export function getActiveVisitors(website_id) {
return Promise.resolve([]);
}
export function getEvents(website_id, start_at, end_at) {
export function getEventRankings(website_id, start_at, end_at) {
const db = getDatabase();
if (db === POSTGRESQL) {
@ -438,3 +438,48 @@ export function getEvents(website_id, start_at, end_at) {
return Promise.resolve([]);
}
export function getEvents(website_id, start_at, end_at, timezone = 'utc', unit = 'day') {
const db = getDatabase();
if (db === POSTGRESQL) {
return prisma.$queryRaw(
`
select
event_value x,
date_trunc('${unit}', created_at at time zone '${timezone}') t,
count(*) y
from event
where website_id=$1
and created_at between $2 and $3
group by 1, 2
order by 2
`,
website_id,
start_at,
end_at,
);
}
if (db === MYSQL) {
const tz = moment.tz(timezone).format('Z');
return prisma.$queryRaw(
`
select
event_value x,
date_trunc('${unit}', convert_tz(created_at,'+00:00','${tz}')) t,
count(*) y
from event
where website_id=?
and created_at between ? and ?
group by 1, 2
order by 2
`,
website_id,
start_at,
end_at,
);
}
return Promise.resolve([]);
}

View File

@ -0,0 +1,21 @@
import moment from 'moment-timezone';
import { getEvents } from 'lib/queries';
import { ok, badRequest } from 'lib/response';
const unitTypes = ['month', 'hour', 'day'];
export default async (req, res) => {
const { id, start_at, end_at, unit, tz } = req.query;
if (!moment.tz.zone(tz) || !unitTypes.includes(unit)) {
return badRequest(res);
}
const websiteId = +id;
const startDate = new Date(+start_at);
const endDate = new Date(+end_at);
const events = await getEvents(websiteId, startDate, endDate, tz, unit);
return ok(res, events);
};

View File

@ -11,12 +11,13 @@ export default async (req, res) => {
return badRequest(res);
}
const start = new Date(+start_at);
const end = new Date(+end_at);
const websiteId = +id;
const startDate = new Date(+start_at);
const endDate = new Date(+end_at);
const [pageviews, uniques] = await Promise.all([
getPageviews(+id, start, end, tz, unit, '*'),
getPageviews(+id, start, end, tz, unit, 'distinct session_id'),
getPageviews(websiteId, startDate, endDate, tz, unit, '*'),
getPageviews(websiteId, startDate, endDate, tz, unit, 'distinct session_id'),
]);
return ok(res, { pageviews, uniques });

View File

@ -1,4 +1,4 @@
import { getRankings, getEvents } from 'lib/queries';
import { getRankings, getEventRankings } from 'lib/queries';
import { ok, badRequest } from 'lib/response';
const sessionColumns = ['browser', 'os', 'device', 'country'];
@ -15,7 +15,7 @@ export default async (req, res) => {
}
if (type === 'event') {
const events = await getEvents(websiteId, startDate, endDate);
const events = await getEventRankings(websiteId, startDate, endDate);
return ok(res, events);
}