From e0f2f89a56e1cc9d623d2d1038434502e7c5a2e5 Mon Sep 17 00:00:00 2001 From: Simon Laux Date: Thu, 19 Oct 2023 16:01:56 +0200 Subject: [PATCH 1/3] draft: replace chartjs with a smaller package for the heatmap. --- package.json | 1 + public/DependencyLicenses.txt | 3 + src/pages/StatisticsPage.tsx | 272 +++++++++++----------------------- yarn.lock | 5 + 4 files changed, 98 insertions(+), 183 deletions(-) diff --git a/package.json b/package.json index 9723783..90e7adf 100644 --- a/package.json +++ b/package.json @@ -31,6 +31,7 @@ "prettier-plugin-tailwindcss": "^0.2.3", "react": "^18.2.0", "react-chartjs-2": "^5.2.0", + "react-contribution-calendar": "^1.3.5", "react-dom": "^18.2.0", "react-hook-form": "^7.45.0", "react-router-dom": "^6.17.0", diff --git a/public/DependencyLicenses.txt b/public/DependencyLicenses.txt index 54411de..7e8df3f 100644 --- a/public/DependencyLicenses.txt +++ b/public/DependencyLicenses.txt @@ -793,6 +793,9 @@ │ │ ├─ URL: https://github.com/reactchartjs/react-chartjs-2.git │ │ ├─ VendorName: Jeremy Ayerst │ │ └─ VendorUrl: https://github.com/reactchartjs/react-chartjs-2 +│ ├─ react-contribution-calendar@1.3.5 +│ │ ├─ URL: git+https://github.com/SeiwonPark/react-contribution-calendar.git +│ │ └─ VendorUrl: https://github.com/SeiwonPark/react-contribution-calendar#readme │ ├─ react-dom@18.2.0 │ │ ├─ URL: https://github.com/facebook/react.git │ │ └─ VendorUrl: https://reactjs.org/ diff --git a/src/pages/StatisticsPage.tsx b/src/pages/StatisticsPage.tsx index e616429..81de117 100644 --- a/src/pages/StatisticsPage.tsx +++ b/src/pages/StatisticsPage.tsx @@ -1,20 +1,4 @@ -import { Chart, ChartProps } from "react-chartjs-2"; -import { - Chart as ChartJS, - Title, - CategoryScale, - TimeScale, - Tooltip, -} from "chart.js"; -import "chartjs-adapter-luxon"; - -ChartJS.register(Title, CategoryScale, TimeScale, Tooltip); - -import { MatrixController, MatrixElement } from "chartjs-chart-matrix"; - -ChartJS.register(MatrixController, MatrixElement); - -import { useEffect, useMemo, useState } from "react"; +import { useEffect, useRef, useState } from "react"; import { DateTime, Duration } from "luxon"; import { useNavigate } from "react-router-dom"; import { useStore } from "../store"; @@ -26,6 +10,7 @@ import { WeekView } from "../components/StatsWeekView"; import { QuickStats } from "../components/TrackPageStats"; import { MonthView } from "../components/StatsMonthViewDays"; import { TaskDistributionPie } from "../components/StatsTaskDistribution"; +import { ContributionCalendar } from "react-contribution-calendar"; export function StatisticsPage() { const now = DateTime.now(); @@ -139,7 +124,7 @@ export function StatisticsPage() {
-
Current Year
+
Heatmap
Time spend by Task
@@ -178,34 +163,38 @@ export function StatisticsPage() { ); } +const maxFeasableMinutesPerDay = 60 * 8; +// const maxPossibleMinutesPerDay = 60 * 24; + +const day_size = 12; +const free_space = 70; // space for other stuff + function ActivityMap() { - type dataType = { - x: string; - y: string; - d: string; - v: number; + type dataType2 = { + [date: string]: { + level: number; + data: { + // this is not a lib feature yet + custom_tooltip: string; + }; + }; }; - const [data, setData] = useState(null); + const [data2, setData2] = useState(null); const entries = useStore((store) => store.getTrackedEntries()); - const [timeSpan] = useState([ - DateTime.now() - .minus(Duration.fromObject({ year: 1 })) - .toMillis(), - DateTime.now().endOf("day").toMillis(), + const [timeSpan, setTimeSpan] = useState([ + DateTime.now().minus(Duration.fromObject({ year: 1 })), + DateTime.now().endOf("day"), ]); - console.log(entries.length); useEffect(() => { - console.log(entries.length, timeSpan[0], timeSpan[1]); - // idea for later: move work into webworker? - setData(null); + setData2(null); - const dataEntries: dataType[] = []; - let working_day = DateTime.fromMillis(timeSpan[0]); - const end = DateTime.fromMillis(timeSpan[1]); + const data2Entries: dataType2[] = []; + let working_day = timeSpan[0]; + const end = timeSpan[1]; // this is for improving performance a little bit const entries_in_span = getEntriesTouchingTimeframe( entries, @@ -229,167 +218,84 @@ function ActivityMap() { .map((e) => e.duration || 0) .reduce((previous, current) => previous + current, 0); - dataEntries.push({ - x: startOfDay.toISODate() || "", - y: String(startOfDay.weekday), - v: Math.floor(timeSpentThatDay / 60000), // convert to minutes per day - d: startOfDay.toISODate() || "", + const minutes = Math.floor(timeSpentThatDay / 60000); // convert to minutes per day + + const level = Math.min( + Math.max(Math.floor((minutes / maxFeasableMinutesPerDay) * 4), 0), + 4, + ); + + data2Entries.push({ + [startOfDay.toFormat("yyyy-MM-dd")]: { + level, + data: { + custom_tooltip: + startOfDay.toFormat("yyyy-MM-dd") + + "\n" + + Duration.fromObject({ minutes }) + .shiftTo("hours", "minutes") + .toHuman(), + }, + }, }); working_day = working_day.plus({ days: 1 }); } - console.log(dataEntries); + // console.log(dataEntries); - setData(dataEntries); + setData2(data2Entries); }, [`${entries.length}`, timeSpan[0], timeSpan[1]]); - const { options, chartData } = useMemo(() => { - const scales: any = { - y: { - type: "time", - offset: true, - time: { - unit: "day", - round: "day", - isoWeekday: 1, - parser: "E", - displayFormats: { - day: "EEE", - }, - }, - reverse: true, - position: "right", - ticks: { - maxRotation: 0, - autoSkip: true, - padding: 1, - font: { - size: 9, - }, - }, - grid: { - display: false, - drawBorder: false, - tickLength: 0, - }, - }, - x: { - type: "time", - position: "bottom", - offset: true, - time: { - unit: "week", - round: "week", - isoWeekday: 1, - displayFormats: { - week: "MMM dd", - }, - }, - ticks: { - maxRotation: 0, - autoSkip: true, - font: { - size: 9, - }, - }, - grid: { - display: false, - drawBorder: false, - tickLength: 0, - }, - }, - }; - const options: ChartProps< - "matrix", - { x: string; y: string; d: string; v: number }[], - unknown - >["options"] = { - responsive: true, - aspectRatio: 5, - plugins: { - tooltip: { - displayColors: false, - callbacks: { - title(items) { - const context = items[0]; - if (context) { - return ( - context.dataset.data[context.dataIndex] as any as dataType - ).d; - } - }, - label(context) { - const { v } = context.dataset.data[ - context.dataIndex - ] as any as dataType; - return Duration.fromObject({ minutes: v }) - .shiftTo("hours", "minutes") - .toHuman(); - }, - }, - }, - }, - scales: scales, - layout: { - padding: { - top: 10, - }, - }, - }; + const container = useRef(null); + const [availableWidth, setAvailableWidth] = useState(null); - const maxFeasableMinutesPerDay = 60 * 8; - // const maxPossibleMinutesPerDay = 60 * 24; + useEffect(() => { + const update = () => { + if (container.current) { + console.log("hi"); + // TODO on reszie does not work yet - const chartData: ChartProps< - "matrix", - { x: string; y: string; d: string; v: number }[], - unknown - >["data"] = { - datasets: [ - { - data: data || [], - backgroundColor(c) { - const minutes = (c.dataset.data[c.dataIndex] as any as dataType).v; - const alpha = minutes / maxFeasableMinutesPerDay; - let green = 255; - if (alpha > 1) { - green = 255 - 255 * (alpha - 1); - } - return `rgba(0,${green},200, ${alpha})`; - }, - borderColor(c) { - const minutes = (c.dataset.data[c.dataIndex] as any as dataType).v; - const alpha = minutes / maxFeasableMinutesPerDay; - let green = 255; - if (alpha > 1) { - green = 255 - 255 * (alpha - 1); - } - return `rgba(0,${green},200, ${alpha})`; - }, - borderWidth: 1, - hoverBackgroundColor: "lightblue", - hoverBorderColor: "blue", - width(c) { - const a = c.chart.chartArea || {}; - return (a.right - a.left) / 53 - 1; - }, - height(c) { - const a = c.chart.chartArea || {}; - return (a.bottom - a.top) / 7 - 1; - }, - }, - ], + const availableWidth = container.current.getBoundingClientRect().width; + const weeks = Math.floor( + Math.max((availableWidth || 0) - free_space, 0) / day_size, + ); + setTimeSpan([DateTime.now().minus({ weeks }), DateTime.now()]); + setAvailableWidth(availableWidth); + } }; + if (container.current) { + update(); + window.addEventListener("resize", update); + return window.removeEventListener("resize", update); + } + }, [container.current]); - return { options, chartData }; - }, [data]); + const start = timeSpan[0].toFormat("yyyy-MM-dd"); + const end = timeSpan[1].toFormat("yyyy-MM-dd"); return (
- {data === null &&
Loading Data...
} - {data !== null && ( - - )} + {data2 === null &&
Loading Data...
} +
+ {availableWidth && data2 !== null && ( + console.log(data)} + scroll={false} + style={{}} + /> + )} +
); } diff --git a/yarn.lock b/yarn.lock index 660f4e4..c32326b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1623,6 +1623,11 @@ react-chartjs-2@^5.2.0: resolved "https://registry.npmjs.org/react-chartjs-2/-/react-chartjs-2-5.2.0.tgz" integrity sha512-98iN5aguJyVSxp5U3CblRLH67J8gkfyGNbiK3c+l1QI/G4irHMPQw44aEPmjVag+YKTyQ260NcF82GTQ3bdscA== +react-contribution-calendar@^1.3.5: + version "1.3.5" + resolved "https://registry.yarnpkg.com/react-contribution-calendar/-/react-contribution-calendar-1.3.5.tgz#e6f45069d422ca014cef970ac652ee0191a5c998" + integrity sha512-aSsjEuLV9hxDcZoIOEnacTkBHUzGihbIJ5Sd4JvwUCqzlF1sr+dc7lDy0ZX5+jThW89DLKFtWIpxCB6HeBXOcw== + react-dom@^18.2.0: version "18.2.0" resolved "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz" From 0347af614a1aecbcc09a2558c36cd081973331c7 Mon Sep 17 00:00:00 2001 From: Simon Laux Date: Thu, 26 Oct 2023 17:36:26 +0200 Subject: [PATCH 2/3] update dependency --- package.json | 2 +- public/DependencyLicenses.txt | 2 +- yarn.lock | 8 ++++---- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index 90e7adf..61bbf63 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,7 @@ "prettier-plugin-tailwindcss": "^0.2.3", "react": "^18.2.0", "react-chartjs-2": "^5.2.0", - "react-contribution-calendar": "^1.3.5", + "react-contribution-calendar": "^1.4.0", "react-dom": "^18.2.0", "react-hook-form": "^7.45.0", "react-router-dom": "^6.17.0", diff --git a/public/DependencyLicenses.txt b/public/DependencyLicenses.txt index 7e8df3f..4ea9152 100644 --- a/public/DependencyLicenses.txt +++ b/public/DependencyLicenses.txt @@ -793,7 +793,7 @@ │ │ ├─ URL: https://github.com/reactchartjs/react-chartjs-2.git │ │ ├─ VendorName: Jeremy Ayerst │ │ └─ VendorUrl: https://github.com/reactchartjs/react-chartjs-2 -│ ├─ react-contribution-calendar@1.3.5 +│ ├─ react-contribution-calendar@1.4.0 │ │ ├─ URL: git+https://github.com/SeiwonPark/react-contribution-calendar.git │ │ └─ VendorUrl: https://github.com/SeiwonPark/react-contribution-calendar#readme │ ├─ react-dom@18.2.0 diff --git a/yarn.lock b/yarn.lock index c32326b..19d0ab0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1623,10 +1623,10 @@ react-chartjs-2@^5.2.0: resolved "https://registry.npmjs.org/react-chartjs-2/-/react-chartjs-2-5.2.0.tgz" integrity sha512-98iN5aguJyVSxp5U3CblRLH67J8gkfyGNbiK3c+l1QI/G4irHMPQw44aEPmjVag+YKTyQ260NcF82GTQ3bdscA== -react-contribution-calendar@^1.3.5: - version "1.3.5" - resolved "https://registry.yarnpkg.com/react-contribution-calendar/-/react-contribution-calendar-1.3.5.tgz#e6f45069d422ca014cef970ac652ee0191a5c998" - integrity sha512-aSsjEuLV9hxDcZoIOEnacTkBHUzGihbIJ5Sd4JvwUCqzlF1sr+dc7lDy0ZX5+jThW89DLKFtWIpxCB6HeBXOcw== +react-contribution-calendar@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/react-contribution-calendar/-/react-contribution-calendar-1.4.0.tgz#fda910c14cac6e005ea686cf3757b7af0e271e88" + integrity sha512-LjkLAMejUMLzc0CIGn9l3Iq5nPfkOpzJ9tRyGkul9LcPA4fPEoZNuZSwVUkZD/JQBCBfa/0MF1iuQ9DfgEbjFQ== react-dom@^18.2.0: version "18.2.0" From e319c6276a880b3b3fad35bd74f54aae6c7607ad Mon Sep 17 00:00:00 2001 From: Simon Laux Date: Thu, 26 Oct 2023 18:27:26 +0200 Subject: [PATCH 3/3] fix taking up available width --- src/App.css | 6 +++++ src/pages/StatisticsPage.tsx | 48 +++++++++++++++++++----------------- src/util.ts | 23 +++++++++++++++++ 3 files changed, 55 insertions(+), 22 deletions(-) diff --git a/src/App.css b/src/App.css index 0713abd..a00b7dc 100644 --- a/src/App.css +++ b/src/App.css @@ -45,3 +45,9 @@ html { --color: rgb(0, 69, 198); } } + +/* workaround because library has annoying breakpoints that hide parts of the graph */ +.contribution-calendar > .container { + width: 100% !important; + max-width: 100% !important; +} diff --git a/src/pages/StatisticsPage.tsx b/src/pages/StatisticsPage.tsx index 81de117..147a909 100644 --- a/src/pages/StatisticsPage.tsx +++ b/src/pages/StatisticsPage.tsx @@ -11,6 +11,7 @@ import { QuickStats } from "../components/TrackPageStats"; import { MonthView } from "../components/StatsMonthViewDays"; import { TaskDistributionPie } from "../components/StatsTaskDistribution"; import { ContributionCalendar } from "react-contribution-calendar"; +import { useIsDarkTheme } from "../util"; export function StatisticsPage() { const now = DateTime.now(); @@ -170,6 +171,7 @@ const day_size = 12; const free_space = 70; // space for other stuff function ActivityMap() { + const isDarkTheme = useIsDarkTheme(); type dataType2 = { [date: string]: { level: number; @@ -252,10 +254,8 @@ function ActivityMap() { useEffect(() => { const update = () => { if (container.current) { - console.log("hi"); - // TODO on reszie does not work yet - - const availableWidth = container.current.getBoundingClientRect().width; + const availableWidth = + container.current.getBoundingClientRect().width - 30; const weeks = Math.floor( Math.max((availableWidth || 0) - free_space, 0) / day_size, ); @@ -265,35 +265,39 @@ function ActivityMap() { }; if (container.current) { update(); - window.addEventListener("resize", update); - return window.removeEventListener("resize", update); } + window.addEventListener("resize", update); + return () => window.removeEventListener("resize", update); }, [container.current]); const start = timeSpan[0].toFormat("yyyy-MM-dd"); const end = timeSpan[1].toFormat("yyyy-MM-dd"); + const theme = isDarkTheme ? "dark_winter" : "winter"; + return (
{data2 === null &&
Loading Data...
}
{availableWidth && data2 !== null && ( - console.log(data)} - scroll={false} - style={{}} - /> +
+ console.log(data)} + scroll={false} + style={{}} + /> +
)}
diff --git a/src/util.ts b/src/util.ts index a43af7c..e27b8dd 100644 --- a/src/util.ts +++ b/src/util.ts @@ -1,3 +1,5 @@ +import { useEffect, useState } from "react"; + // taken from https://blog.logrocket.com/react-suspense-data-fetching/#how-to-use-suspense /** convert promise to suspend */ export function wrapPromise(promise: Promise) { @@ -39,3 +41,24 @@ export function arrayMin(arr: number[]) { } return min; } + +export function useIsDarkTheme() { + const [isDarkTheme, setIsDarkTheme] = useState( + window.matchMedia && + window.matchMedia("(prefers-color-scheme: dark)").matches, + ); + useEffect(() => { + if (!window.matchMedia) { + return; + } + const mm = window.matchMedia("(prefers-color-scheme: dark)"); + const callback = (event: MediaQueryListEvent) => { + setIsDarkTheme(event.matches); + }; + + mm.addEventListener("change", callback); + return () => mm.removeEventListener("change", callback); + }); + + return isDarkTheme; +}