diff --git a/.gitignore b/.gitignore index 3d841da..fe9e8da 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ node_modules _site *.local data +local diff --git a/src/components/daily-registrations-report.test.tsx b/src/components/daily-registrations-report.test.tsx new file mode 100644 index 0000000..b974e06 --- /dev/null +++ b/src/components/daily-registrations-report.test.tsx @@ -0,0 +1,39 @@ +import { expect } from "chai"; +import { yearMonthDayParse } from "../formats"; +import { ProcessedResult } from "../models/daily-registrations-report-data"; +import { tabulate } from "./daily-registrations-report"; + +describe("DailyRegistrationsReport", () => { + describe("#tabulate", () => { + it("renders a table", () => { + const results: ProcessedResult[] = [ + { + date: yearMonthDayParse("2020-01-01"), + totalUsers: 5, + fullyRegisteredUsers: 1, + totalUsersCumulative: 10, + fullyRegisteredUsersCumulative: 2, + }, + { + date: yearMonthDayParse("2020-01-02"), + totalUsers: 6, + fullyRegisteredUsers: 2, + totalUsersCumulative: 17, + fullyRegisteredUsersCumulative: 4, + }, + ]; + + const table = tabulate(results); + + expect(table).to.deep.equal({ + header: ["", "2020-01-01", "2020-01-02"], + body: [ + ["New Users", 5, 6], + ["New Fully Registered Users", 1, 2], + ["Cumulative Users", 10, 17], + ["Cumulative Fully Registered Users", 2, 4], + ], + }); + }); + }); +}); diff --git a/src/components/daily-registrations-report.tsx b/src/components/daily-registrations-report.tsx new file mode 100644 index 0000000..524e4c6 --- /dev/null +++ b/src/components/daily-registrations-report.tsx @@ -0,0 +1,125 @@ +import { VNode } from "preact"; +import { useQuery } from "preact-fetching"; +import { useRef, useState, useContext } from "preact/hooks"; +import * as Plot from "@observablehq/plot"; +import Markdown from "preact-markdown"; +import { ReportFilterContext } from "../contexts/report-filter-context"; +import useResizeListener from "../hooks/resize-listener"; +import { + DataType, + loadData, + ProcessedResult, + ProcessedRenderableData, + toRenderableData, +} from "../models/daily-registrations-report-data"; +import PlotComponent from "./plot"; +import { formatSIDropTrailingZeroes, formatWithCommas, yearMonthDayFormat } from "../formats"; +import Table, { TableData } from "./table"; +import Accordion from "./accordion"; + +function plot({ data, width }: { data: ProcessedRenderableData[]; width?: number }): HTMLElement { + return Plot.plot({ + color: { + legend: true, + type: "ordinal", + tickFormat: (type: DataType): string => { + switch (type) { + case DataType.TOTAL_USERS: + case DataType.TOTAL_USERS_CUMULATIVE: + return "Total Users"; + case DataType.FULLY_REGISTERED_USERS: + case DataType.FULLY_REGISTERED_USERS_CUMULATIVE: + return "Full Registered Users"; + default: + throw new Error(`Unknown type ${type}`); + } + }, + }, + marks: [ + Plot.ruleY([0]), + Plot.line(data, { + x: "date", + y: "value", + z: "type", + stroke: "type", + }), + ], + width, + y: { + tickFormat: formatSIDropTrailingZeroes, + }, + }); +} + +/** + * Assumes that results is pre-sorted, pre-filtered + */ +function tabulate(results: ProcessedResult[]): TableData { + return { + header: ["", ...results.map(({ date }) => yearMonthDayFormat(date))], + body: [ + ["New Users", ...results.map(({ totalUsers }) => totalUsers)], + [ + "New Fully Registered Users", + ...results.map(({ fullyRegisteredUsers }) => fullyRegisteredUsers), + ], + ["Cumulative Users", ...results.map(({ totalUsersCumulative }) => totalUsersCumulative)], + [ + "Cumulative Fully Registered Users", + ...results.map(({ fullyRegisteredUsersCumulative }) => fullyRegisteredUsersCumulative), + ], + ], + }; +} + +function DailyRegistrationsReport(): VNode { + const ref = useRef(null as HTMLDivElement | null); + const [width, setWidth] = useState(undefined as number | undefined); + useResizeListener(() => setWidth(ref.current?.offsetWidth)); + const { start, finish, env, cumulative } = useContext(ReportFilterContext); + + const { data } = useQuery(`daily-registrations-${finish.valueOf()}`, () => loadData(finish, env)); + + const filteredData = + data && + toRenderableData(data).filter(({ type }) => { + switch (type) { + case DataType.TOTAL_USERS: + case DataType.FULLY_REGISTERED_USERS: + return !cumulative; + case DataType.TOTAL_USERS_CUMULATIVE: + case DataType.FULLY_REGISTERED_USERS_CUMULATIVE: + return !!cumulative; + default: + throw new Error(`Unknown data type ${type}`); + } + }); + + const windowedData = data && data.filter(({ date }) => start <= date && date <= finish); + + return ( +