Add Donut component that can render server side (if that ever actually works)
This commit is contained in:
parent
17edc05ec4
commit
e67e01fd0a
29
src/app/donut/page.tsx
Normal file
29
src/app/donut/page.tsx
Normal file
@ -0,0 +1,29 @@
|
||||
import './styles.css';
|
||||
import {Donut} from "@/components/Donut";
|
||||
|
||||
const data = [
|
||||
{
|
||||
label: "One",
|
||||
colour: "#ce4b99",
|
||||
value: 35,
|
||||
},
|
||||
{
|
||||
label: "Two",
|
||||
colour: "#b1c94e",
|
||||
value: 15,
|
||||
},
|
||||
{
|
||||
label: "Three",
|
||||
colour: "#377bbc",
|
||||
value: 50,
|
||||
},
|
||||
{
|
||||
label: "Four",
|
||||
colour: "#f49f35",
|
||||
value: 100,
|
||||
}
|
||||
]
|
||||
|
||||
export default function App() {
|
||||
return <Donut data={data}/>
|
||||
}
|
||||
19
src/app/donut/styles.css
Normal file
19
src/app/donut/styles.css
Normal file
@ -0,0 +1,19 @@
|
||||
.chart-text {
|
||||
font: 16px/1.4em "Montserrat", Arial, sans-serif;
|
||||
fill: #000;
|
||||
transform: translateY(0.25em);
|
||||
}
|
||||
|
||||
.chart-number {
|
||||
font-size: 0.6em;
|
||||
line-height: 1;
|
||||
text-anchor: middle;
|
||||
transform: translateY(-0.25em);
|
||||
}
|
||||
|
||||
.chart-label {
|
||||
font-size: 0.2em;
|
||||
text-transform: uppercase;
|
||||
text-anchor: middle;
|
||||
transform: translateY(0.7em);
|
||||
}
|
||||
90
src/components/Donut.tsx
Normal file
90
src/components/Donut.tsx
Normal file
@ -0,0 +1,90 @@
|
||||
import * as R from "ramda";
|
||||
import DonutTooltip from "@/components/DonutTooltip";
|
||||
import './donut-styles.css'
|
||||
|
||||
export type DonutChartData = {
|
||||
label: string;
|
||||
colour: string;
|
||||
value: number;
|
||||
}[];
|
||||
|
||||
export function Donut({data, centerLabel, formatter, hoverSuffix}: {
|
||||
data: DonutChartData,
|
||||
formatter: (value: number) => string,
|
||||
centerLabel: string,
|
||||
hoverSuffix: string,
|
||||
}) {
|
||||
const radius = 100 / (2 * Math.PI);
|
||||
const backgroundColour = "#d2d3d4";
|
||||
|
||||
const preppedData = R.sortBy(R.compose(R.negate, R.prop('value')), data);
|
||||
const total = R.sum(R.pluck('value', preppedData));
|
||||
const normalisingFactor = 100 / total;
|
||||
|
||||
return (
|
||||
<>
|
||||
<DonutTooltip/>
|
||||
<svg width="100%" height="100%" viewBox="0 0 42 42" className="donut">
|
||||
{/* The hole in the middle of the donut */}
|
||||
<circle
|
||||
className="donut-hole"
|
||||
cx="21"
|
||||
cy="21"
|
||||
r={radius}
|
||||
fill="#fff"
|
||||
/>
|
||||
|
||||
{/* The ring in the background of the donut, if it was not 100% full */}
|
||||
<circle
|
||||
className="donut-ring"
|
||||
cx="21"
|
||||
cy="21"
|
||||
r={radius}
|
||||
fill="transparent"
|
||||
stroke={backgroundColour}
|
||||
strokeWidth="3"
|
||||
/>
|
||||
|
||||
{
|
||||
preppedData.map((segment, index, array) => {
|
||||
const {
|
||||
label,
|
||||
colour,
|
||||
value
|
||||
} = segment;
|
||||
const precedingSegments = array.slice(0, index);
|
||||
const offset = precedingSegments.reduce((acc, segment) => acc + segment.value, 0);
|
||||
|
||||
return (
|
||||
<circle
|
||||
data-tooltip-id="donut-tooltip"
|
||||
data-tooltip-content={`${label}: ${formatter(value)}${hoverSuffix}`}
|
||||
data-tooltip-place="top"
|
||||
|
||||
key={index}
|
||||
className="donut-segment"
|
||||
cx="21"
|
||||
cy="21"
|
||||
r={radius}
|
||||
fill="transparent"
|
||||
stroke={colour}
|
||||
strokeWidth="3"
|
||||
strokeDasharray={`${value * normalisingFactor} ${100 - value * normalisingFactor}`}
|
||||
strokeDashoffset={100 - (offset * normalisingFactor) + 25}
|
||||
/>
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
<g className="chart-text">
|
||||
<text x="50%" y="50%" className="chart-number">
|
||||
{formatter(total)}
|
||||
</text>
|
||||
<text x="50%" y="50%" className="chart-label">
|
||||
{centerLabel}
|
||||
</text>
|
||||
</g>
|
||||
</svg>
|
||||
</>
|
||||
);
|
||||
}
|
||||
7
src/components/DonutTooltip.tsx
Normal file
7
src/components/DonutTooltip.tsx
Normal file
@ -0,0 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import {Tooltip} from "react-tooltip";
|
||||
|
||||
export default function DonutTooltip() {
|
||||
return <Tooltip id="donut-tooltip"/>
|
||||
}
|
||||
@ -34,7 +34,7 @@ export default function OverviewPage({
|
||||
<main className="m-6">
|
||||
<h1 className="text-3xl font-semibold text-slate-900 dark:text-white my-2">{config.title}</h1>
|
||||
|
||||
<div className="grid gap-5 grid-cols-1 sm:grid-cols-4">
|
||||
<div className="grid gap-5 grid-cols-1 sm:grid-cols-2 lg:grid-cols-4">
|
||||
{config.subjects.map((subject) => (
|
||||
<SubjectOverviewCard
|
||||
key={subject.projectId}
|
||||
|
||||
@ -1,24 +1,20 @@
|
||||
"use client";
|
||||
|
||||
import {Card, DonutChart, Title} from "@tremor/react";
|
||||
import {Card, Title} from "@tremor/react";
|
||||
import * as R from "ramda";
|
||||
import {Data} from "@/data/fetchData";
|
||||
import {Donut, DonutChartData} from "@/components/Donut";
|
||||
|
||||
function useBreakdownData(data: Data): {
|
||||
name: string,
|
||||
value: number,
|
||||
colour: string
|
||||
}[] {
|
||||
function useBreakdownData(data: Data): DonutChartData {
|
||||
const sorted = R.sortBy(R.prop('projectId'), data.timeEntries);
|
||||
const grouped = R.groupWith(R.eqBy(R.prop('projectId')), sorted);
|
||||
|
||||
return grouped
|
||||
.map((entries) => {
|
||||
const project = data.projects.find(p => p.projectId === entries[0].projectId)!;
|
||||
const totalDuration = (entries).map((entry) => entry.duration).reduce((a, b) => a + b, 0);
|
||||
|
||||
return ({
|
||||
name: project.name,
|
||||
value: (entries).map((entry) => entry.duration).reduce((a, b) => a + b, 0),
|
||||
label: project.name,
|
||||
value: (totalDuration / (60 * 60)),
|
||||
colour: project.color,
|
||||
});
|
||||
})
|
||||
@ -30,20 +26,15 @@ export function SubjectComparisonCard({
|
||||
data: Data,
|
||||
}) {
|
||||
const breakdownData = useBreakdownData(data);
|
||||
const colours = breakdownData.map((entry) => entry.colour);
|
||||
|
||||
const valueFormatter = (number: number) => `${(number / (60 * 60)).toFixed(2)} hours`;
|
||||
|
||||
return (
|
||||
<Card className="col">
|
||||
<Title>Relative Breakdown</Title>
|
||||
<DonutChart
|
||||
className="mt-6"
|
||||
<Donut
|
||||
data={breakdownData ?? []}
|
||||
category="value"
|
||||
index="name"
|
||||
valueFormatter={valueFormatter}
|
||||
colors={colours}
|
||||
formatter={v => v.toFixed(2)}
|
||||
centerLabel={"hours"}
|
||||
hoverSuffix={"h"}
|
||||
/>
|
||||
</Card>
|
||||
)
|
||||
|
||||
19
src/components/donut-styles.css
Normal file
19
src/components/donut-styles.css
Normal file
@ -0,0 +1,19 @@
|
||||
.chart-text {
|
||||
font: 16px/1.4em "Montserrat", Arial, sans-serif;
|
||||
fill: #000;
|
||||
transform: translateY(0.25em);
|
||||
}
|
||||
|
||||
.chart-number {
|
||||
font-size: 0.6em;
|
||||
line-height: 1;
|
||||
text-anchor: middle;
|
||||
transform: translateY(-0.25em);
|
||||
}
|
||||
|
||||
.chart-label {
|
||||
font-size: 0.2em;
|
||||
text-transform: uppercase;
|
||||
text-anchor: middle;
|
||||
transform: translateY(0.7em);
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user