Compare commits
No commits in common. "cf1788ebcd840d4211bd133b4a24aec53e3e5ad5" and "17edc05ec4e1e2b6785b794a679fe46f55dcb568" have entirely different histories.
cf1788ebcd
...
17edc05ec4
@ -1,90 +0,0 @@
|
|||||||
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>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -1,7 +0,0 @@
|
|||||||
'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">
|
<main className="m-6">
|
||||||
<h1 className="text-3xl font-semibold text-slate-900 dark:text-white my-2">{config.title}</h1>
|
<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-2 lg:grid-cols-4">
|
<div className="grid gap-5 grid-cols-1 sm:grid-cols-4">
|
||||||
{config.subjects.map((subject) => (
|
{config.subjects.map((subject) => (
|
||||||
<SubjectOverviewCard
|
<SubjectOverviewCard
|
||||||
key={subject.projectId}
|
key={subject.projectId}
|
||||||
|
|||||||
@ -1,20 +1,24 @@
|
|||||||
import {Card, Title} from "@tremor/react";
|
"use client";
|
||||||
|
|
||||||
|
import {Card, DonutChart, Title} from "@tremor/react";
|
||||||
import * as R from "ramda";
|
import * as R from "ramda";
|
||||||
import {Data} from "@/data/fetchData";
|
import {Data} from "@/data/fetchData";
|
||||||
import {Donut, DonutChartData} from "@/components/Donut";
|
|
||||||
|
|
||||||
function useBreakdownData(data: Data): DonutChartData {
|
function useBreakdownData(data: Data): {
|
||||||
|
name: string,
|
||||||
|
value: number,
|
||||||
|
colour: string
|
||||||
|
}[] {
|
||||||
const sorted = R.sortBy(R.prop('projectId'), data.timeEntries);
|
const sorted = R.sortBy(R.prop('projectId'), data.timeEntries);
|
||||||
const grouped = R.groupWith(R.eqBy(R.prop('projectId')), sorted);
|
const grouped = R.groupWith(R.eqBy(R.prop('projectId')), sorted);
|
||||||
|
|
||||||
return grouped
|
return grouped
|
||||||
.map((entries) => {
|
.map((entries) => {
|
||||||
const project = data.projects.find(p => p.projectId === entries[0].projectId)!;
|
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 ({
|
return ({
|
||||||
label: project.name,
|
name: project.name,
|
||||||
value: (totalDuration / (60 * 60)),
|
value: (entries).map((entry) => entry.duration).reduce((a, b) => a + b, 0),
|
||||||
colour: project.color,
|
colour: project.color,
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
@ -26,15 +30,20 @@ export function SubjectComparisonCard({
|
|||||||
data: Data,
|
data: Data,
|
||||||
}) {
|
}) {
|
||||||
const breakdownData = useBreakdownData(data);
|
const breakdownData = useBreakdownData(data);
|
||||||
|
const colours = breakdownData.map((entry) => entry.colour);
|
||||||
|
|
||||||
|
const valueFormatter = (number: number) => `${(number / (60 * 60)).toFixed(2)} hours`;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className="col">
|
<Card className="col">
|
||||||
<Title>Relative Breakdown</Title>
|
<Title>Relative Breakdown</Title>
|
||||||
<Donut
|
<DonutChart
|
||||||
|
className="mt-6"
|
||||||
data={breakdownData ?? []}
|
data={breakdownData ?? []}
|
||||||
formatter={v => v.toFixed(2)}
|
category="value"
|
||||||
centerLabel={"hours"}
|
index="name"
|
||||||
hoverSuffix={"h"}
|
valueFormatter={valueFormatter}
|
||||||
|
colors={colours}
|
||||||
/>
|
/>
|
||||||
</Card>
|
</Card>
|
||||||
)
|
)
|
||||||
|
|||||||
@ -1,19 +0,0 @@
|
|||||||
.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