mirror of
https://github.com/apricote/Listory.git
synced 2026-01-13 21:21:02 +00:00
fix(frontend): improve reporting charts
This commit is contained in:
parent
8c5f495ce5
commit
bdbe5f574a
3 changed files with 179 additions and 340 deletions
|
|
@ -1,13 +1,15 @@
|
|||
import { AxisBottom, AxisLeft } from "@vx/axis";
|
||||
import { curveBasis } from "@vx/curve";
|
||||
import { GradientLightgreenGreen } from "@vx/gradient";
|
||||
import { Grid } from "@vx/grid";
|
||||
import { Group } from "@vx/group";
|
||||
import { scaleLinear, scaleTime } from "@vx/scale";
|
||||
import { Area, Line, LinePath } from "@vx/shape";
|
||||
import { extent } from "d3-array";
|
||||
import { format, getTime } from "date-fns";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { Redirect } from "react-router-dom";
|
||||
import {
|
||||
Area,
|
||||
AreaChart,
|
||||
CartesianGrid,
|
||||
ResponsiveContainer,
|
||||
Tooltip,
|
||||
XAxis,
|
||||
YAxis,
|
||||
} from "recharts";
|
||||
import { getListensReport } from "../api/api";
|
||||
import { ListenReportItem } from "../api/entities/listen-report-item";
|
||||
import { ListenReportOptions } from "../api/entities/listen-report-options";
|
||||
|
|
@ -74,179 +76,47 @@ export const ReportListens: React.FC = () => {
|
|||
const ReportGraph: React.FC<{
|
||||
options: ListenReportOptions;
|
||||
data: ListenReportItem[];
|
||||
width?: number;
|
||||
height?: number;
|
||||
margin?: {
|
||||
top: number;
|
||||
right: number;
|
||||
bottom: number;
|
||||
left: number;
|
||||
};
|
||||
}> = ({
|
||||
options,
|
||||
data,
|
||||
width = 900,
|
||||
height = 500,
|
||||
margin = { left: 70, right: 70, top: 20, bottom: 80 },
|
||||
}) => {
|
||||
// Then we'll create some bounds
|
||||
const xMax = width - margin.left - margin.right;
|
||||
const yMax = height - margin.top - margin.bottom;
|
||||
|
||||
// We'll make some helpers to get at the data we want
|
||||
const x = (d: ListenReportItem) => d.date;
|
||||
const y = (d: ListenReportItem) => d.count;
|
||||
|
||||
// responsive utils for axis ticks
|
||||
const numTicksForHeight = (heightT: number): number => {
|
||||
if (heightT <= 300) return 3;
|
||||
if (300 < heightT && heightT <= 600) return 5;
|
||||
return 10;
|
||||
};
|
||||
|
||||
const numTicksForWidth = (widthT: number): number => {
|
||||
if (widthT <= 300) return 2;
|
||||
if (300 < widthT && widthT <= 400) return 5;
|
||||
return 10;
|
||||
};
|
||||
|
||||
// And then scale the graph by our data
|
||||
const xScaleTime = scaleTime<number>({
|
||||
range: [0, xMax],
|
||||
domain: extent(data, x) as [Date, Date],
|
||||
});
|
||||
|
||||
/*
|
||||
const xScaleBand = scaleBand({
|
||||
range: [0, xMax],
|
||||
domain: extent(data, x) as [Date, Date],
|
||||
padding: 0.2,
|
||||
});
|
||||
*/
|
||||
const yScale = scaleLinear({
|
||||
range: [yMax, 0],
|
||||
domain: [0, Math.max(...data.map(y))],
|
||||
nice: true,
|
||||
});
|
||||
|
||||
// Compose together the scale and accessor functions to get point functions
|
||||
const compose = (scale: any, accessor: any) => (d: ListenReportItem) =>
|
||||
scale(accessor(d));
|
||||
const xPoint = compose(xScaleTime, x);
|
||||
const yPoint = compose(yScale, y);
|
||||
}> = ({ options, data }) => {
|
||||
const dataLocal = data.map(({ date, ...other }) => ({
|
||||
...other,
|
||||
date: getTime(date),
|
||||
}));
|
||||
|
||||
return (
|
||||
<svg width={width} height={height}>
|
||||
<GradientLightgreenGreen
|
||||
id="linear"
|
||||
vertical={false}
|
||||
fromOpacity={0.8}
|
||||
toOpacity={0.8}
|
||||
/>
|
||||
<Grid
|
||||
top={margin.top}
|
||||
left={margin.left}
|
||||
xScale={xScaleTime}
|
||||
yScale={yScale}
|
||||
stroke="rgba(142, 32, 95, 0.9)"
|
||||
width={xMax}
|
||||
height={yMax}
|
||||
numTicksRows={numTicksForHeight(height)}
|
||||
numTicksColumns={numTicksForWidth(width)}
|
||||
/>
|
||||
<Group top={margin.top} left={margin.left}>
|
||||
<ResponsiveContainer width="90%" height={400}>
|
||||
<AreaChart
|
||||
data={dataLocal}
|
||||
margin={{
|
||||
top: 5,
|
||||
right: 30,
|
||||
left: 20,
|
||||
bottom: 5,
|
||||
}}
|
||||
>
|
||||
<defs>
|
||||
<linearGradient id="colorPv" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="5%" stopColor="#48bb78" stopOpacity={0.8} />
|
||||
<stop offset="90%" stopColor="#48bb78" stopOpacity={0.1} />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<CartesianGrid vertical={false} />
|
||||
<XAxis
|
||||
scale="time"
|
||||
type="number"
|
||||
domain={["auto", "auto"]}
|
||||
dataKey="date"
|
||||
tickFormatter={(date) => format(date, "P")}
|
||||
/>
|
||||
<YAxis />
|
||||
<Tooltip separator=": " formatter={(value) => [value, "Listens"]} />
|
||||
<Area
|
||||
data={data}
|
||||
x={(d) => xScaleTime(x(d))}
|
||||
y0={(d) => yScale.range()[0]}
|
||||
y1={(d) => yScale(y(d))}
|
||||
strokeWidth={2}
|
||||
stroke={"transparent"}
|
||||
fill={"url(#linear)"}
|
||||
curve={curveBasis}
|
||||
type="monotone"
|
||||
dataKey="count"
|
||||
stroke="#48bb78"
|
||||
fillOpacity={1}
|
||||
fill="url(#colorPv)"
|
||||
/>
|
||||
<LinePath
|
||||
data={data}
|
||||
x={(d) => xScaleTime(x(d))}
|
||||
y={(d) => yScale(y(d))}
|
||||
stroke={"url('#linear')"}
|
||||
strokeWidth={2}
|
||||
curve={curveBasis}
|
||||
/>
|
||||
</Group>
|
||||
<Group left={margin.left}>
|
||||
<AxisLeft
|
||||
top={margin.top}
|
||||
left={0}
|
||||
scale={yScale}
|
||||
hideZero
|
||||
numTicks={numTicksForHeight(height)}
|
||||
label="Axis Left Label"
|
||||
labelProps={{
|
||||
fill: "#8e205f",
|
||||
textAnchor: "middle",
|
||||
fontSize: 12,
|
||||
}}
|
||||
stroke="#1b1a1e"
|
||||
tickStroke="#8e205f"
|
||||
tickLabelProps={(value, index) => ({
|
||||
fill: "#8e205f",
|
||||
textAnchor: "end",
|
||||
fontSize: 10,
|
||||
dx: "-0.25em",
|
||||
dy: "0.25em",
|
||||
})}
|
||||
tickComponent={({ formattedValue, ...tickProps }) => (
|
||||
<text {...tickProps}>{formattedValue}</text>
|
||||
)}
|
||||
/>
|
||||
/>
|
||||
<AxisBottom
|
||||
top={height - margin.bottom}
|
||||
left={0}
|
||||
scale={xScaleTime}
|
||||
numTicks={numTicksForWidth(width)}
|
||||
label="Time"
|
||||
>
|
||||
{(axis) => {
|
||||
const tickLabelSize = 10;
|
||||
const tickRotate = 45;
|
||||
const tickColor = "#8e205f";
|
||||
const axisCenter = (axis.axisToPoint.x - axis.axisFromPoint.x) / 2;
|
||||
return (
|
||||
<g className="my-custom-bottom-axis">
|
||||
{axis.ticks.map((tick, i) => {
|
||||
const tickX = tick.to.x;
|
||||
const tickY = tick.to.y + tickLabelSize + axis.tickLength;
|
||||
return (
|
||||
<Group
|
||||
key={`vx-tick-${tick.value}-${i}`}
|
||||
className={"vx-axis-tick"}
|
||||
>
|
||||
<Line from={tick.from} to={tick.to} stroke={tickColor} />
|
||||
<text
|
||||
transform={`translate(${tickX}, ${tickY}) rotate(${tickRotate})`}
|
||||
fontSize={tickLabelSize}
|
||||
textAnchor="middle"
|
||||
fill={tickColor}
|
||||
>
|
||||
{tick.formattedValue}
|
||||
</text>
|
||||
</Group>
|
||||
);
|
||||
})}
|
||||
<text
|
||||
textAnchor="middle"
|
||||
transform={`translate(${axisCenter}, 50)`}
|
||||
fontSize="8"
|
||||
>
|
||||
{axis.label}
|
||||
</text>
|
||||
</g>
|
||||
);
|
||||
}}
|
||||
</AxisBottom>
|
||||
</Group>
|
||||
</svg>
|
||||
</AreaChart>
|
||||
</ResponsiveContainer>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue