From 1591dc246cbf77e214bfc42eb560a8e24a071d41 Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Mon, 24 Aug 2020 10:40:01 -0700 Subject: [PATCH 01/12] Don't filter subdomains. --- lib/filters.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/filters.js b/lib/filters.js index 6c5a0294..ad72024f 100644 --- a/lib/filters.js +++ b/lib/filters.js @@ -54,7 +54,7 @@ export const refFilter = (data, { domain, domainOnly, raw }) => { }; if (raw) { - const regex = new RegExp(`http[s]?://([^.]+.)?${domain}`); + const regex = new RegExp(`http[s]?://${domain}`); return data.filter(({ x }) => isValidRef(x) && !regex.test(x)); } From a19248d71350cb052139894ab2fb9e9e290d304a Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Mon, 24 Aug 2020 10:52:47 -0700 Subject: [PATCH 02/12] Fixed bounce rate calculation. --- components/metrics/MetricsBar.js | 2 +- lib/queries.js | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/components/metrics/MetricsBar.js b/components/metrics/MetricsBar.js index 18a1a43b..00238fbe 100644 --- a/components/metrics/MetricsBar.js +++ b/components/metrics/MetricsBar.js @@ -35,7 +35,7 @@ export default function MetricsBar({ websiteId, startDate, endDate, className }) Number(n).toFixed(0) + '%'} /> Date: Mon, 24 Aug 2020 23:49:14 -0700 Subject: [PATCH 03/12] Events table. --- components/WebsiteDetails.js | 9 +++++ components/WebsiteDetails.module.css | 4 +++ components/metrics/EventsTable.js | 32 +++++++++++++++++ components/metrics/EventsTable.module.css | 7 ++++ components/metrics/MetricsTable.js | 43 +++++++++++------------ components/metrics/PagesTable.js | 1 + components/metrics/ReferrersTable.js | 8 ++--- lib/db.js | 1 + lib/filters.js | 2 +- lib/queries.js | 38 ++++++++++++++++++++ pages/api/website/[id]/rankings.js | 15 ++++++-- sql/schema.mysql.sql | 1 + sql/schema.postgresql.sql | 1 + 13 files changed, 131 insertions(+), 31 deletions(-) create mode 100644 components/metrics/EventsTable.js create mode 100644 components/metrics/EventsTable.module.css diff --git a/components/WebsiteDetails.js b/components/WebsiteDetails.js index 4f901efc..5731fe75 100644 --- a/components/WebsiteDetails.js +++ b/components/WebsiteDetails.js @@ -16,6 +16,7 @@ import BrowsersTable from './metrics/BrowsersTable'; import OSTable from './metrics/OSTable'; import DevicesTable from './metrics/DevicesTable'; import CountriesTable from './metrics/CountriesTable'; +import EventsTable from './metrics/EventsTable'; export default function WebsiteDetails({ websiteId, defaultDateRange = '7day' }) { const [data, setData] = useState(); @@ -23,6 +24,7 @@ export default function WebsiteDetails({ websiteId, defaultDateRange = '7day' }) const [countryData, setCountryData] = useState(); const [dateRange, setDateRange] = useState(getDateRange(defaultDateRange)); const [expand, setExpand] = useState(); + const [showEvents, setShowEvents] = useState(false); const { startDate, endDate } = dateRange; const BackButton = () => ( @@ -50,6 +52,7 @@ export default function WebsiteDetails({ websiteId, defaultDateRange = '7day' }) value: 'country', component: props => setCountryData(data)} />, }, + { label: 'Events', value: 'event', component: EventsTable }, ]; const tableProps = { @@ -135,6 +138,12 @@ export default function WebsiteDetails({ websiteId, defaultDateRange = '7day' }) setCountryData(data)} /> +
+
+ setShowEvents(data.length > 0)} /> +
+
events
+
)} {expand && ( diff --git a/components/WebsiteDetails.module.css b/components/WebsiteDetails.module.css index ce26f8b1..9e7352a9 100644 --- a/components/WebsiteDetails.module.css +++ b/components/WebsiteDetails.module.css @@ -42,6 +42,10 @@ padding-right: 0; } +.hidden { + display: none; +} + @media only screen and (max-width: 992px) { .row { border: 0; diff --git a/components/metrics/EventsTable.js b/components/metrics/EventsTable.js new file mode 100644 index 00000000..9110bfb8 --- /dev/null +++ b/components/metrics/EventsTable.js @@ -0,0 +1,32 @@ +import React from 'react'; +import MetricsTable from './MetricsTable'; +import styles from './EventsTable.module.css'; + +export default function DevicesTable({ + websiteId, + startDate, + endDate, + limit, + onExpand, + onDataLoad, +}) { + return ( + ( + <> + {w} + {x} + + )} + onExpand={onExpand} + onDataLoad={onDataLoad} + /> + ); +} diff --git a/components/metrics/EventsTable.module.css b/components/metrics/EventsTable.module.css new file mode 100644 index 00000000..e6cb3961 --- /dev/null +++ b/components/metrics/EventsTable.module.css @@ -0,0 +1,7 @@ +.type { + font-size: var(--font-size-small); + padding: 2px 4px; + border: 1px solid var(--gray300); + border-radius: 4px; + margin-right: 10px; +} diff --git a/components/metrics/MetricsTable.js b/components/metrics/MetricsTable.js index 207366fc..bd66cb85 100644 --- a/components/metrics/MetricsTable.js +++ b/components/metrics/MetricsTable.js @@ -21,9 +21,9 @@ export default function MetricsTable({ filterOptions, limit, headerComponent, + renderLabel, onDataLoad = () => {}, onExpand = () => {}, - labelRenderer = e => e, }) { const [data, setData] = useState(); const [format, setFormat] = useState(true); @@ -43,37 +43,34 @@ export default function MetricsTable({ async function loadData() { const data = await get(`/api/website/${websiteId}/rankings`, { + type, start_at: +startDate, end_at: +endDate, - type, }); setData(data); onDataLoad(data); } - function handleSetFormat() { - setFormat(state => !state); - } + const handleSetFormat = () => setFormat(state => !state); - function getRow(x, y, z) { + const getRow = row => { + const { x: label, y: value, z: percent } = row; return ( ); - } + }; const Row = ({ index, style }) => { - const { x, y, z } = rankings[index]; - return
{getRow(x, y, z)}
; + return
{getRow(rankings[index])}
; }; useEffect(() => { @@ -96,13 +93,13 @@ export default function MetricsTable({
- {limit ? ( - rankings.map(({ x, y, z }) => getRow(x, y, z)) - ) : ( - - {Row} - - )} + {limit + ? rankings.map(row => getRow(row)) + : data?.length > 0 && ( + + {Row} + + )}
{limit && data.length > limit && ( @@ -115,7 +112,7 @@ export default function MetricsTable({ ); } -const AnimatedRow = ({ label, value = 0, percent, animate, format, onClick, labelRenderer }) => { +const AnimatedRow = ({ label, value = 0, percent, animate, format, onClick }) => { const props = useSpring({ width: percent, y: value, @@ -125,7 +122,7 @@ const AnimatedRow = ({ label, value = 0, percent, animate, format, onClick, labe return (
-
{labelRenderer(decodeURI(label))}
+
{label}
{props.y?.interpolate(format)}
diff --git a/components/metrics/PagesTable.js b/components/metrics/PagesTable.js index 48ba85c3..aab8b23c 100644 --- a/components/metrics/PagesTable.js +++ b/components/metrics/PagesTable.js @@ -25,6 +25,7 @@ export default function PagesTable({ limit={limit} dataFilter={urlFilter} filterOptions={{ domain: websiteDomain, raw: filter === 'Raw' }} + renderLabel={({ x }) => decodeURI(x)} onExpand={onExpand} /> ); diff --git a/components/metrics/ReferrersTable.js b/components/metrics/ReferrersTable.js index 480a299e..abaa1208 100644 --- a/components/metrics/ReferrersTable.js +++ b/components/metrics/ReferrersTable.js @@ -13,13 +13,13 @@ export default function Referrers({ }) { const [filter, setFilter] = useState('Combined'); - const renderLink = url => { + const renderLink = ({ x: url }) => { return url.startsWith('http') ? ( - {url} + {decodeURI(url)} ) : ( - url + decodeURI(url) ); }; @@ -40,7 +40,7 @@ export default function Referrers({ raw: filter === 'Raw', }} onExpand={onExpand} - labelRenderer={renderLink} + renderLabel={renderLink} /> ); } diff --git a/lib/db.js b/lib/db.js index e6d69fdb..d32b9f27 100644 --- a/lib/db.js +++ b/lib/db.js @@ -35,5 +35,6 @@ export default prisma; export async function runQuery(query) { return query.catch(e => { console.error(e); + throw e; }); } diff --git a/lib/filters.js b/lib/filters.js index ad72024f..a1bf1b96 100644 --- a/lib/filters.js +++ b/lib/filters.js @@ -124,5 +124,5 @@ export const countryFilter = data => export const percentFilter = data => { const total = data.reduce((n, { y }) => n + y, 0); - return data.map(({ x, y }) => ({ x, y, z: total ? (y / total) * 100 : 0 })); + return data.map(({ x, y, ...props }) => ({ x, y, z: total ? (y / total) * 100 : 0, ...props })); }; diff --git a/lib/queries.js b/lib/queries.js index b9678240..5b2fccad 100644 --- a/lib/queries.js +++ b/lib/queries.js @@ -400,3 +400,41 @@ export function getActiveVisitors(website_id) { return Promise.resolve([]); } + +export function getEvents(website_id, start_at, end_at) { + const db = getDatabase(); + + if (db === POSTGRESQL) { + return prisma.$queryRaw( + ` + select distinct event_type w, event_value x, count(*) y + from event + where website_id=$1 + and created_at between $2 and $3 + group by 1, 2 + order by 3 desc + `, + website_id, + start_at, + end_at, + ); + } + + if (db === MYSQL) { + return prisma.$queryRaw( + ` + select distinct event_type w, event_value x, count(*) y + from event + where website_id=? + and created_at between ? and ? + group by 1, 2 + order by 3 desc + `, + website_id, + start_at, + end_at, + ); + } + + return Promise.resolve([]); +} diff --git a/pages/api/website/[id]/rankings.js b/pages/api/website/[id]/rankings.js index a930bca9..d86f3e27 100644 --- a/pages/api/website/[id]/rankings.js +++ b/pages/api/website/[id]/rankings.js @@ -1,4 +1,4 @@ -import { getRankings } from 'lib/queries'; +import { getRankings, getEvents } from 'lib/queries'; import { ok, badRequest } from 'lib/response'; const sessionColumns = ['browser', 'os', 'device', 'country']; @@ -6,14 +6,23 @@ const pageviewColumns = ['url', 'referrer']; export default async (req, res) => { const { id, type, start_at, end_at } = req.query; + const websiteId = +id; + const startDate = new Date(+start_at); + const endDate = new Date(+end_at); - if (!sessionColumns.includes(type) && !pageviewColumns.includes(type)) { + if (type !== 'event' && !sessionColumns.includes(type) && !pageviewColumns.includes(type)) { return badRequest(res); } + if (type === 'event') { + const events = await getEvents(websiteId, startDate, endDate); + + return ok(res, events); + } + const table = sessionColumns.includes(type) ? 'session' : 'pageview'; - const rankings = await getRankings(+id, new Date(+start_at), new Date(+end_at), type, table); + const rankings = await getRankings(websiteId, startDate, endDate, type, table); return ok(res, rankings); }; diff --git a/sql/schema.mysql.sql b/sql/schema.mysql.sql index 61a16509..106a6748 100644 --- a/sql/schema.mysql.sql +++ b/sql/schema.mysql.sql @@ -71,6 +71,7 @@ create index session_website_id_idx on session(website_id); create index pageview_created_at_idx on pageview(created_at); create index pageview_website_id_idx on pageview(website_id); create index pageview_session_id_idx on pageview(session_id); +create index pageview_website_id_created_at_idx on pageview(website_id, created_at); create index event_created_at_idx on event(created_at); create index event_website_id_idx on event(website_id); diff --git a/sql/schema.postgresql.sql b/sql/schema.postgresql.sql index 5206257d..43533349 100644 --- a/sql/schema.postgresql.sql +++ b/sql/schema.postgresql.sql @@ -64,6 +64,7 @@ create index session_website_id_idx on session(website_id); create index pageview_created_at_idx on pageview(created_at); create index pageview_website_id_idx on pageview(website_id); create index pageview_session_id_idx on pageview(session_id); +create index pageview_website_id_created_at_idx on pageview(website_id, created_at); create index event_created_at_idx on event(created_at); create index event_website_id_idx on event(website_id); From 13ec32b1c932217aea66e55945e56efd3b3f5e67 Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Mon, 24 Aug 2020 10:40:01 -0700 Subject: [PATCH 04/12] Don't filter subdomains. --- lib/filters.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/filters.js b/lib/filters.js index 6c5a0294..ad72024f 100644 --- a/lib/filters.js +++ b/lib/filters.js @@ -54,7 +54,7 @@ export const refFilter = (data, { domain, domainOnly, raw }) => { }; if (raw) { - const regex = new RegExp(`http[s]?://([^.]+.)?${domain}`); + const regex = new RegExp(`http[s]?://${domain}`); return data.filter(({ x }) => isValidRef(x) && !regex.test(x)); } From 2addb96e4b0c1b3b240cd2a09eb44f8acb77d8eb Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Mon, 24 Aug 2020 10:52:47 -0700 Subject: [PATCH 05/12] Fixed bounce rate calculation. --- components/metrics/MetricsBar.js | 2 +- lib/queries.js | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/components/metrics/MetricsBar.js b/components/metrics/MetricsBar.js index 18a1a43b..00238fbe 100644 --- a/components/metrics/MetricsBar.js +++ b/components/metrics/MetricsBar.js @@ -35,7 +35,7 @@ export default function MetricsBar({ websiteId, startDate, endDate, className }) Number(n).toFixed(0) + '%'} /> Date: Mon, 24 Aug 2020 23:49:14 -0700 Subject: [PATCH 06/12] Events table. --- components/WebsiteDetails.js | 9 +++++ components/WebsiteDetails.module.css | 4 +++ components/metrics/EventsTable.js | 32 +++++++++++++++++ components/metrics/EventsTable.module.css | 7 ++++ components/metrics/MetricsTable.js | 43 +++++++++++------------ components/metrics/PagesTable.js | 1 + components/metrics/ReferrersTable.js | 8 ++--- lib/db.js | 1 + lib/filters.js | 2 +- lib/queries.js | 38 ++++++++++++++++++++ pages/api/website/[id]/rankings.js | 15 ++++++-- sql/schema.mysql.sql | 1 + sql/schema.postgresql.sql | 1 + 13 files changed, 131 insertions(+), 31 deletions(-) create mode 100644 components/metrics/EventsTable.js create mode 100644 components/metrics/EventsTable.module.css diff --git a/components/WebsiteDetails.js b/components/WebsiteDetails.js index 4f901efc..5731fe75 100644 --- a/components/WebsiteDetails.js +++ b/components/WebsiteDetails.js @@ -16,6 +16,7 @@ import BrowsersTable from './metrics/BrowsersTable'; import OSTable from './metrics/OSTable'; import DevicesTable from './metrics/DevicesTable'; import CountriesTable from './metrics/CountriesTable'; +import EventsTable from './metrics/EventsTable'; export default function WebsiteDetails({ websiteId, defaultDateRange = '7day' }) { const [data, setData] = useState(); @@ -23,6 +24,7 @@ export default function WebsiteDetails({ websiteId, defaultDateRange = '7day' }) const [countryData, setCountryData] = useState(); const [dateRange, setDateRange] = useState(getDateRange(defaultDateRange)); const [expand, setExpand] = useState(); + const [showEvents, setShowEvents] = useState(false); const { startDate, endDate } = dateRange; const BackButton = () => ( @@ -50,6 +52,7 @@ export default function WebsiteDetails({ websiteId, defaultDateRange = '7day' }) value: 'country', component: props => setCountryData(data)} />, }, + { label: 'Events', value: 'event', component: EventsTable }, ]; const tableProps = { @@ -135,6 +138,12 @@ export default function WebsiteDetails({ websiteId, defaultDateRange = '7day' }) setCountryData(data)} />
+
+
+ setShowEvents(data.length > 0)} /> +
+
events
+
)} {expand && ( diff --git a/components/WebsiteDetails.module.css b/components/WebsiteDetails.module.css index ce26f8b1..9e7352a9 100644 --- a/components/WebsiteDetails.module.css +++ b/components/WebsiteDetails.module.css @@ -42,6 +42,10 @@ padding-right: 0; } +.hidden { + display: none; +} + @media only screen and (max-width: 992px) { .row { border: 0; diff --git a/components/metrics/EventsTable.js b/components/metrics/EventsTable.js new file mode 100644 index 00000000..9110bfb8 --- /dev/null +++ b/components/metrics/EventsTable.js @@ -0,0 +1,32 @@ +import React from 'react'; +import MetricsTable from './MetricsTable'; +import styles from './EventsTable.module.css'; + +export default function DevicesTable({ + websiteId, + startDate, + endDate, + limit, + onExpand, + onDataLoad, +}) { + return ( + ( + <> + {w} + {x} + + )} + onExpand={onExpand} + onDataLoad={onDataLoad} + /> + ); +} diff --git a/components/metrics/EventsTable.module.css b/components/metrics/EventsTable.module.css new file mode 100644 index 00000000..e6cb3961 --- /dev/null +++ b/components/metrics/EventsTable.module.css @@ -0,0 +1,7 @@ +.type { + font-size: var(--font-size-small); + padding: 2px 4px; + border: 1px solid var(--gray300); + border-radius: 4px; + margin-right: 10px; +} diff --git a/components/metrics/MetricsTable.js b/components/metrics/MetricsTable.js index 207366fc..bd66cb85 100644 --- a/components/metrics/MetricsTable.js +++ b/components/metrics/MetricsTable.js @@ -21,9 +21,9 @@ export default function MetricsTable({ filterOptions, limit, headerComponent, + renderLabel, onDataLoad = () => {}, onExpand = () => {}, - labelRenderer = e => e, }) { const [data, setData] = useState(); const [format, setFormat] = useState(true); @@ -43,37 +43,34 @@ export default function MetricsTable({ async function loadData() { const data = await get(`/api/website/${websiteId}/rankings`, { + type, start_at: +startDate, end_at: +endDate, - type, }); setData(data); onDataLoad(data); } - function handleSetFormat() { - setFormat(state => !state); - } + const handleSetFormat = () => setFormat(state => !state); - function getRow(x, y, z) { + const getRow = row => { + const { x: label, y: value, z: percent } = row; return ( ); - } + }; const Row = ({ index, style }) => { - const { x, y, z } = rankings[index]; - return
{getRow(x, y, z)}
; + return
{getRow(rankings[index])}
; }; useEffect(() => { @@ -96,13 +93,13 @@ export default function MetricsTable({
- {limit ? ( - rankings.map(({ x, y, z }) => getRow(x, y, z)) - ) : ( - - {Row} - - )} + {limit + ? rankings.map(row => getRow(row)) + : data?.length > 0 && ( + + {Row} + + )}
{limit && data.length > limit && ( @@ -115,7 +112,7 @@ export default function MetricsTable({ ); } -const AnimatedRow = ({ label, value = 0, percent, animate, format, onClick, labelRenderer }) => { +const AnimatedRow = ({ label, value = 0, percent, animate, format, onClick }) => { const props = useSpring({ width: percent, y: value, @@ -125,7 +122,7 @@ const AnimatedRow = ({ label, value = 0, percent, animate, format, onClick, labe return (
-
{labelRenderer(decodeURI(label))}
+
{label}
{props.y?.interpolate(format)}
diff --git a/components/metrics/PagesTable.js b/components/metrics/PagesTable.js index 48ba85c3..aab8b23c 100644 --- a/components/metrics/PagesTable.js +++ b/components/metrics/PagesTable.js @@ -25,6 +25,7 @@ export default function PagesTable({ limit={limit} dataFilter={urlFilter} filterOptions={{ domain: websiteDomain, raw: filter === 'Raw' }} + renderLabel={({ x }) => decodeURI(x)} onExpand={onExpand} /> ); diff --git a/components/metrics/ReferrersTable.js b/components/metrics/ReferrersTable.js index 480a299e..abaa1208 100644 --- a/components/metrics/ReferrersTable.js +++ b/components/metrics/ReferrersTable.js @@ -13,13 +13,13 @@ export default function Referrers({ }) { const [filter, setFilter] = useState('Combined'); - const renderLink = url => { + const renderLink = ({ x: url }) => { return url.startsWith('http') ? ( - {url} + {decodeURI(url)} ) : ( - url + decodeURI(url) ); }; @@ -40,7 +40,7 @@ export default function Referrers({ raw: filter === 'Raw', }} onExpand={onExpand} - labelRenderer={renderLink} + renderLabel={renderLink} /> ); } diff --git a/lib/db.js b/lib/db.js index e6d69fdb..d32b9f27 100644 --- a/lib/db.js +++ b/lib/db.js @@ -35,5 +35,6 @@ export default prisma; export async function runQuery(query) { return query.catch(e => { console.error(e); + throw e; }); } diff --git a/lib/filters.js b/lib/filters.js index ad72024f..a1bf1b96 100644 --- a/lib/filters.js +++ b/lib/filters.js @@ -124,5 +124,5 @@ export const countryFilter = data => export const percentFilter = data => { const total = data.reduce((n, { y }) => n + y, 0); - return data.map(({ x, y }) => ({ x, y, z: total ? (y / total) * 100 : 0 })); + return data.map(({ x, y, ...props }) => ({ x, y, z: total ? (y / total) * 100 : 0, ...props })); }; diff --git a/lib/queries.js b/lib/queries.js index 258d910f..bc5ed7fd 100644 --- a/lib/queries.js +++ b/lib/queries.js @@ -400,3 +400,41 @@ export function getActiveVisitors(website_id) { return Promise.resolve([]); } + +export function getEvents(website_id, start_at, end_at) { + const db = getDatabase(); + + if (db === POSTGRESQL) { + return prisma.$queryRaw( + ` + select distinct event_type w, event_value x, count(*) y + from event + where website_id=$1 + and created_at between $2 and $3 + group by 1, 2 + order by 3 desc + `, + website_id, + start_at, + end_at, + ); + } + + if (db === MYSQL) { + return prisma.$queryRaw( + ` + select distinct event_type w, event_value x, count(*) y + from event + where website_id=? + and created_at between ? and ? + group by 1, 2 + order by 3 desc + `, + website_id, + start_at, + end_at, + ); + } + + return Promise.resolve([]); +} diff --git a/pages/api/website/[id]/rankings.js b/pages/api/website/[id]/rankings.js index a930bca9..d86f3e27 100644 --- a/pages/api/website/[id]/rankings.js +++ b/pages/api/website/[id]/rankings.js @@ -1,4 +1,4 @@ -import { getRankings } from 'lib/queries'; +import { getRankings, getEvents } from 'lib/queries'; import { ok, badRequest } from 'lib/response'; const sessionColumns = ['browser', 'os', 'device', 'country']; @@ -6,14 +6,23 @@ const pageviewColumns = ['url', 'referrer']; export default async (req, res) => { const { id, type, start_at, end_at } = req.query; + const websiteId = +id; + const startDate = new Date(+start_at); + const endDate = new Date(+end_at); - if (!sessionColumns.includes(type) && !pageviewColumns.includes(type)) { + if (type !== 'event' && !sessionColumns.includes(type) && !pageviewColumns.includes(type)) { return badRequest(res); } + if (type === 'event') { + const events = await getEvents(websiteId, startDate, endDate); + + return ok(res, events); + } + const table = sessionColumns.includes(type) ? 'session' : 'pageview'; - const rankings = await getRankings(+id, new Date(+start_at), new Date(+end_at), type, table); + const rankings = await getRankings(websiteId, startDate, endDate, type, table); return ok(res, rankings); }; diff --git a/sql/schema.mysql.sql b/sql/schema.mysql.sql index 61a16509..106a6748 100644 --- a/sql/schema.mysql.sql +++ b/sql/schema.mysql.sql @@ -71,6 +71,7 @@ create index session_website_id_idx on session(website_id); create index pageview_created_at_idx on pageview(created_at); create index pageview_website_id_idx on pageview(website_id); create index pageview_session_id_idx on pageview(session_id); +create index pageview_website_id_created_at_idx on pageview(website_id, created_at); create index event_created_at_idx on event(created_at); create index event_website_id_idx on event(website_id); diff --git a/sql/schema.postgresql.sql b/sql/schema.postgresql.sql index 5206257d..43533349 100644 --- a/sql/schema.postgresql.sql +++ b/sql/schema.postgresql.sql @@ -64,6 +64,7 @@ create index session_website_id_idx on session(website_id); create index pageview_created_at_idx on pageview(created_at); create index pageview_website_id_idx on pageview(website_id); create index pageview_session_id_idx on pageview(session_id); +create index pageview_website_id_created_at_idx on pageview(website_id, created_at); create index event_created_at_idx on event(created_at); create index event_website_id_idx on event(website_id); From 5f47f328bed02187c64ec0cf7d9ca7e99d6c55ea Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Wed, 26 Aug 2020 09:58:24 -0700 Subject: [PATCH 07/12] 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 => ( - + <> + - + )}
From 4618dc7f1529c3cbe8070270ad5b886226353dfc Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Thu, 27 Aug 2020 03:42:24 -0700 Subject: [PATCH 08/12] Events chart. --- components/WebsiteDetails.js | 22 ++- components/metrics/BarChart.js | 120 ++++++++------- components/metrics/EventsChart.js | 218 ++++++++------------------- components/metrics/PageviewsChart.js | 3 +- lib/date.js | 2 +- lib/queries.js | 47 +++++- pages/api/website/[id]/events.js | 21 +++ pages/api/website/[id]/pageviews.js | 9 +- pages/api/website/[id]/rankings.js | 4 +- 9 files changed, 220 insertions(+), 226 deletions(-) create mode 100644 pages/api/website/[id]/events.js diff --git a/components/WebsiteDetails.js b/components/WebsiteDetails.js index 5731fe75..6606bc2d 100644 --- a/components/WebsiteDetails.js +++ b/components/WebsiteDetails.js @@ -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 = () => (
)} diff --git a/components/metrics/BarChart.js b/components/metrics/BarChart.js index 96063fd6..c411bb14 100644 --- a/components/metrics/BarChart.js +++ b/components/metrics/BarChart.js @@ -12,6 +12,7 @@ export default function BarChart({ records, animationDuration = 300, className, + stacked = false, onUpdate = () => {}, }) { const canvas = useRef(); @@ -54,70 +55,75 @@ export default function BarChart({ } }; - function draw() { - if (!chart.current) { - chart.current = new ChartJS(canvas.current, { - type: 'bar', - data: { - datasets, + const createChart = () => { + chart.current = new ChartJS(canvas.current, { + type: 'bar', + data: { + datasets, + }, + options: { + animation: { + duration: animationDuration, }, - 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, - }, - }, - ], - }, + tooltips: { + enabled: false, + custom: renderTooltip, }, - }); - } else { - const { options } = chart.current; + 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, + }, + stacked, + }, + ], + }, + }, + }); + }; - options.scales.xAxes[0].time.unit = unit; - options.scales.xAxes[0].ticks.callback = renderLabel; + const updateChart = () => { + const { options } = chart.current; - onUpdate(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); + if (!chart.current) { + createChart(); + } else { + setTooltip(null); + updateChart(); + } } }, [datasets]); diff --git a/components/metrics/EventsChart.js b/components/metrics/EventsChart.js index 56a4a1a9..8ee622dc 100644 --- a/components/metrics/EventsChart.js +++ b/components/metrics/EventsChart.js @@ -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, - unit, - animationDuration = 300, - className, - children, -}) { - const canvas = useRef(); - const chart = useRef(); - const [tooltip, setTooltip] = useState({}); +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)', +]; - const renderLabel = useCallback( - (label, index, values) => { - const d = new Date(values[index].value); - const n = data.pageviews.length; +export default function EventsChart({ websiteId, startDate, endDate, unit, className }) { + const [data, setData] = useState(); - 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; + async function loadData() { + const data = await get(`/api/website/${websiteId}/events`, { + start_at: +startDate, + end_at: +endDate, + unit, + tz: getTimezone(), + }); + console.log({ data }); + const map = data.reduce((obj, { x, t, y }) => { + if (!obj[x]) { + obj[x] = []; } - }, - [unit, data], - ); - const renderTooltip = model => { - const { opacity, title, body, labelColors } = model; + obj[x].push({ t, y }); - if (!opacity) { - setTooltip(null); - } else { - const [label, value] = body[0].lines[0].split(':'); + return obj; + }, {}); - setTooltip({ - title: title[0], - value, - label, - labelColor: labelColors[0].backgroundColor, - }); - } - }; + Object.keys(map).forEach(key => { + map[key] = getDateArray(map[key], startDate, endDate, unit); + }); - function draw() { - if (!canvas.current) return; + setData(map); + } - 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; + function handleUpdate(chart) { + const { + data: { datasets }, + options, + } = 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; + datasets[0].data = data.uniques; + datasets[1].data = data.pageviews; - chart.current.update(); - } + chart.update(); } useEffect(() => { - if (data) { - draw(); - setTooltip(null); - } - }, [data]); + loadData(); + }, [websiteId, startDate, endDate]); + + if (!data) { + return null; + } return ( -
- - - {tooltip ? : null} - - {children} +
+ ({ + label: key, + data: data[key], + lineTension: 0, + backgroundColor: COLORS[index], + borderColor: COLORS[index], + borderWidth: 1, + }))} + unit={unit} + records={7} + onUpdate={handleUpdate} + stacked + />
); } - -const Tooltip = ({ title, value, label, labelColor }) => ( -
-
-
{title}
-
-
-
-
- {value} {label} -
-
-
-); diff --git a/components/metrics/PageviewsChart.js b/components/metrics/PageviewsChart.js index f174d604..d810acf3 100644 --- a/components/metrics/PageviewsChart.js +++ b/components/metrics/PageviewsChart.js @@ -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 }) { diff --git a/lib/date.js b/lib/date.js index f53521b9..7cfcb8f3 100644 --- a/lib/date.js +++ b/lib/date.js @@ -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; diff --git a/lib/queries.js b/lib/queries.js index bc5ed7fd..592ae85c 100644 --- a/lib/queries.js +++ b/lib/queries.js @@ -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([]); +} diff --git a/pages/api/website/[id]/events.js b/pages/api/website/[id]/events.js new file mode 100644 index 00000000..d7ead27c --- /dev/null +++ b/pages/api/website/[id]/events.js @@ -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); +}; diff --git a/pages/api/website/[id]/pageviews.js b/pages/api/website/[id]/pageviews.js index 5e07c0ab..c90e1c6b 100644 --- a/pages/api/website/[id]/pageviews.js +++ b/pages/api/website/[id]/pageviews.js @@ -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 }); diff --git a/pages/api/website/[id]/rankings.js b/pages/api/website/[id]/rankings.js index d86f3e27..9c742390 100644 --- a/pages/api/website/[id]/rankings.js +++ b/pages/api/website/[id]/rankings.js @@ -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); } From d936ecc86eb743dd95bd8c048c535a7f8005a3c3 Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Thu, 27 Aug 2020 13:46:05 -0700 Subject: [PATCH 09/12] Refactor BarChart component. --- components/metrics/BarChart.js | 91 ++++++++++++++++--------------- components/metrics/EventsChart.js | 40 +++++++------- lib/date.js | 5 ++ 3 files changed, 73 insertions(+), 63 deletions(-) diff --git a/components/metrics/BarChart.js b/components/metrics/BarChart.js index c411bb14..e3853b0d 100644 --- a/components/metrics/BarChart.js +++ b/components/metrics/BarChart.js @@ -13,6 +13,7 @@ export default function BarChart({ animationDuration = 300, className, stacked = false, + onCreate = () => {}, onUpdate = () => {}, }) { const canvas = useRef(); @@ -56,54 +57,58 @@ export default function BarChart({ }; const createChart = () => { + const 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, + }, + stacked, + }, + ], + }, + }; + + onCreate(options); + 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, - }, - stacked, - }, - ], - }, - }, + options, }); }; diff --git a/components/metrics/EventsChart.js b/components/metrics/EventsChart.js index 8ee622dc..6400539a 100644 --- a/components/metrics/EventsChart.js +++ b/components/metrics/EventsChart.js @@ -1,21 +1,34 @@ -import React, { useState, useEffect } from 'react'; +import React, { useState, useEffect, useMemo } from 'react'; import classNames from 'classnames'; import BarChart from './BarChart'; import { get } from 'lib/web'; -import { getTimezone, getDateArray } from 'lib/date'; +import { getTimezone, getDateArray, getDateLength } from 'lib/date'; import styles from './PageviewsChart.module.css'; const COLORS = [ 'rgba(38, 128, 235, 0.5)', - 'rgba(227, 72, 80, 0.5)', + 'rgba(146, 86, 217, 0.5)', 'rgba(45, 157, 120, 0.5)', + 'rgba(216, 55, 144, 0.5)', + 'rgba(227, 72, 80, 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(); + const datasets = useMemo(() => { + if (!data) return []; + + return Object.keys(data).map((key, index) => ({ + label: key, + data: data[key], + lineTension: 0, + backgroundColor: COLORS[index], + borderColor: COLORS[index], + borderWidth: 1, + })); + }, [data]); async function loadData() { const data = await get(`/api/website/${websiteId}/events`, { @@ -43,13 +56,7 @@ export default function EventsChart({ websiteId, startDate, endDate, unit, class } function handleUpdate(chart) { - const { - data: { datasets }, - options, - } = chart; - - datasets[0].data = data.uniques; - datasets[1].data = data.pageviews; + chart.data.datasets = datasets; chart.update(); } @@ -66,16 +73,9 @@ export default function EventsChart({ websiteId, startDate, endDate, unit, class
({ - label: key, - data: data[key], - lineTension: 0, - backgroundColor: COLORS[index], - borderColor: COLORS[index], - borderWidth: 1, - }))} + datasets={datasets} unit={unit} - records={7} + records={getDateLength(startDate, endDate, unit)} onUpdate={handleUpdate} stacked /> diff --git a/lib/date.js b/lib/date.js index 7cfcb8f3..6e10f610 100644 --- a/lib/date.js +++ b/lib/date.js @@ -113,3 +113,8 @@ export function getDateArray(data, startDate, endDate, unit) { return arr; } + +export function getDateLength(startDate, endDate, unit) { + const [diff] = dateFuncs[unit]; + return diff(endDate, startDate) + 1; +} From a7e7469d22400862b010b1184725429b8cadc825 Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Thu, 27 Aug 2020 18:44:20 -0700 Subject: [PATCH 10/12] Bar chart styling. --- components/WebsiteDetails.js | 12 ++-- components/common/ButtonGroup.module.css | 8 ++- components/metrics/BarChart.js | 22 ++++--- ...wsChart.module.css => BarChart.module.css} | 0 components/metrics/EventsChart.js | 66 +++++++++++-------- components/metrics/EventsTable.js | 19 ++++-- components/metrics/PageviewsChart.js | 53 +++++++-------- lib/queries.js | 38 ----------- pages/api/website/[id]/rankings.js | 37 ++++++++--- 9 files changed, 130 insertions(+), 125 deletions(-) rename components/metrics/{PageviewsChart.module.css => BarChart.module.css} (100%) diff --git a/components/WebsiteDetails.js b/components/WebsiteDetails.js index 6606bc2d..4ebe5a0d 100644 --- a/components/WebsiteDetails.js +++ b/components/WebsiteDetails.js @@ -23,9 +23,9 @@ export default function WebsiteDetails({ websiteId, defaultDateRange = '7day' }) const [data, setData] = useState(); const [chartLoaded, setChartLoaded] = useState(false); const [countryData, setCountryData] = useState(); + const [eventsData, setEventsData] = useState(); const [dateRange, setDateRange] = useState(getDateRange(defaultDateRange)); const [expand, setExpand] = useState(); - const [showEvents, setShowEvents] = useState(false); const { startDate, endDate, unit } = dateRange; const BackButton = () => ( @@ -145,14 +145,16 @@ export default function WebsiteDetails({ websiteId, defaultDateRange = '7day' })
- setCountryData(data)} /> +
-
+
0 })} + >
- setShowEvents(data.length > 0)} /> +
-
+
diff --git a/components/common/ButtonGroup.module.css b/components/common/ButtonGroup.module.css index b93189e5..d18a8e9c 100644 --- a/components/common/ButtonGroup.module.css +++ b/components/common/ButtonGroup.module.css @@ -2,13 +2,13 @@ display: inline-flex; border-radius: 4px; overflow: hidden; - border: 1px solid var(--gray400); + border: 1px solid var(--gray500); } .group .button { border-radius: 0; background: var(--gray50); - border-left: 1px solid var(--gray400); + border-left: 1px solid var(--gray500); padding: 4px 8px; } @@ -16,6 +16,10 @@ border: 0; } +.group .button:hover { + background: var(--gray100); +} + .group .button + .button { margin: 0; } diff --git a/components/metrics/BarChart.js b/components/metrics/BarChart.js index e3853b0d..fdce6330 100644 --- a/components/metrics/BarChart.js +++ b/components/metrics/BarChart.js @@ -2,7 +2,7 @@ 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 styles from './BarChart.module.css'; import { format } from 'date-fns'; export default function BarChart({ @@ -10,6 +10,7 @@ export default function BarChart({ datasets, unit, records, + height = 400, animationDuration = 300, className, stacked = false, @@ -68,7 +69,9 @@ export default function BarChart({ hover: { animationDuration: 0, }, + responsive: true, responsiveAnimationDuration: 0, + maintainAspectRatio: false, scales: { xAxes: [ { @@ -133,16 +136,19 @@ export default function BarChart({ }, [datasets]); return ( -
- + <> +
+ +
{tooltip ? : null} -
+ ); } diff --git a/components/metrics/PageviewsChart.module.css b/components/metrics/BarChart.module.css similarity index 100% rename from components/metrics/PageviewsChart.module.css rename to components/metrics/BarChart.module.css diff --git a/components/metrics/EventsChart.js b/components/metrics/EventsChart.js index 6400539a..1825116f 100644 --- a/components/metrics/EventsChart.js +++ b/components/metrics/EventsChart.js @@ -1,33 +1,38 @@ import React, { useState, useEffect, useMemo } from 'react'; import classNames from 'classnames'; +import tinycolor from 'tinycolor2'; import BarChart from './BarChart'; import { get } from 'lib/web'; import { getTimezone, getDateArray, getDateLength } from 'lib/date'; -import styles from './PageviewsChart.module.css'; +import styles from './BarChart.module.css'; const COLORS = [ - 'rgba(38, 128, 235, 0.5)', - 'rgba(146, 86, 217, 0.5)', - 'rgba(45, 157, 120, 0.5)', - 'rgba(216, 55, 144, 0.5)', - 'rgba(227, 72, 80, 0.5)', - 'rgba(103, 103, 236, 0.5)', - 'rgba(68, 181, 86, 0.5)', + '#2680eb', + '#9256d9', + '#44b556', + '#e68619', + '#e34850', + '#1b959a', + '#d83790', + '#85d044', ]; -export default function EventsChart({ websiteId, startDate, endDate, unit, className }) { +export default function EventsChart({ websiteId, startDate, endDate, unit }) { const [data, setData] = useState(); const datasets = useMemo(() => { if (!data) return []; - return Object.keys(data).map((key, index) => ({ - label: key, - data: data[key], - lineTension: 0, - backgroundColor: COLORS[index], - borderColor: COLORS[index], - borderWidth: 1, - })); + return Object.keys(data).map((key, index) => { + const color = tinycolor(COLORS[index]); + return { + label: key, + data: data[key], + lineTension: 0, + backgroundColor: color.setAlpha(0.4).toRgbString(), + borderColor: color.setAlpha(0.5).toRgbString(), + borderWidth: 1, + }; + }); }, [data]); async function loadData() { @@ -55,6 +60,14 @@ export default function EventsChart({ websiteId, startDate, endDate, unit, class setData(map); } + function handleCreate(options) { + const legend = { + position: 'bottom', + }; + + options.legend = legend; + } + function handleUpdate(chart) { chart.data.datasets = datasets; @@ -70,15 +83,14 @@ export default function EventsChart({ websiteId, startDate, endDate, unit, class } return ( -
- -
+ ); } diff --git a/components/metrics/EventsTable.js b/components/metrics/EventsTable.js index 9110bfb8..91e5a401 100644 --- a/components/metrics/EventsTable.js +++ b/components/metrics/EventsTable.js @@ -2,7 +2,7 @@ import React from 'react'; import MetricsTable from './MetricsTable'; import styles from './EventsTable.module.css'; -export default function DevicesTable({ +export default function EventsTable({ websiteId, startDate, endDate, @@ -19,14 +19,19 @@ export default function DevicesTable({ startDate={startDate} endDate={endDate} limit={limit} - renderLabel={({ w, x }) => ( - <> - {w} - {x} - - )} + renderLabel={({ x }) =>