refactor: migrate to recharts
This commit is contained in:
1 parent
3e4de842c1
commit
dd859a6b67
10 files changed
+689
-948
No files matched your search
Generated
+394
-750
File diff suppressed because it is too large.
Load diff
@@ -17,9 +17,6 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@apollo/client": "^4.2.0",
|
||||
"@nivo/bar": "^0.99.0",
|
||||
"@nivo/line": "^0.99.0",
|
||||
"@nivo/pie": "^0.99.0",
|
||||
"clsx": "^2.1.1",
|
||||
"date-fns": "^4.3.0",
|
||||
"graphql": "^16.14.0",
|
||||
@@ -27,6 +24,7 @@
|
||||
"react-dom": "^19.2.6",
|
||||
"react-hook-form": "^7.76.1",
|
||||
"react-router-dom": "^7.18.4",
|
||||
"recharts": "^3.10.1",
|
||||
"use-url-search-params": "^2.5.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
import React from "react";
|
||||
|
||||
import { chartTooltipStyle } from "./chartTheme";
|
||||
|
||||
type TooltipRow = {
|
||||
name: React.ReactNode;
|
||||
value: React.ReactNode;
|
||||
color: string;
|
||||
};
|
||||
|
||||
export const ChartTooltip = ({ title, rows }: { title?: React.ReactNode; rows: TooltipRow[] }) => (
|
||||
<div style={chartTooltipStyle}>
|
||||
{title !== undefined && (
|
||||
<div>
|
||||
<strong>{title}</strong>
|
||||
</div>
|
||||
)}
|
||||
{rows.map((row, index) => (
|
||||
<div key={index}>
|
||||
<span style={{ background: row.color, width: 13, height: 13, display: "inline-block" }} />{" "}
|
||||
<strong>{row.value}</strong> {row.name}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
@@ -1,9 +1,10 @@
|
||||
import { useQuery } from "@apollo/client/react";
|
||||
import { BarDatum, ResponsiveBar } from "@nivo/bar";
|
||||
import { Bar, BarChart, CartesianGrid, Rectangle, Tooltip, XAxis, YAxis } from "recharts";
|
||||
|
||||
import { ChartTooltip } from "./ChartTooltip";
|
||||
import styles from "./Stats.module.css";
|
||||
import { StatsView } from "./StatsView";
|
||||
import { chartTheme } from "./chartTheme";
|
||||
import { axisLabelStyle, axisProps, chartColor, chartStyle, gridStroke, tooltipCursor } from "./chartTheme";
|
||||
import { OrderStatsDocument, SchemaOrderStatsByTime } from "../../types";
|
||||
|
||||
const days = { 1: "Mandag", 2: "Tirsdag", 3: "Onsdag", 4: "Torsdag", 5: "Fredag", 6: "Lørdag", 7: "Søndag" };
|
||||
@@ -23,18 +24,39 @@ const months = {
|
||||
12: "Desember",
|
||||
};
|
||||
|
||||
const OrderBar = ({ data }: { data: BarDatum[] }) => (
|
||||
type OrderDatum = {
|
||||
id: string | number;
|
||||
label: number;
|
||||
value: number;
|
||||
};
|
||||
|
||||
const OrderBar = ({ data }: { data: OrderDatum[] }) => (
|
||||
<div className={styles.chart}>
|
||||
<ResponsiveBar
|
||||
<BarChart
|
||||
responsive
|
||||
style={chartStyle}
|
||||
data={data}
|
||||
theme={chartTheme}
|
||||
margin={{ top: 10, right: 10, bottom: 70, left: 60 }}
|
||||
colorBy="indexValue"
|
||||
enableLabel={false}
|
||||
tooltipLabel={(d) => d.id.toString()}
|
||||
axisLeft={{ legend: "Ordre", legendPosition: "middle", legendOffset: -50 }}
|
||||
axisBottom={{ tickRotation: -35 }}
|
||||
/>
|
||||
barCategoryGap="5%"
|
||||
margin={{ top: 10, right: 10, bottom: 10, left: 10 }}
|
||||
>
|
||||
<CartesianGrid stroke={gridStroke} vertical={false} />
|
||||
<XAxis {...axisProps} dataKey="id" interval={0} angle={-35} textAnchor="end" height={60} />
|
||||
<YAxis
|
||||
{...axisProps}
|
||||
allowDecimals={false}
|
||||
label={{ value: "Ordre", angle: -90, position: "insideLeft", style: axisLabelStyle }}
|
||||
/>
|
||||
<Tooltip
|
||||
cursor={tooltipCursor}
|
||||
content={({ active, payload }) => {
|
||||
const entry = payload[0];
|
||||
if (!active || !entry) return null;
|
||||
const index = data.findIndex((datum) => datum.id === entry.payload.id);
|
||||
return <ChartTooltip rows={[{ name: entry.payload.id, value: entry.value, color: chartColor(index) }]} />;
|
||||
}}
|
||||
/>
|
||||
<Bar dataKey="value" shape={(props) => <Rectangle {...props} fill={chartColor(props.index)} />} />
|
||||
</BarChart>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -48,7 +70,7 @@ const OrderStats = () => {
|
||||
data={(data?.orderStats?.hourly || []).map((stat: SchemaOrderStatsByTime) => ({
|
||||
id: `${stat.period.toString().padStart(2, "0")}:00`,
|
||||
label: stat.period,
|
||||
value: stat.count,
|
||||
value: Number(stat.count),
|
||||
}))}
|
||||
/>
|
||||
<h3>Ordre per ukedag</h3>
|
||||
@@ -57,7 +79,7 @@ const OrderStats = () => {
|
||||
/* @ts-expect-error Here, stat.period can only be a key in days range. */
|
||||
id: days[stat.period],
|
||||
label: stat.period,
|
||||
value: stat.count,
|
||||
value: Number(stat.count),
|
||||
}))}
|
||||
/>
|
||||
<h3>Ordre per måned</h3>
|
||||
@@ -66,7 +88,7 @@ const OrderStats = () => {
|
||||
/* @ts-expect-error Here, stat.period can only be a key in months range. */
|
||||
id: months[stat.period],
|
||||
label: stat.period,
|
||||
value: stat.count,
|
||||
value: Number(stat.count),
|
||||
}))}
|
||||
/>
|
||||
<h3>Ordre per år</h3>
|
||||
@@ -74,7 +96,7 @@ const OrderStats = () => {
|
||||
data={(data?.orderStats?.yearly || []).map((stat: SchemaOrderStatsByTime) => ({
|
||||
id: stat.period,
|
||||
label: stat.period,
|
||||
value: stat.count,
|
||||
value: Number(stat.count),
|
||||
}))}
|
||||
/>
|
||||
</StatsView>
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { useQuery } from "@apollo/client/react";
|
||||
import { ResponsivePie } from "@nivo/pie";
|
||||
import { Pie, PieChart, PieLabelRenderProps, Sector, Tooltip } from "recharts";
|
||||
|
||||
import { ChartTooltip } from "./ChartTooltip";
|
||||
import styles from "./Stats.module.css";
|
||||
import { StatsView } from "./StatsView";
|
||||
import { chartTheme } from "./chartTheme";
|
||||
import { chartColor, chartStyle, chartTextColor } from "./chartTheme";
|
||||
import { AllProductStatsDocument } from "../../types";
|
||||
|
||||
type UserCount = {
|
||||
@@ -11,6 +12,45 @@ type UserCount = {
|
||||
count: number;
|
||||
};
|
||||
|
||||
const RADIAN = Math.PI / 180;
|
||||
// Slices narrower than this are too small to hold their count
|
||||
const MIN_VALUE_LABEL_ANGLE = 8;
|
||||
|
||||
// The user's name goes outside the slice, at the end of its label line, and the count inside it
|
||||
const renderLabel = ({
|
||||
cx,
|
||||
cy,
|
||||
midAngle = 0,
|
||||
innerRadius,
|
||||
outerRadius,
|
||||
percent = 0,
|
||||
x,
|
||||
y,
|
||||
textAnchor,
|
||||
name,
|
||||
value,
|
||||
}: PieLabelRenderProps) => {
|
||||
const radius = Number(innerRadius) + (Number(outerRadius) - Number(innerRadius)) / 2;
|
||||
return (
|
||||
<g>
|
||||
<text x={x} y={y} textAnchor={textAnchor} dominantBaseline="central" fill={chartTextColor}>
|
||||
{name}
|
||||
</text>
|
||||
{percent * 360 >= MIN_VALUE_LABEL_ANGLE && (
|
||||
<text
|
||||
x={Number(cx) + radius * Math.cos(-midAngle * RADIAN)}
|
||||
y={Number(cy) + radius * Math.sin(-midAngle * RADIAN)}
|
||||
textAnchor="middle"
|
||||
dominantBaseline="central"
|
||||
fill="#17121a"
|
||||
>
|
||||
{value}
|
||||
</text>
|
||||
)}
|
||||
</g>
|
||||
);
|
||||
};
|
||||
|
||||
const ProductStats = () => {
|
||||
const { data, loading } = useQuery(AllProductStatsDocument, {
|
||||
variables: { active: true },
|
||||
@@ -23,17 +63,25 @@ const ProductStats = () => {
|
||||
<div key={product.id}>
|
||||
{product.name}
|
||||
<div className={styles.productChart}>
|
||||
<ResponsivePie
|
||||
margin={{ top: 30, right: 50, bottom: 30, left: 50 }}
|
||||
data={product.userCounts.map((uc: UserCount) => ({
|
||||
id: uc.name,
|
||||
label: uc.name,
|
||||
value: uc.count,
|
||||
}))}
|
||||
theme={chartTheme}
|
||||
arcLabelsTextColor="#17121a"
|
||||
arcLabelsSkipAngle={8}
|
||||
/>
|
||||
<PieChart responsive style={chartStyle} margin={{ top: 30, right: 50, bottom: 30, left: 50 }}>
|
||||
<Pie
|
||||
data={product.userCounts as UserCount[]}
|
||||
dataKey="count"
|
||||
nameKey="name"
|
||||
outerRadius="80%"
|
||||
label={renderLabel}
|
||||
labelLine={{ stroke: "#b9a3c5" }}
|
||||
shape={(props) => <Sector {...props} fill={chartColor(props.index)} />}
|
||||
/>
|
||||
<Tooltip
|
||||
content={({ active, payload }) => {
|
||||
const entry = payload[0];
|
||||
if (!active || !entry) return null;
|
||||
const index = (product.userCounts as UserCount[]).findIndex((uc) => uc.name === entry.name);
|
||||
return <ChartTooltip rows={[{ name: entry.name, value: entry.value, color: chartColor(index) }]} />;
|
||||
}}
|
||||
/>
|
||||
</PieChart>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
import { addMonths, format, parseISO, startOfMonth } from "date-fns";
|
||||
import { CartesianGrid, Legend, Line, LineChart, Tooltip, XAxis, YAxis } from "recharts";
|
||||
|
||||
import { ChartTooltip } from "./ChartTooltip";
|
||||
import { axisLabelStyle, axisProps, chartColor, chartStyle, chartTextColor, gridStroke } from "./chartTheme";
|
||||
|
||||
type ProductOrderStat = {
|
||||
productName: string;
|
||||
data: { x: string; y: number }[];
|
||||
};
|
||||
|
||||
type Row = {
|
||||
date: number;
|
||||
// Keyed by product name; a product has no value before its first purchase
|
||||
totals: Record<string, number>;
|
||||
};
|
||||
|
||||
// One row per day with each product's running total. Products without purchases that day are left
|
||||
// out of the row, and connectNulls draws their line straight to the next day they have one.
|
||||
const toRows = (stats: ProductOrderStat[]): Row[] => {
|
||||
const rows = new Map<number, Row>();
|
||||
for (const { productName, data } of stats) {
|
||||
let total = 0;
|
||||
for (const point of data) {
|
||||
total += point.y;
|
||||
const date = parseISO(point.x).getTime();
|
||||
const row = rows.get(date) ?? { date, totals: {} };
|
||||
row.totals[productName] = total;
|
||||
rows.set(date, row);
|
||||
}
|
||||
}
|
||||
return [...rows.values()].sort((a, b) => a.date - b.date);
|
||||
};
|
||||
|
||||
// A tick on the first of each month, so each month is labelled once. Spans too short to have
|
||||
// two of them get Recharts' own ticks, labelled with the full date.
|
||||
const monthTicks = (rows: Row[]): number[] | undefined => {
|
||||
if (!rows.length) return undefined;
|
||||
const ticks = [];
|
||||
for (
|
||||
let month = startOfMonth(addMonths(rows[0].date, 1));
|
||||
month.getTime() <= rows[rows.length - 1].date;
|
||||
month = addMonths(month, 1)
|
||||
) {
|
||||
ticks.push(month.getTime());
|
||||
}
|
||||
return ticks.length >= 2 ? ticks : undefined;
|
||||
};
|
||||
|
||||
export const PurchasesOverTimeChart = ({ stats }: { stats: ProductOrderStat[] }) => {
|
||||
const rows = toRows(stats);
|
||||
const ticks = monthTicks(rows);
|
||||
return (
|
||||
<LineChart responsive style={chartStyle} data={rows} margin={{ top: 20, right: 30, bottom: 30, left: 10 }}>
|
||||
<CartesianGrid stroke={gridStroke} />
|
||||
<XAxis
|
||||
{...axisProps}
|
||||
dataKey="date"
|
||||
type="number"
|
||||
scale="time"
|
||||
domain={["dataMin", "dataMax"]}
|
||||
ticks={ticks}
|
||||
tickFormatter={(date: number) => format(date, ticks ? "MMM yyyy" : "yyyy-MM-dd")}
|
||||
label={{ value: "Date", position: "bottom", offset: 10, style: axisLabelStyle }}
|
||||
/>
|
||||
<YAxis
|
||||
{...axisProps}
|
||||
allowDecimals={false}
|
||||
label={{ value: "Purchases", angle: -90, position: "insideLeft", style: axisLabelStyle }}
|
||||
/>
|
||||
<Tooltip
|
||||
cursor={{ stroke: chartTextColor, strokeOpacity: 0.4 }}
|
||||
content={({ active, payload, label }) =>
|
||||
active && payload.length ? (
|
||||
<ChartTooltip
|
||||
title={format(Number(label), "yyyy-MM-dd")}
|
||||
rows={payload
|
||||
.filter((entry) => entry.value !== undefined)
|
||||
.map((entry) => ({ name: entry.name, value: entry.value, color: String(entry.color) }))}
|
||||
/>
|
||||
) : null
|
||||
}
|
||||
/>
|
||||
<Legend
|
||||
verticalAlign="top"
|
||||
formatter={(value) => <span style={{ color: chartTextColor }}>{value}</span>}
|
||||
wrapperStyle={{ paddingBottom: 8 }}
|
||||
/>
|
||||
{stats.map(({ productName }, index) => (
|
||||
<Line
|
||||
key={productName}
|
||||
name={productName}
|
||||
dataKey={(row: Row) => row.totals[productName]}
|
||||
stroke={chartColor(index)}
|
||||
strokeWidth={2}
|
||||
dot={{ r: 3, fill: chartColor(index) }}
|
||||
connectNulls
|
||||
/>
|
||||
))}
|
||||
</LineChart>
|
||||
);
|
||||
};
|
||||
@@ -1,65 +1,18 @@
|
||||
import { useQuery } from "@apollo/client/react";
|
||||
import { ResponsiveLine } from "@nivo/line";
|
||||
|
||||
import { PurchasesOverTimeChart } from "./PurchasesOverTimeChart";
|
||||
import styles from "./Stats.module.css";
|
||||
import { StatsView } from "./StatsView";
|
||||
import { chartTheme, chartTooltipStyle } from "./chartTheme";
|
||||
import { ProductStatsDocument } from "../../types";
|
||||
|
||||
const Stats = () => {
|
||||
const { data, loading } = useQuery(ProductStatsDocument);
|
||||
|
||||
const series = (data?.productStats || []).map(({ productName: id, data }) => ({
|
||||
id,
|
||||
data: data.map((point, index, points) => ({
|
||||
x: point.x,
|
||||
y: point.y + (points[index - 1]?.y || 0),
|
||||
})),
|
||||
}));
|
||||
|
||||
return (
|
||||
<StatsView loading={loading}>
|
||||
<h2>Kjøp over tid</h2>
|
||||
<div className={styles.chart}>
|
||||
<ResponsiveLine
|
||||
data={series}
|
||||
theme={chartTheme}
|
||||
margin={{ top: 50, right: 110, bottom: 50, left: 60 }}
|
||||
xScale={{
|
||||
type: "time",
|
||||
format: "%Y-%m-%d",
|
||||
useUTC: false,
|
||||
precision: "day",
|
||||
}}
|
||||
xFormat="time:%Y-%m-%d"
|
||||
yScale={{
|
||||
type: "linear",
|
||||
stacked: false,
|
||||
}}
|
||||
axisLeft={{
|
||||
legend: "Purchases",
|
||||
legendOffset: 12,
|
||||
}}
|
||||
axisBottom={{
|
||||
format: "%b %Y",
|
||||
legend: "Date",
|
||||
legendPosition: "middle",
|
||||
legendOffset: -20,
|
||||
}}
|
||||
useMesh
|
||||
pointLabelYOffset={0}
|
||||
tooltip={({ point }) => (
|
||||
<div style={chartTooltipStyle}>
|
||||
<div>
|
||||
<strong>{point.data.xFormatted}</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span style={{ background: point.seriesColor, width: 13, height: 13, display: "inline-block" }} />{" "}
|
||||
<strong>{String(point.data.yFormatted)}</strong> {point.seriesId}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
<PurchasesOverTimeChart stats={data?.productStats || []} />
|
||||
</div>
|
||||
</StatsView>
|
||||
);
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { useQuery } from "@apollo/client/react";
|
||||
import { ResponsiveBar } from "@nivo/bar";
|
||||
import { Bar, BarChart, CartesianGrid, Tooltip, XAxis, YAxis } from "recharts";
|
||||
|
||||
import { ChartTooltip } from "./ChartTooltip";
|
||||
import styles from "./Stats.module.css";
|
||||
import { StatsView } from "./StatsView";
|
||||
import { chartTheme } from "./chartTheme";
|
||||
import { axisLabelStyle, axisProps, chartColor, chartStyle, gridStroke, tooltipCursor } from "./chartTheme";
|
||||
import { GQLError } from "../../components/GQLError";
|
||||
import { UserStatsDocument } from "../../types";
|
||||
|
||||
@@ -19,32 +20,46 @@ const UserStats = () => {
|
||||
total: Number(user.profile?.orderSumTotal || 0),
|
||||
}))
|
||||
.sort((a, b) => b.total - a.total)
|
||||
.slice(0, 10)
|
||||
// nivo draws the first item of a horizontal chart at the bottom
|
||||
.reverse();
|
||||
.slice(0, 10);
|
||||
|
||||
const formatKroner = (value: number) => `${value.toLocaleString("no")} kr`;
|
||||
|
||||
return (
|
||||
<StatsView loading={loading}>
|
||||
<h3>Topp 10 etter total kjøpesum</h3>
|
||||
{users.length ? (
|
||||
<div className={styles.chart} style={{ height: Math.max(320, users.length * 44) }}>
|
||||
<ResponsiveBar
|
||||
<BarChart
|
||||
responsive
|
||||
style={chartStyle}
|
||||
data={users}
|
||||
keys={["total"]}
|
||||
indexBy="name"
|
||||
layout="horizontal"
|
||||
theme={chartTheme}
|
||||
margin={{ top: 10, right: 35, bottom: 40, left: 100 }}
|
||||
padding={0.3}
|
||||
enableLabel={false}
|
||||
axisBottom={{
|
||||
legend: "Kjøp totalt (kr)",
|
||||
legendPosition: "middle",
|
||||
legendOffset: 32,
|
||||
}}
|
||||
axisLeft={{ tickSize: 0, tickPadding: 8 }}
|
||||
valueFormat={(value) => `${Number(value).toLocaleString("no")} kr`}
|
||||
/>
|
||||
layout="vertical"
|
||||
barCategoryGap="15%"
|
||||
margin={{ top: 10, right: 35, bottom: 30, left: 10 }}
|
||||
>
|
||||
<CartesianGrid stroke={gridStroke} horizontal={false} />
|
||||
<XAxis
|
||||
{...axisProps}
|
||||
type="number"
|
||||
label={{ value: "Kjøp totalt (kr)", position: "bottom", offset: 10, style: axisLabelStyle }}
|
||||
/>
|
||||
<YAxis {...axisProps} type="category" dataKey="name" width={100} tickLine={false} />
|
||||
<Tooltip
|
||||
cursor={tooltipCursor}
|
||||
content={({ active, payload }) => {
|
||||
const entry = payload[0];
|
||||
if (!active || !entry) return null;
|
||||
return (
|
||||
<ChartTooltip
|
||||
rows={[
|
||||
{ name: entry.payload.name, value: formatKroner(entry.payload.total), color: chartColor(0) },
|
||||
]}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<Bar dataKey="total" fill={chartColor(0)} />
|
||||
</BarChart>
|
||||
</div>
|
||||
) : (
|
||||
<p>Ingen kjøp å vise ennå.</p>
|
||||
|
||||
@@ -1,53 +1,33 @@
|
||||
export const chartTheme = {
|
||||
background: "transparent",
|
||||
text: {
|
||||
fill: "#f7effa",
|
||||
fontFamily: '"IBM Plex Mono", monospace',
|
||||
fontSize: 13,
|
||||
textShadow: "none",
|
||||
},
|
||||
axis: {
|
||||
domain: {
|
||||
line: { stroke: "#b9a3c5", strokeWidth: 1 },
|
||||
},
|
||||
ticks: {
|
||||
line: { stroke: "#b9a3c5", strokeWidth: 1 },
|
||||
text: { fill: "#f7effa", fontSize: 12, textShadow: "none" },
|
||||
},
|
||||
legend: {
|
||||
text: { fill: "#f7effa", fontSize: 13, textShadow: "none" },
|
||||
},
|
||||
},
|
||||
grid: {
|
||||
line: { stroke: "rgba(221, 81, 127, 0.24)", strokeWidth: 1 },
|
||||
},
|
||||
legends: {
|
||||
text: { fill: "#f7effa", fontSize: 12, textShadow: "none" },
|
||||
title: {
|
||||
text: { fill: "#f7effa", fontSize: 13, textShadow: "none" },
|
||||
},
|
||||
},
|
||||
labels: {
|
||||
text: {
|
||||
fill: "#ffffff",
|
||||
fontSize: 13,
|
||||
textShadow: "none",
|
||||
},
|
||||
},
|
||||
tooltip: {
|
||||
container: {
|
||||
background: "#2b1733",
|
||||
color: "#ffffff",
|
||||
fontFamily: '"IBM Plex Mono", monospace',
|
||||
fontSize: 13,
|
||||
border: "1px solid #dd517f",
|
||||
borderRadius: 4,
|
||||
boxShadow: "0 4px 16px rgba(0, 0, 0, 0.45)",
|
||||
},
|
||||
},
|
||||
import type { CSSProperties } from "react";
|
||||
|
||||
export const chartTextColor = "#f7effa";
|
||||
const axisColor = "#b9a3c5";
|
||||
|
||||
// nivo's default categorical scheme, kept so the charts look the same after moving to Recharts
|
||||
export const chartColors = ["#e8c1a0", "#f47560", "#f1e15b", "#e8a838", "#61cdbb", "#97e3d5"];
|
||||
export const chartColor = (index: number) => chartColors[index % chartColors.length];
|
||||
|
||||
// Recharts draws plain SVG; give it the app font and fill its container
|
||||
export const chartStyle: CSSProperties = {
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
fontFamily: '"IBM Plex Mono", monospace',
|
||||
fontSize: 13,
|
||||
};
|
||||
|
||||
export const chartTooltipStyle = {
|
||||
export const axisProps = {
|
||||
stroke: axisColor,
|
||||
tick: { fill: chartTextColor, fontSize: 12 },
|
||||
tickLine: { stroke: axisColor },
|
||||
};
|
||||
|
||||
export const axisLabelStyle = { fill: chartTextColor, fontSize: 13 };
|
||||
|
||||
export const gridStroke = "rgba(221, 81, 127, 0.24)";
|
||||
|
||||
export const tooltipCursor = { fill: "rgba(255, 255, 255, 0.08)" };
|
||||
|
||||
export const chartTooltipStyle: CSSProperties = {
|
||||
background: "#2b1733",
|
||||
color: "#ffffff",
|
||||
padding: "9px 12px",
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
import { useQuery } from "@apollo/client/react";
|
||||
import { ResponsiveLine } from "@nivo/line";
|
||||
import { useParams } from "react-router-dom";
|
||||
|
||||
import styles from "./UserDetail.module.css";
|
||||
import { GQLError } from "../../components/GQLError";
|
||||
import { SchemaUser as User, UserDetailDocument, SchemaUserProfile as UserProfile } from "../../types";
|
||||
import { formatMonth, formatTime } from "../../utils/time";
|
||||
import { chartTheme, chartTooltipStyle } from "../stats/chartTheme";
|
||||
import { PurchasesOverTimeChart } from "../stats/PurchasesOverTimeChart";
|
||||
|
||||
const UserDetail = () => {
|
||||
const { userId } = useParams();
|
||||
@@ -18,13 +17,6 @@ const UserDetail = () => {
|
||||
|
||||
const user = data?.user as User;
|
||||
const profile = user?.profile as UserProfile;
|
||||
const series = (profile?.productOrderStats || []).map(({ productName: id, data }) => ({
|
||||
id,
|
||||
data: data.map((point, index, points) => ({
|
||||
x: point.x,
|
||||
y: point.y + (points[index - 1]?.y || 0),
|
||||
})),
|
||||
}));
|
||||
return (
|
||||
<div className={styles.userDetail}>
|
||||
<div>
|
||||
@@ -61,45 +53,7 @@ const UserDetail = () => {
|
||||
<div>
|
||||
<h2>Kjøp over tid</h2>
|
||||
<div className={styles.chart}>
|
||||
<ResponsiveLine
|
||||
data={series}
|
||||
theme={chartTheme}
|
||||
margin={{ top: 50, right: 110, bottom: 50, left: 60 }}
|
||||
xScale={{
|
||||
type: "time",
|
||||
format: "%Y-%m-%d",
|
||||
useUTC: false,
|
||||
precision: "day",
|
||||
}}
|
||||
xFormat="time:%Y-%m-%d"
|
||||
yScale={{
|
||||
type: "linear",
|
||||
stacked: false,
|
||||
}}
|
||||
axisLeft={{
|
||||
legend: "Purchases",
|
||||
legendOffset: 12,
|
||||
}}
|
||||
axisBottom={{
|
||||
format: "%b %Y",
|
||||
legend: "Date",
|
||||
legendPosition: "middle",
|
||||
legendOffset: -20,
|
||||
}}
|
||||
useMesh
|
||||
pointLabelYOffset={0}
|
||||
tooltip={({ point }) => (
|
||||
<div style={chartTooltipStyle}>
|
||||
<div>
|
||||
<strong>{point.data.xFormatted}</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span style={{ background: point.seriesColor, width: 13, height: 13, display: "inline-block" }} />{" "}
|
||||
<strong>{String(point.data.yFormatted)}</strong> {point.seriesId}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
<PurchasesOverTimeChart stats={profile?.productOrderStats || []} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in new issue
Block a user