├── .npmrc ├── src ├── settings │ └── language.settings.ts ├── schemas │ ├── colorsList.schema.ts │ ├── palettes.schema.ts │ ├── common.ts │ ├── intensityConfig.schema.ts │ ├── colorScheme.schema.ts │ ├── insight.schema.ts │ ├── entry.schema.ts │ ├── ui.schema.ts │ ├── trackerData.schema.ts │ ├── __tests__ │ │ └── palettes.schema.test.ts │ └── validation.ts ├── global.d.ts ├── styles │ ├── heatmap-documentation.scss │ ├── heatmap-tracker-footer.scss │ ├── heatmap-tracker-months.scss │ ├── heatmap-breaking-changes.scss │ ├── heatmap-legend.scss │ ├── heatmap-tracker-box.scss │ ├── heatmap-statistics.scss │ ├── heatmap-tracker-days.scss │ ├── heatmap-tracker-week-nums.scss │ ├── heatmap-tracker-boxes.scss │ └── heatmap-tracker-header.scss ├── utils │ ├── notify.ts │ ├── tabs.tsx │ ├── colors.ts │ ├── __tests__ │ │ ├── notify.spec.ts │ │ └── colors.spec.ts │ ├── date.ts │ └── statistics.ts ├── localization │ ├── languages.json │ ├── i18n.ts │ └── locales │ │ ├── zh.json │ │ ├── en.json │ │ └── hi.json ├── context │ └── app │ │ └── app.context.tsx ├── __mocks__ │ ├── settings.mock.ts │ └── obsidian.ts ├── components │ ├── icons │ │ ├── MenuIcon.tsx │ │ ├── ReportBugIcon.tsx │ │ ├── HeatmapIcon.tsx │ │ ├── ShieldXIcon.tsx │ │ ├── StatisticsIcon.tsx │ │ ├── HandCoinsIcon.tsx │ │ ├── DocumentationIcon.tsx │ │ └── LegendIcon.tsx │ ├── TipOfTheDay │ │ └── TipOfTheDay.tsx │ ├── HeatmapBoxesList │ │ └── HeatmapBoxesList.tsx │ ├── HeatmapTabs │ │ └── HeatmapTabs.tsx │ ├── HeatmapMonthsList │ │ └── HeatmapMonthsList.tsx │ ├── HeatmapTab │ │ └── HeatmapTab.tsx │ ├── HeatmapFooter │ │ └── HeatmapFooter.tsx │ ├── HeatmapWeekDays │ │ └── HeatmapWeekDays.tsx │ ├── HeatmapWeekNums │ │ ├── HeatmapWeekNums.tsx │ │ └── __tests__ │ │ │ └── HeatmapWeekNums.test.tsx │ ├── HeatmapBox │ │ └── HeatmapBox.tsx │ └── HeatmapHeader │ │ └── HeatmapHeader.tsx ├── constants │ ├── defaultTrackerData.ts │ └── defaultSettings.ts ├── views │ ├── HeatmapTrackerView │ │ └── HeatmapTrackerView.tsx │ ├── LegendView │ │ └── LegendView.tsx │ └── DocumentationView │ │ └── DocumentationView.tsx ├── App.tsx ├── types.ts ├── render.tsx └── styles.scss ├── EXAMPLE_VAULT ├── .obsidian │ ├── hotkeys.json │ ├── page-preview.json │ ├── plugins │ │ ├── heatmap-tracker │ │ │ └── .hotreload │ │ └── hot-reload │ │ │ └── manifest.json │ ├── community-plugins.json │ ├── appearance.json │ ├── app.json │ ├── types.json │ ├── snippets │ │ └── properties.css │ ├── core-plugins-migration.json │ ├── core-plugins.json │ └── workspace-mobile.json ├── daily notes │ ├── 2024-03-14.md │ ├── 2023-05-06.md │ ├── 2023-05-07.md │ ├── 2023-05-08.md │ ├── 2023-05-09.md │ ├── 2023-05-10.md │ ├── 2023-05-11.md │ ├── 2023-05-12.md │ ├── 2023-05-13.md │ ├── 2023-05-14.md │ ├── 2023-05-15.md │ ├── 2023-05-16.md │ ├── 2023-05-17.md │ ├── 2023-05-18.md │ ├── 2023-05-19.md │ ├── 2023-05-20.md │ ├── 2024-05-01.md │ ├── 2024-05-02.md │ ├── 2024-05-03.md │ ├── 2024-05-04.md │ ├── 2024-05-05.md │ ├── 2024-05-06.md │ ├── 2024-05-07.md │ ├── 2024-05-08.md │ ├── 2024-05-09.md │ ├── 2024-05-10.md │ ├── 2024-05-11.md │ ├── 2024-05-12.md │ ├── 2024-05-13.md │ ├── 2024-05-14.md │ ├── 2024-05-15.md │ ├── 2024-05-16.md │ ├── 2024-05-17.md │ ├── 2024-05-18.md │ ├── 2024-05-19.md │ ├── 2024-05-20.md │ ├── 2025-05-01.md │ ├── 2025-05-02.md │ ├── 2025-05-03.md │ ├── 2025-05-04.md │ ├── 2025-05-05.md │ ├── 2025-05-06.md │ ├── 2025-05-07.md │ ├── 2025-05-08.md │ ├── 2025-05-09.md │ ├── 2025-05-10.md │ ├── 2025-05-11.md │ ├── 2025-05-12.md │ ├── 2025-05-13.md │ ├── 2025-05-14.md │ ├── 2025-05-15.md │ ├── 2025-05-16.md │ ├── 2025-05-17.md │ ├── 2025-05-18.md │ ├── 2025-05-19.md │ ├── 2025-05-20.md │ ├── 2023-05-02.md │ ├── 2023-05-03.md │ ├── 2023-05-04.md │ ├── 2023-05-05.md │ └── 2023-05-01.md ├── README.md ├── Documentation with Examples │ ├── 4. Insights │ │ ├── 3. Total Pages Read.md │ │ ├── 6. Total Hours Slept.md │ │ ├── 7. Average Sleep Per Night.md │ │ ├── 4. Most Pages Read in a Single Day.md │ │ ├── 5. Average Headache Intensity.md │ │ ├── 1. Days Achieved Step Goal (8,000 steps).md │ │ ├── 2. Longest Streak of Meeting Step Goal.md │ │ └── 8. Activity Intensity by Day.md │ ├── 3. trackerData parameters │ │ ├── 8. basePath.md │ │ ├── 7. disableFileCreation.md │ │ ├── 9. intensityConfig.md │ │ ├── 10. colorScheme.md │ │ ├── 1. heatmapTitle.md │ │ ├── 11. entries.md │ │ ├── 5. showCurrentDayBorder.md │ │ ├── 3. year.md │ │ ├── 4. separateMonths.md │ │ ├── 2. heatmapSubtitle (Description).md │ │ └── 6. insights.md │ ├── 1. How to start? │ │ ├── 0. I installed plugin, how to see chart?.md │ │ └── 1. How to add a heatmap to your obsidian page?.md │ ├── 2. Features │ │ ├── Display statistics separately.md │ │ ├── Display legend separately.md │ │ └── Display Legend under Heatmap.md │ └── index.md ├── Showcase │ ├── Showcase.md │ ├── 5. Task (Project) tracker.md │ └── 4. Activities Tracking in one file v2.md ├── test.md ├── Github Issues │ ├── 2. Add Week Numbers Under the Heatmap.md │ ├── 62. Does not change color when value is 0.md │ └── 56. Ability to disable elements in header.md ├── Examples │ ├── Examples.md │ ├── Task Tracking Example from Reddit │ │ ├── notes │ │ │ └── 2025-01-02.md │ │ └── Task Tracking example from Reddit.md │ ├── Habit Tracker │ │ └── Exercise.md │ ├── Mood Tracker │ │ └── Daily Mood.md │ └── Project Progress │ │ └── Project Alpha.md ├── Activities Tracking │ ├── Gym.md │ └── Reading.md └── Start Here.md ├── .eslintignore ├── public ├── readme-cover.png ├── heatmap-how-to.gif ├── two-mac-mockup.png ├── mac-mockup-dark.png └── tracker-overview.png ├── .github ├── ISSUE_TEMPLATE │ ├── custom.md │ ├── feature_request.md │ └── bug_report.md └── workflows │ └── release.yml ├── manifest.json ├── .gitattributes ├── .eslintrc ├── tsconfig.json ├── jest.config.js ├── version-bump.mjs ├── docs └── add-new-language.md ├── .gitignore ├── SECURITY.md ├── update-version.sh ├── ROADMAP.md ├── generate_examples.sh ├── versions.json ├── esbuild.config.mjs └── package.json /.npmrc: -------------------------------------------------------------------------------- 1 | tag-version-prefix="" -------------------------------------------------------------------------------- /src/settings/language.settings.ts: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /EXAMPLE_VAULT/.obsidian/hotkeys.json: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | npm node_modules 2 | build -------------------------------------------------------------------------------- /EXAMPLE_VAULT/.obsidian/page-preview.json: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /EXAMPLE_VAULT/daily notes/2024-03-14.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /EXAMPLE_VAULT/.obsidian/plugins/heatmap-tracker/.hotreload: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/readme-cover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mokkiebear/heatmap-tracker/HEAD/public/readme-cover.png -------------------------------------------------------------------------------- /public/heatmap-how-to.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mokkiebear/heatmap-tracker/HEAD/public/heatmap-how-to.gif -------------------------------------------------------------------------------- /public/two-mac-mockup.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mokkiebear/heatmap-tracker/HEAD/public/two-mac-mockup.png -------------------------------------------------------------------------------- /public/mac-mockup-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mokkiebear/heatmap-tracker/HEAD/public/mac-mockup-dark.png -------------------------------------------------------------------------------- /public/tracker-overview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mokkiebear/heatmap-tracker/HEAD/public/tracker-overview.png -------------------------------------------------------------------------------- /src/schemas/colorsList.schema.ts: -------------------------------------------------------------------------------- 1 | import z from "zod"; 2 | 3 | export const ColorsListSchema = z.array(z.string()); 4 | -------------------------------------------------------------------------------- /src/global.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.module.scss' { 2 | const classes: { [key: string]: string }; 3 | export default classes; 4 | } 5 | -------------------------------------------------------------------------------- /EXAMPLE_VAULT/.obsidian/community-plugins.json: -------------------------------------------------------------------------------- 1 | [ 2 | "dataview", 3 | "obsidian-metatable", 4 | "hot-reload", 5 | "heatmap-tracker" 6 | ] -------------------------------------------------------------------------------- /src/styles/heatmap-documentation.scss: -------------------------------------------------------------------------------- 1 | /* DOCUMENTATION VIEW */ 2 | .documentation-view__container { 3 | font-size: 0.8em; 4 | } 5 | /* END DOCUMENTATION VIEW */ -------------------------------------------------------------------------------- /src/utils/notify.ts: -------------------------------------------------------------------------------- 1 | import { Notice } from "obsidian"; 2 | 3 | export function notify(message: string, duration: number = 3000) { 4 | return new Notice(message, duration); 5 | } -------------------------------------------------------------------------------- /src/schemas/palettes.schema.ts: -------------------------------------------------------------------------------- 1 | import z from "zod"; 2 | import { ColorsListSchema } from "./colorsList.schema"; 3 | 4 | export const PalettesSchema = z.record(z.string(), ColorsListSchema); 5 | -------------------------------------------------------------------------------- /EXAMPLE_VAULT/.obsidian/appearance.json: -------------------------------------------------------------------------------- 1 | { 2 | "baseFontSize": 16, 3 | "theme": "obsidian", 4 | "translucency": false, 5 | "accentColor": "", 6 | "enabledCssSnippets": [], 7 | "cssTheme": "" 8 | } -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/custom.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Custom issue template 3 | about: Describe this issue template's purpose here. 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | 11 | -------------------------------------------------------------------------------- /src/schemas/common.ts: -------------------------------------------------------------------------------- 1 | import z from "zod"; 2 | 3 | export const NumberLike = z.preprocess((val: number | undefined) => { 4 | const num = Number(val); 5 | return Number.isNaN(num) ? val : num; 6 | }, z.number()); 7 | -------------------------------------------------------------------------------- /EXAMPLE_VAULT/.obsidian/app.json: -------------------------------------------------------------------------------- 1 | { 2 | "legacyEditor": false, 3 | "livePreview": true, 4 | "useTab": false, 5 | "promptDelete": false, 6 | "showFrontmatter": true, 7 | "showUnsupportedFiles": true, 8 | "alwaysUpdateLinks": true 9 | } -------------------------------------------------------------------------------- /EXAMPLE_VAULT/.obsidian/types.json: -------------------------------------------------------------------------------- 1 | { 2 | "types": { 3 | "aliases": "aliases", 4 | "cssclasses": "multitext", 5 | "tags": "tags", 6 | "tracking": "checkbox", 7 | "status": "multitext", 8 | "issue_62": "number" 9 | } 10 | } -------------------------------------------------------------------------------- /EXAMPLE_VAULT/.obsidian/snippets/properties.css: -------------------------------------------------------------------------------- 1 | .metadata-property-value input[type="number"]::-webkit-inner-spin-button, 2 | .metadata-property-value input[type="number"]::-webkit-outer-spin-button { 3 | -webkit-appearance: auto; 4 | display: inline; 5 | } -------------------------------------------------------------------------------- /src/styles/heatmap-tracker-footer.scss: -------------------------------------------------------------------------------- 1 | .heatmap-tracker-footer { 2 | padding: 0.2em 0; 3 | } 4 | 5 | .heatmap-tracker-footer__important { 6 | font-size: 0.7em; 7 | display: flex; 8 | align-items: center; 9 | column-gap: 4px; 10 | justify-content: center; 11 | } -------------------------------------------------------------------------------- /EXAMPLE_VAULT/README.md: -------------------------------------------------------------------------------- 1 | This example vault is for testing and learning the Heatmap Tracker plugin. 2 | 3 | **To get started, open the [[Start Here]] note in Obsidian.** 4 | 5 | > [!IMPORTANT] 6 | > Remember to install **"Dataview"** and **"Heatmap Tracker"** in *settings -> community plugins* -------------------------------------------------------------------------------- /src/schemas/intensityConfig.schema.ts: -------------------------------------------------------------------------------- 1 | import z from "zod"; 2 | 3 | export const IntensityConfigSchema = z.strictObject({ 4 | scaleStart: z.number().or(z.undefined()), 5 | scaleEnd: z.number().or(z.undefined()), 6 | defaultIntensity: z.number(), 7 | showOutOfRange: z.boolean(), 8 | }); 9 | -------------------------------------------------------------------------------- /EXAMPLE_VAULT/.obsidian/plugins/hot-reload/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "hot-reload", 3 | "name": "Hot Reload", 4 | "version": "0.1.9", 5 | "minAppVersion": "0.11.13", 6 | "description": "Automatically reload in-development plugins when their files are changed", 7 | "isDesktopOnly": true 8 | } 9 | -------------------------------------------------------------------------------- /src/localization/languages.json: -------------------------------------------------------------------------------- 1 | { 2 | "en": "English", 3 | "ru": "Русский", 4 | "de": "Deutsch", 5 | "zh": "中文 (Mandarin Chinese)", 6 | "hi": "हिन्दी (Hindi)", 7 | "es": "Español (Spanish)", 8 | "fr": "Français (French)", 9 | "pt": "Portuguese", 10 | "pl": "Polski" 11 | } 12 | -------------------------------------------------------------------------------- /EXAMPLE_VAULT/Documentation with Examples/4. Insights/3. Total Pages Read.md: -------------------------------------------------------------------------------- 1 | ``` 2 | { 3 | name: "Total Pages Read", 4 | calculate: ({ yearEntries }) => { 5 | const totalPages = yearEntries.reduce((sum, entry) => sum + (entry.value || 0), 0); 6 | return totalPages.toString(); 7 | }, 8 | } 9 | ``` -------------------------------------------------------------------------------- /EXAMPLE_VAULT/Documentation with Examples/4. Insights/6. Total Hours Slept.md: -------------------------------------------------------------------------------- 1 | ``` 2 | { 3 | name: "Total Hours Slept", 4 | calculate: ({ yearEntries }) => { 5 | const totalHours = yearEntries.reduce((sum, entry) => sum + (entry.value || 0), 0); 6 | return totalHours.toString(); 7 | }, 8 | } 9 | ``` -------------------------------------------------------------------------------- /src/schemas/colorScheme.schema.ts: -------------------------------------------------------------------------------- 1 | import z from "zod"; 2 | import { ColorsListSchema } from "./colorsList.schema"; 3 | 4 | export const ColorSchemeSchema = z 5 | .object({ 6 | paletteName: z.string().optional(), 7 | customColors: ColorsListSchema.optional(), 8 | }) 9 | .strict(); 10 | -------------------------------------------------------------------------------- /src/styles/heatmap-tracker-months.scss: -------------------------------------------------------------------------------- 1 | /* Months */ 2 | .heatmap-tracker-months { 3 | display: grid; 4 | grid-template-columns: repeat(12, 1fr); 5 | grid-column: 1; 6 | margin: 2px 0 4px; 7 | grid-gap: 0.3em; 8 | justify-items: center; 9 | line-height: var(--heatmap-tracker-months-height); 10 | } -------------------------------------------------------------------------------- /EXAMPLE_VAULT/Documentation with Examples/4. Insights/7. Average Sleep Per Night.md: -------------------------------------------------------------------------------- 1 | Use it in [[Hours slept example]] 2 | 3 | ``` 4 | { 5 | name: "Average Sleep Per Night", 6 | calculate: ({ yearEntries }) => { 7 | const totalHours = yearEntries.reduce((sum, entry) => sum + (entry.value || 0), 0); 8 | return (totalHours / yearEntries.length).toFixed(2); 9 | }, 10 | } 11 | ``` -------------------------------------------------------------------------------- /EXAMPLE_VAULT/Documentation with Examples/4. Insights/4. Most Pages Read in a Single Day.md: -------------------------------------------------------------------------------- 1 | ``` 2 | { 3 | name: "Most Pages Read in a Single Day", 4 | calculate: ({ yearEntries }) => { 5 | const maxEntry = yearEntries.reduce((max, entry) => 6 | (entry.value || 0) > (max.value || 0) ? entry : max 7 | ); 8 | return `${maxEntry.value} pages on ${maxEntry.date}`; 9 | }, 10 | } 11 | ``` -------------------------------------------------------------------------------- /src/schemas/insight.schema.ts: -------------------------------------------------------------------------------- 1 | import z from "zod"; 2 | import { EntrySchema } from "./entry.schema"; 3 | 4 | export const InsightSchema = z.strictObject({ 5 | name: z.string(), 6 | calculate: z.function({ 7 | input: [ 8 | z 9 | .object({ 10 | yearEntries: z.array(EntrySchema.strict()), 11 | }) 12 | .strict(), 13 | ], 14 | output: z.string(), 15 | }), 16 | }); 17 | -------------------------------------------------------------------------------- /src/styles/heatmap-breaking-changes.scss: -------------------------------------------------------------------------------- 1 | .breaking-changes-view__maintenance-border { 2 | background: repeating-linear-gradient( 3 | 45deg, 4 | rgb(151, 190, 90), 5 | rgb(151, 190, 90) 35px, 6 | rgb(255, 232, 197) 35px, 7 | rgb(255, 232, 197) 70px 8 | ); 9 | padding: 8px; 10 | } 11 | 12 | .breaking-changes-view__container { 13 | background-color: var(--background-primary-alt); 14 | padding: 8px; 15 | } -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "heatmap-tracker", 3 | "name": "Heatmap Tracker", 4 | "version": "2.0.0", 5 | "minAppVersion": "0.1.0", 6 | "description": "Visualize your activity and track goals, progress, habits, tasks, exercise, finances, and more—all in a single, interactive heatmap!", 7 | "author": "Maksim Rubanau", 8 | "isDesktopOnly": false, 9 | "fundingUrl": { 10 | "Buy Me a Coffee": "https://www.buymeacoffee.com/mrubanau" 11 | } 12 | } -------------------------------------------------------------------------------- /src/context/app/app.context.tsx: -------------------------------------------------------------------------------- 1 | import { App } from "obsidian"; 2 | import { createContext, useContext } from "react"; 3 | 4 | export const AppContext = createContext(undefined); 5 | 6 | export const useAppContext = (): App => { 7 | const context = useContext(AppContext); 8 | 9 | if (!context) { 10 | throw new Error("useAppContext must be used within an AppContextProvider"); 11 | } 12 | 13 | return context; 14 | }; 15 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Set the default behavior, in case people don't have core.autocrlf set. 2 | * text=auto 3 | 4 | # Explicitly declare text files you want to always be normalized and converted 5 | # to native line endings on checkout. 6 | *.ts text 7 | 8 | # Declare files that will always have CRLF line endings on checkout. 9 | *.sln text eol=crlf 10 | 11 | # Denote all files that are truly binary and should not be modified. 12 | *.png binary 13 | *.jpg binary -------------------------------------------------------------------------------- /src/styles/heatmap-legend.scss: -------------------------------------------------------------------------------- 1 | /* LEGEND VIEW */ 2 | .legend-view { 3 | display: flex; 4 | align-items: center; 5 | justify-content: center; 6 | 7 | table { 8 | border-collapse: collapse; 9 | 10 | th, 11 | td { 12 | padding: 8px; 13 | text-align: center; 14 | font-size: 0.8em; 15 | } 16 | } 17 | 18 | &__color-cell { 19 | display: flex; 20 | align-items: center; 21 | } 22 | } 23 | /* END LEGEND VIEW */ -------------------------------------------------------------------------------- /src/styles/heatmap-tracker-box.scss: -------------------------------------------------------------------------------- 1 | .heatmap-tracker-box { 2 | position: relative; 3 | font-size: 0.75em; 4 | border-radius: 2px; 5 | background-color: #ebedf0; 6 | transition: transform 0.2s, border 0.2s; 7 | aspect-ratio: 1; 8 | 9 | &:hover { 10 | transform: scale(1.4); 11 | border: solid var(--border-width) rgb(61, 61, 61); 12 | cursor: pointer; 13 | } 14 | } 15 | 16 | .heatmap-tracker-box:not(.task-list-item)::before { 17 | content: unset; 18 | } -------------------------------------------------------------------------------- /EXAMPLE_VAULT/Documentation with Examples/4. Insights/5. Average Headache Intensity.md: -------------------------------------------------------------------------------- 1 | Use it in [[3. Headache Tracker in one file]] 2 | 3 | ``` 4 | { 5 | name: "Average Headache Intensity", 6 | calculate: ({ yearEntries }) => { 7 | const headacheEntries = yearEntries.filter((entry) => entry.value > 0); 8 | const totalIntensity = headacheEntries.reduce((sum, entry) => sum + (entry.intensity || 0), 0); 9 | return (totalIntensity / headacheEntries.length).toFixed(2); 10 | }, 11 | } 12 | ``` -------------------------------------------------------------------------------- /EXAMPLE_VAULT/Showcase/Showcase.md: -------------------------------------------------------------------------------- 1 | # Showcase 2 | 3 | These files demonstrate comprehensive use cases and the full power of the Heatmap Tracker plugin. They are great for seeing what's possible! 4 | 5 | - [[1. Heatmap Tracker Overview|Overview]] 6 | - [[2. Activities Tracking in one file|Activities Tracking (Single File)]] 7 | - [[3. Headache Tracker in one file|Headache Tracker]] 8 | - [[4. Activities Tracking in one file v2|Activities Tracking v2]] 9 | - [[5. Task (Project) tracker|Task/Project Tracker]] 10 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "parser": "@typescript-eslint/parser", 4 | "env": { "node": true }, 5 | "plugins": [ 6 | "@typescript-eslint" 7 | ], 8 | "extends": [ 9 | "eslint:recommended", 10 | "plugin:@typescript-eslint/eslint-recommended", 11 | "plugin:@typescript-eslint/recommended" 12 | ], 13 | "parserOptions": { 14 | "sourceType": "module" 15 | }, 16 | "rules": { 17 | "@typescript-eslint/ban-ts-comment": "off", 18 | "semi": ["error", "always"] 19 | } 20 | } -------------------------------------------------------------------------------- /src/__mocks__/settings.mock.ts: -------------------------------------------------------------------------------- 1 | import { TrackerSettings } from "src/types"; 2 | 3 | export const settingsMock: TrackerSettings = { 4 | "palettes": { 5 | "default": ["#c6e48b", "#7bc96f", "#49af5d", "#2e8840", "#196127"] 6 | }, 7 | "weekStartDay": 1, 8 | "weekDisplayMode": "even", 9 | "separateMonths": false, 10 | "language": "en", 11 | "viewTabsVisibility": { 12 | "heatmap-tracker": true, 13 | "heatmap-tracker-statistics": true, 14 | "documentation": true, 15 | "legend": true 16 | }, 17 | "showWeekNums": false 18 | }; 19 | 20 | -------------------------------------------------------------------------------- /EXAMPLE_VAULT/daily notes/2023-05-06.md: -------------------------------------------------------------------------------- 1 | --- 2 | steps: 6215 3 | exercise: 53 minutes 4 | learning: 33 minutes 5 | --- 6 | ## Day No 6 in 2023 7 | Good morning! Today is a beautiful day. 8 | I'm going to learn something new today. 9 | 10 | I learned about the history of the Roman Empire. 11 | 12 | What do you think about the Roman Empire? 13 | 14 | I woke up today at [Woke:: 2023-05-06T06:19] 15 | I went to bed today at [Sleep:: 2023-05-06T21:23] 16 | 17 | #### Task tracking example 18 | - [ ] Task 1 19 | - [ ] Task 2 20 | - [x] Task 3 21 | - [ ] Task 4 22 | - [x] Task 5 23 | 24 | -------------------------------------------------------------------------------- /EXAMPLE_VAULT/daily notes/2023-05-07.md: -------------------------------------------------------------------------------- 1 | --- 2 | steps: 3386 3 | exercise: 10 minutes 4 | learning: 51 minutes 5 | --- 6 | ## Day No 7 in 2023 7 | Good morning! Today is a beautiful day. 8 | I'm going to learn something new today. 9 | 10 | I learned about the history of the Roman Empire. 11 | 12 | What do you think about the Roman Empire? 13 | 14 | I woke up today at [Woke:: 2023-05-07T04:16] 15 | I went to bed today at [Sleep:: 2023-05-07T23:44] 16 | 17 | #### Task tracking example 18 | - [ ] Task 1 19 | - [ ] Task 2 20 | - [x] Task 3 21 | - [ ] Task 4 22 | - [x] Task 5 23 | 24 | -------------------------------------------------------------------------------- /EXAMPLE_VAULT/daily notes/2023-05-08.md: -------------------------------------------------------------------------------- 1 | --- 2 | steps: 7949 3 | exercise: 35 minutes 4 | learning: 24 minutes 5 | --- 6 | ## Day No 8 in 2023 7 | Good morning! Today is a beautiful day. 8 | I'm going to learn something new today. 9 | 10 | I learned about the history of the Roman Empire. 11 | 12 | What do you think about the Roman Empire? 13 | 14 | I woke up today at [Woke:: 2023-05-08T05:08] 15 | I went to bed today at [Sleep:: 2023-05-08T20:32] 16 | 17 | #### Task tracking example 18 | - [ ] Task 1 19 | - [x] Task 2 20 | - [x] Task 3 21 | - [ ] Task 4 22 | - [ ] Task 5 23 | 24 | -------------------------------------------------------------------------------- /EXAMPLE_VAULT/daily notes/2023-05-09.md: -------------------------------------------------------------------------------- 1 | --- 2 | steps: 1855 3 | exercise: 12 minutes 4 | learning: 33 minutes 5 | --- 6 | ## Day No 9 in 2023 7 | Good morning! Today is a beautiful day. 8 | I'm going to learn something new today. 9 | 10 | I learned about the history of the Roman Empire. 11 | 12 | What do you think about the Roman Empire? 13 | 14 | I woke up today at [Woke:: 2023-05-09T07:52] 15 | I went to bed today at [Sleep:: 2023-05-09T23:59] 16 | 17 | #### Task tracking example 18 | - [ ] Task 1 19 | - [x] Task 2 20 | - [ ] Task 3 21 | - [ ] Task 4 22 | - [ ] Task 5 23 | 24 | -------------------------------------------------------------------------------- /EXAMPLE_VAULT/daily notes/2023-05-10.md: -------------------------------------------------------------------------------- 1 | --- 2 | steps: 1154 3 | exercise: 68 minutes 4 | learning: 47 minutes 5 | --- 6 | ## Day No 10 in 2023 7 | Good morning! Today is a beautiful day. 8 | I'm going to learn something new today. 9 | 10 | I learned about the history of the Roman Empire. 11 | 12 | What do you think about the Roman Empire? 13 | 14 | I woke up today at [Woke:: 2023-05-10T08:40] 15 | I went to bed today at [Sleep:: 2023-05-10T21:32] 16 | 17 | #### Task tracking example 18 | - [ ] Task 1 19 | - [ ] Task 2 20 | - [ ] Task 3 21 | - [ ] Task 4 22 | - [ ] Task 5 23 | 24 | -------------------------------------------------------------------------------- /EXAMPLE_VAULT/daily notes/2023-05-11.md: -------------------------------------------------------------------------------- 1 | --- 2 | steps: 1726 3 | exercise: 59 minutes 4 | learning: 13 minutes 5 | --- 6 | ## Day No 11 in 2023 7 | Good morning! Today is a beautiful day. 8 | I'm going to learn something new today. 9 | 10 | I learned about the history of the Roman Empire. 11 | 12 | What do you think about the Roman Empire? 13 | 14 | I woke up today at [Woke:: 2023-05-11T05:19] 15 | I went to bed today at [Sleep:: 2023-05-11T24:39] 16 | 17 | #### Task tracking example 18 | - [x] Task 1 19 | - [ ] Task 2 20 | - [ ] Task 3 21 | - [ ] Task 4 22 | - [x] Task 5 23 | 24 | -------------------------------------------------------------------------------- /EXAMPLE_VAULT/daily notes/2023-05-12.md: -------------------------------------------------------------------------------- 1 | --- 2 | steps: 4903 3 | exercise: 34 minutes 4 | learning: 60 minutes 5 | --- 6 | ## Day No 12 in 2023 7 | Good morning! Today is a beautiful day. 8 | I'm going to learn something new today. 9 | 10 | I learned about the history of the Roman Empire. 11 | 12 | What do you think about the Roman Empire? 13 | 14 | I woke up today at [Woke:: 2023-05-12T05:56] 15 | I went to bed today at [Sleep:: 2023-05-12T24:10] 16 | 17 | #### Task tracking example 18 | - [ ] Task 1 19 | - [ ] Task 2 20 | - [x] Task 3 21 | - [x] Task 4 22 | - [ ] Task 5 23 | 24 | -------------------------------------------------------------------------------- /EXAMPLE_VAULT/daily notes/2023-05-13.md: -------------------------------------------------------------------------------- 1 | --- 2 | steps: 5936 3 | exercise: 66 minutes 4 | learning: 70 minutes 5 | --- 6 | ## Day No 13 in 2023 7 | Good morning! Today is a beautiful day. 8 | I'm going to learn something new today. 9 | 10 | I learned about the history of the Roman Empire. 11 | 12 | What do you think about the Roman Empire? 13 | 14 | I woke up today at [Woke:: 2023-05-13T08:46] 15 | I went to bed today at [Sleep:: 2023-05-13T22:10] 16 | 17 | #### Task tracking example 18 | - [ ] Task 1 19 | - [ ] Task 2 20 | - [x] Task 3 21 | - [x] Task 4 22 | - [ ] Task 5 23 | 24 | -------------------------------------------------------------------------------- /EXAMPLE_VAULT/daily notes/2023-05-14.md: -------------------------------------------------------------------------------- 1 | --- 2 | steps: 3326 3 | exercise: 11 minutes 4 | learning: 111 minutes 5 | --- 6 | ## Day No 14 in 2023 7 | Good morning! Today is a beautiful day. 8 | I'm going to learn something new today. 9 | 10 | I learned about the history of the Roman Empire. 11 | 12 | What do you think about the Roman Empire? 13 | 14 | I woke up today at [Woke:: 2023-05-14T06:10] 15 | I went to bed today at [Sleep:: 2023-05-14T24:46] 16 | 17 | #### Task tracking example 18 | - [ ] Task 1 19 | - [ ] Task 2 20 | - [ ] Task 3 21 | - [ ] Task 4 22 | - [ ] Task 5 23 | 24 | -------------------------------------------------------------------------------- /EXAMPLE_VAULT/daily notes/2023-05-15.md: -------------------------------------------------------------------------------- 1 | --- 2 | steps: 3501 3 | exercise: 50 minutes 4 | learning: 111 minutes 5 | --- 6 | ## Day No 15 in 2023 7 | Good morning! Today is a beautiful day. 8 | I'm going to learn something new today. 9 | 10 | I learned about the history of the Roman Empire. 11 | 12 | What do you think about the Roman Empire? 13 | 14 | I woke up today at [Woke:: 2023-05-15T05:26] 15 | I went to bed today at [Sleep:: 2023-05-15T24:55] 16 | 17 | #### Task tracking example 18 | - [ ] Task 1 19 | - [x] Task 2 20 | - [x] Task 3 21 | - [ ] Task 4 22 | - [ ] Task 5 23 | 24 | -------------------------------------------------------------------------------- /EXAMPLE_VAULT/daily notes/2023-05-16.md: -------------------------------------------------------------------------------- 1 | --- 2 | steps: 5187 3 | exercise: 65 minutes 4 | learning: 31 minutes 5 | --- 6 | ## Day No 16 in 2023 7 | Good morning! Today is a beautiful day. 8 | I'm going to learn something new today. 9 | 10 | I learned about the history of the Roman Empire. 11 | 12 | What do you think about the Roman Empire? 13 | 14 | I woke up today at [Woke:: 2023-05-16T04:30] 15 | I went to bed today at [Sleep:: 2023-05-16T20:18] 16 | 17 | #### Task tracking example 18 | - [ ] Task 1 19 | - [x] Task 2 20 | - [ ] Task 3 21 | - [ ] Task 4 22 | - [ ] Task 5 23 | 24 | -------------------------------------------------------------------------------- /EXAMPLE_VAULT/daily notes/2023-05-17.md: -------------------------------------------------------------------------------- 1 | --- 2 | steps: 6085 3 | exercise: 69 minutes 4 | learning: 38 minutes 5 | --- 6 | ## Day No 17 in 2023 7 | Good morning! Today is a beautiful day. 8 | I'm going to learn something new today. 9 | 10 | I learned about the history of the Roman Empire. 11 | 12 | What do you think about the Roman Empire? 13 | 14 | I woke up today at [Woke:: 2023-05-17T04:54] 15 | I went to bed today at [Sleep:: 2023-05-17T20:34] 16 | 17 | #### Task tracking example 18 | - [x] Task 1 19 | - [x] Task 2 20 | - [x] Task 3 21 | - [x] Task 4 22 | - [ ] Task 5 23 | 24 | -------------------------------------------------------------------------------- /EXAMPLE_VAULT/daily notes/2023-05-18.md: -------------------------------------------------------------------------------- 1 | --- 2 | steps: 10221 3 | exercise: 48 minutes 4 | learning: 66 minutes 5 | --- 6 | ## Day No 18 in 2023 7 | Good morning! Today is a beautiful day. 8 | I'm going to learn something new today. 9 | 10 | I learned about the history of the Roman Empire. 11 | 12 | What do you think about the Roman Empire? 13 | 14 | I woke up today at [Woke:: 2023-05-18T04:40] 15 | I went to bed today at [Sleep:: 2023-05-18T24:26] 16 | 17 | #### Task tracking example 18 | - [x] Task 1 19 | - [x] Task 2 20 | - [x] Task 3 21 | - [x] Task 4 22 | - [x] Task 5 23 | 24 | -------------------------------------------------------------------------------- /EXAMPLE_VAULT/daily notes/2023-05-19.md: -------------------------------------------------------------------------------- 1 | --- 2 | steps: 4105 3 | exercise: 39 minutes 4 | learning: 111 minutes 5 | --- 6 | ## Day No 19 in 2023 7 | Good morning! Today is a beautiful day. 8 | I'm going to learn something new today. 9 | 10 | I learned about the history of the Roman Empire. 11 | 12 | What do you think about the Roman Empire? 13 | 14 | I woke up today at [Woke:: 2023-05-19T04:30] 15 | I went to bed today at [Sleep:: 2023-05-19T22:01] 16 | 17 | #### Task tracking example 18 | - [ ] Task 1 19 | - [x] Task 2 20 | - [ ] Task 3 21 | - [ ] Task 4 22 | - [ ] Task 5 23 | 24 | -------------------------------------------------------------------------------- /EXAMPLE_VAULT/daily notes/2023-05-20.md: -------------------------------------------------------------------------------- 1 | --- 2 | steps: 7761 3 | exercise: 47 minutes 4 | learning: 122 minutes 5 | --- 6 | ## Day No 20 in 2023 7 | Good morning! Today is a beautiful day. 8 | I'm going to learn something new today. 9 | 10 | I learned about the history of the Roman Empire. 11 | 12 | What do you think about the Roman Empire? 13 | 14 | I woke up today at [Woke:: 2023-05-20T04:24] 15 | I went to bed today at [Sleep:: 2023-05-20T24:57] 16 | 17 | #### Task tracking example 18 | - [x] Task 1 19 | - [ ] Task 2 20 | - [ ] Task 3 21 | - [x] Task 4 22 | - [x] Task 5 23 | 24 | -------------------------------------------------------------------------------- /EXAMPLE_VAULT/daily notes/2024-05-01.md: -------------------------------------------------------------------------------- 1 | --- 2 | steps: 6176 3 | exercise: 44 minutes 4 | learning: 90 minutes 5 | --- 6 | ## Day No 1 in 2024 7 | Good morning! Today is a beautiful day. 8 | I'm going to learn something new today. 9 | 10 | I learned about the history of the Roman Empire. 11 | 12 | What do you think about the Roman Empire? 13 | 14 | I woke up today at [Woke:: 2024-05-01T05:28] 15 | I went to bed today at [Sleep:: 2024-05-01T21:02] 16 | 17 | #### Task tracking example 18 | - [x] Task 1 19 | - [ ] Task 2 20 | - [x] Task 3 21 | - [x] Task 4 22 | - [x] Task 5 23 | 24 | -------------------------------------------------------------------------------- /EXAMPLE_VAULT/daily notes/2024-05-02.md: -------------------------------------------------------------------------------- 1 | --- 2 | steps: 8720 3 | exercise: 44 minutes 4 | learning: 90 minutes 5 | --- 6 | ## Day No 2 in 2024 7 | Good morning! Today is a beautiful day. 8 | I'm going to learn something new today. 9 | 10 | I learned about the history of the Roman Empire. 11 | 12 | What do you think about the Roman Empire? 13 | 14 | I woke up today at [Woke:: 2024-05-02T05:51] 15 | I went to bed today at [Sleep:: 2024-05-02T20:46] 16 | 17 | #### Task tracking example 18 | - [ ] Task 1 19 | - [x] Task 2 20 | - [ ] Task 3 21 | - [ ] Task 4 22 | - [ ] Task 5 23 | 24 | -------------------------------------------------------------------------------- /EXAMPLE_VAULT/daily notes/2024-05-03.md: -------------------------------------------------------------------------------- 1 | --- 2 | steps: 5954 3 | exercise: 48 minutes 4 | learning: 121 minutes 5 | --- 6 | ## Day No 3 in 2024 7 | Good morning! Today is a beautiful day. 8 | I'm going to learn something new today. 9 | 10 | I learned about the history of the Roman Empire. 11 | 12 | What do you think about the Roman Empire? 13 | 14 | I woke up today at [Woke:: 2024-05-03T07:18] 15 | I went to bed today at [Sleep:: 2024-05-03T20:16] 16 | 17 | #### Task tracking example 18 | - [x] Task 1 19 | - [ ] Task 2 20 | - [x] Task 3 21 | - [ ] Task 4 22 | - [ ] Task 5 23 | 24 | -------------------------------------------------------------------------------- /EXAMPLE_VAULT/daily notes/2024-05-04.md: -------------------------------------------------------------------------------- 1 | --- 2 | steps: 7716 3 | exercise: 19 minutes 4 | learning: 37 minutes 5 | --- 6 | ## Day No 4 in 2024 7 | Good morning! Today is a beautiful day. 8 | I'm going to learn something new today. 9 | 10 | I learned about the history of the Roman Empire. 11 | 12 | What do you think about the Roman Empire? 13 | 14 | I woke up today at [Woke:: 2024-05-04T08:59] 15 | I went to bed today at [Sleep:: 2024-05-04T22:59] 16 | 17 | #### Task tracking example 18 | - [ ] Task 1 19 | - [x] Task 2 20 | - [x] Task 3 21 | - [ ] Task 4 22 | - [ ] Task 5 23 | 24 | -------------------------------------------------------------------------------- /EXAMPLE_VAULT/daily notes/2024-05-05.md: -------------------------------------------------------------------------------- 1 | --- 2 | steps: 4465 3 | exercise: 22 minutes 4 | learning: 11 minutes 5 | --- 6 | ## Day No 5 in 2024 7 | Good morning! Today is a beautiful day. 8 | I'm going to learn something new today. 9 | 10 | I learned about the history of the Roman Empire. 11 | 12 | What do you think about the Roman Empire? 13 | 14 | I woke up today at [Woke:: 2024-05-05T05:28] 15 | I went to bed today at [Sleep:: 2024-05-05T20:17] 16 | 17 | #### Task tracking example 18 | - [ ] Task 1 19 | - [x] Task 2 20 | - [x] Task 3 21 | - [x] Task 4 22 | - [ ] Task 5 23 | 24 | -------------------------------------------------------------------------------- /EXAMPLE_VAULT/daily notes/2024-05-06.md: -------------------------------------------------------------------------------- 1 | --- 2 | steps: 8567 3 | exercise: 59 minutes 4 | learning: 32 minutes 5 | --- 6 | ## Day No 6 in 2024 7 | Good morning! Today is a beautiful day. 8 | I'm going to learn something new today. 9 | 10 | I learned about the history of the Roman Empire. 11 | 12 | What do you think about the Roman Empire? 13 | 14 | I woke up today at [Woke:: 2024-05-06T06:14] 15 | I went to bed today at [Sleep:: 2024-05-06T21:53] 16 | 17 | #### Task tracking example 18 | - [x] Task 1 19 | - [ ] Task 2 20 | - [ ] Task 3 21 | - [ ] Task 4 22 | - [x] Task 5 23 | 24 | -------------------------------------------------------------------------------- /EXAMPLE_VAULT/daily notes/2024-05-07.md: -------------------------------------------------------------------------------- 1 | --- 2 | steps: 1515 3 | exercise: 50 minutes 4 | learning: 116 minutes 5 | --- 6 | ## Day No 7 in 2024 7 | Good morning! Today is a beautiful day. 8 | I'm going to learn something new today. 9 | 10 | I learned about the history of the Roman Empire. 11 | 12 | What do you think about the Roman Empire? 13 | 14 | I woke up today at [Woke:: 2024-05-07T05:56] 15 | I went to bed today at [Sleep:: 2024-05-07T24:14] 16 | 17 | #### Task tracking example 18 | - [ ] Task 1 19 | - [ ] Task 2 20 | - [x] Task 3 21 | - [x] Task 4 22 | - [ ] Task 5 23 | 24 | -------------------------------------------------------------------------------- /EXAMPLE_VAULT/daily notes/2024-05-08.md: -------------------------------------------------------------------------------- 1 | --- 2 | steps: 10364 3 | exercise: 57 minutes 4 | learning: 19 minutes 5 | --- 6 | ## Day No 8 in 2024 7 | Good morning! Today is a beautiful day. 8 | I'm going to learn something new today. 9 | 10 | I learned about the history of the Roman Empire. 11 | 12 | What do you think about the Roman Empire? 13 | 14 | I woke up today at [Woke:: 2024-05-08T06:56] 15 | I went to bed today at [Sleep:: 2024-05-08T24:30] 16 | 17 | #### Task tracking example 18 | - [x] Task 1 19 | - [ ] Task 2 20 | - [ ] Task 3 21 | - [x] Task 4 22 | - [ ] Task 5 23 | 24 | -------------------------------------------------------------------------------- /EXAMPLE_VAULT/daily notes/2024-05-09.md: -------------------------------------------------------------------------------- 1 | --- 2 | steps: 8761 3 | exercise: 24 minutes 4 | learning: 42 minutes 5 | --- 6 | ## Day No 9 in 2024 7 | Good morning! Today is a beautiful day. 8 | I'm going to learn something new today. 9 | 10 | I learned about the history of the Roman Empire. 11 | 12 | What do you think about the Roman Empire? 13 | 14 | I woke up today at [Woke:: 2024-05-09T08:17] 15 | I went to bed today at [Sleep:: 2024-05-09T21:25] 16 | 17 | #### Task tracking example 18 | - [ ] Task 1 19 | - [x] Task 2 20 | - [ ] Task 3 21 | - [x] Task 4 22 | - [ ] Task 5 23 | 24 | -------------------------------------------------------------------------------- /EXAMPLE_VAULT/daily notes/2024-05-10.md: -------------------------------------------------------------------------------- 1 | --- 2 | steps: 8164 3 | exercise: 67 minutes 4 | learning: 11 minutes 5 | --- 6 | ## Day No 10 in 2024 7 | Good morning! Today is a beautiful day. 8 | I'm going to learn something new today. 9 | 10 | I learned about the history of the Roman Empire. 11 | 12 | What do you think about the Roman Empire? 13 | 14 | I woke up today at [Woke:: 2024-05-10T08:04] 15 | I went to bed today at [Sleep:: 2024-05-10T23:30] 16 | 17 | #### Task tracking example 18 | - [ ] Task 1 19 | - [x] Task 2 20 | - [ ] Task 3 21 | - [ ] Task 4 22 | - [x] Task 5 23 | 24 | -------------------------------------------------------------------------------- /EXAMPLE_VAULT/daily notes/2024-05-11.md: -------------------------------------------------------------------------------- 1 | --- 2 | steps: 8467 3 | exercise: 22 minutes 4 | learning: 106 minutes 5 | --- 6 | ## Day No 11 in 2024 7 | Good morning! Today is a beautiful day. 8 | I'm going to learn something new today. 9 | 10 | I learned about the history of the Roman Empire. 11 | 12 | What do you think about the Roman Empire? 13 | 14 | I woke up today at [Woke:: 2024-05-11T07:27] 15 | I went to bed today at [Sleep:: 2024-05-11T24:22] 16 | 17 | #### Task tracking example 18 | - [ ] Task 1 19 | - [x] Task 2 20 | - [ ] Task 3 21 | - [ ] Task 4 22 | - [ ] Task 5 23 | 24 | -------------------------------------------------------------------------------- /EXAMPLE_VAULT/daily notes/2024-05-12.md: -------------------------------------------------------------------------------- 1 | --- 2 | steps: 10874 3 | exercise: 18 minutes 4 | learning: 75 minutes 5 | --- 6 | ## Day No 12 in 2024 7 | Good morning! Today is a beautiful day. 8 | I'm going to learn something new today. 9 | 10 | I learned about the history of the Roman Empire. 11 | 12 | What do you think about the Roman Empire? 13 | 14 | I woke up today at [Woke:: 2024-05-12T07:59] 15 | I went to bed today at [Sleep:: 2024-05-12T22:59] 16 | 17 | #### Task tracking example 18 | - [ ] Task 1 19 | - [ ] Task 2 20 | - [x] Task 3 21 | - [ ] Task 4 22 | - [ ] Task 5 23 | 24 | -------------------------------------------------------------------------------- /EXAMPLE_VAULT/daily notes/2024-05-13.md: -------------------------------------------------------------------------------- 1 | --- 2 | steps: 3914 3 | exercise: 54 minutes 4 | learning: 89 minutes 5 | --- 6 | ## Day No 13 in 2024 7 | Good morning! Today is a beautiful day. 8 | I'm going to learn something new today. 9 | 10 | I learned about the history of the Roman Empire. 11 | 12 | What do you think about the Roman Empire? 13 | 14 | I woke up today at [Woke:: 2024-05-13T06:43] 15 | I went to bed today at [Sleep:: 2024-05-13T22:59] 16 | 17 | #### Task tracking example 18 | - [x] Task 1 19 | - [x] Task 2 20 | - [x] Task 3 21 | - [x] Task 4 22 | - [x] Task 5 23 | 24 | -------------------------------------------------------------------------------- /EXAMPLE_VAULT/daily notes/2024-05-14.md: -------------------------------------------------------------------------------- 1 | --- 2 | steps: 10359 3 | exercise: 20 minutes 4 | learning: 79 minutes 5 | --- 6 | ## Day No 14 in 2024 7 | Good morning! Today is a beautiful day. 8 | I'm going to learn something new today. 9 | 10 | I learned about the history of the Roman Empire. 11 | 12 | What do you think about the Roman Empire? 13 | 14 | I woke up today at [Woke:: 2024-05-14T04:34] 15 | I went to bed today at [Sleep:: 2024-05-14T23:21] 16 | 17 | #### Task tracking example 18 | - [ ] Task 1 19 | - [ ] Task 2 20 | - [ ] Task 3 21 | - [ ] Task 4 22 | - [ ] Task 5 23 | 24 | -------------------------------------------------------------------------------- /EXAMPLE_VAULT/daily notes/2024-05-15.md: -------------------------------------------------------------------------------- 1 | --- 2 | steps: 2732 3 | exercise: 17 minutes 4 | learning: 91 minutes 5 | --- 6 | ## Day No 15 in 2024 7 | Good morning! Today is a beautiful day. 8 | I'm going to learn something new today. 9 | 10 | I learned about the history of the Roman Empire. 11 | 12 | What do you think about the Roman Empire? 13 | 14 | I woke up today at [Woke:: 2024-05-15T07:48] 15 | I went to bed today at [Sleep:: 2024-05-15T21:04] 16 | 17 | #### Task tracking example 18 | - [x] Task 1 19 | - [ ] Task 2 20 | - [ ] Task 3 21 | - [ ] Task 4 22 | - [ ] Task 5 23 | 24 | -------------------------------------------------------------------------------- /EXAMPLE_VAULT/daily notes/2024-05-16.md: -------------------------------------------------------------------------------- 1 | --- 2 | steps: 10063 3 | exercise: 11 minutes 4 | learning: 47 minutes 5 | --- 6 | ## Day No 16 in 2024 7 | Good morning! Today is a beautiful day. 8 | I'm going to learn something new today. 9 | 10 | I learned about the history of the Roman Empire. 11 | 12 | What do you think about the Roman Empire? 13 | 14 | I woke up today at [Woke:: 2024-05-16T07:54] 15 | I went to bed today at [Sleep:: 2024-05-16T21:30] 16 | 17 | #### Task tracking example 18 | - [ ] Task 1 19 | - [ ] Task 2 20 | - [x] Task 3 21 | - [ ] Task 4 22 | - [ ] Task 5 23 | 24 | -------------------------------------------------------------------------------- /EXAMPLE_VAULT/daily notes/2024-05-17.md: -------------------------------------------------------------------------------- 1 | --- 2 | steps: 8051 3 | exercise: 29 minutes 4 | learning: 112 minutes 5 | --- 6 | ## Day No 17 in 2024 7 | Good morning! Today is a beautiful day. 8 | I'm going to learn something new today. 9 | 10 | I learned about the history of the Roman Empire. 11 | 12 | What do you think about the Roman Empire? 13 | 14 | I woke up today at [Woke:: 2024-05-17T07:11] 15 | I went to bed today at [Sleep:: 2024-05-17T24:00] 16 | 17 | #### Task tracking example 18 | - [ ] Task 1 19 | - [x] Task 2 20 | - [x] Task 3 21 | - [ ] Task 4 22 | - [ ] Task 5 23 | 24 | -------------------------------------------------------------------------------- /EXAMPLE_VAULT/daily notes/2024-05-18.md: -------------------------------------------------------------------------------- 1 | --- 2 | steps: 5921 3 | exercise: 64 minutes 4 | learning: 114 minutes 5 | --- 6 | ## Day No 18 in 2024 7 | Good morning! Today is a beautiful day. 8 | I'm going to learn something new today. 9 | 10 | I learned about the history of the Roman Empire. 11 | 12 | What do you think about the Roman Empire? 13 | 14 | I woke up today at [Woke:: 2024-05-18T08:15] 15 | I went to bed today at [Sleep:: 2024-05-18T24:15] 16 | 17 | #### Task tracking example 18 | - [x] Task 1 19 | - [ ] Task 2 20 | - [x] Task 3 21 | - [x] Task 4 22 | - [x] Task 5 23 | 24 | -------------------------------------------------------------------------------- /EXAMPLE_VAULT/daily notes/2024-05-19.md: -------------------------------------------------------------------------------- 1 | --- 2 | steps: 3639 3 | exercise: 24 minutes 4 | learning: 113 minutes 5 | --- 6 | ## Day No 19 in 2024 7 | Good morning! Today is a beautiful day. 8 | I'm going to learn something new today. 9 | 10 | I learned about the history of the Roman Empire. 11 | 12 | What do you think about the Roman Empire? 13 | 14 | I woke up today at [Woke:: 2024-05-19T08:43] 15 | I went to bed today at [Sleep:: 2024-05-19T24:30] 16 | 17 | #### Task tracking example 18 | - [ ] Task 1 19 | - [ ] Task 2 20 | - [x] Task 3 21 | - [x] Task 4 22 | - [ ] Task 5 23 | 24 | -------------------------------------------------------------------------------- /EXAMPLE_VAULT/daily notes/2024-05-20.md: -------------------------------------------------------------------------------- 1 | --- 2 | steps: 6796 3 | exercise: 61 minutes 4 | learning: 97 minutes 5 | --- 6 | ## Day No 20 in 2024 7 | Good morning! Today is a beautiful day. 8 | I'm going to learn something new today. 9 | 10 | I learned about the history of the Roman Empire. 11 | 12 | What do you think about the Roman Empire? 13 | 14 | I woke up today at [Woke:: 2024-05-20T05:10] 15 | I went to bed today at [Sleep:: 2024-05-20T21:13] 16 | 17 | #### Task tracking example 18 | - [ ] Task 1 19 | - [x] Task 2 20 | - [x] Task 3 21 | - [x] Task 4 22 | - [x] Task 5 23 | 24 | -------------------------------------------------------------------------------- /EXAMPLE_VAULT/daily notes/2025-05-01.md: -------------------------------------------------------------------------------- 1 | --- 2 | steps: 9000 3 | exercise: 54 minutes 4 | learning: 72 minutes 5 | --- 6 | ## Day No 1 in 2025 7 | Good morning! Today is a beautiful day. 8 | I'm going to learn something new today. 9 | 10 | I learned about the history of the Roman Empire. 11 | 12 | What do you think about the Roman Empire? 13 | 14 | I woke up today at [Woke:: 2025-05-01T06:14] 15 | I went to bed today at [Sleep:: 2025-05-01T23:37] 16 | 17 | #### Task tracking example 18 | - [x] Task 1 19 | - [ ] Task 2 20 | - [x] Task 3 21 | - [x] Task 4 22 | - [ ] Task 5 23 | 24 | -------------------------------------------------------------------------------- /EXAMPLE_VAULT/daily notes/2025-05-02.md: -------------------------------------------------------------------------------- 1 | --- 2 | steps: 5493 3 | exercise: 32 minutes 4 | learning: 71 minutes 5 | --- 6 | ## Day No 2 in 2025 7 | Good morning! Today is a beautiful day. 8 | I'm going to learn something new today. 9 | 10 | I learned about the history of the Roman Empire. 11 | 12 | What do you think about the Roman Empire? 13 | 14 | I woke up today at [Woke:: 2025-05-02T04:22] 15 | I went to bed today at [Sleep:: 2025-05-02T24:09] 16 | 17 | #### Task tracking example 18 | - [ ] Task 1 19 | - [ ] Task 2 20 | - [ ] Task 3 21 | - [ ] Task 4 22 | - [ ] Task 5 23 | 24 | -------------------------------------------------------------------------------- /EXAMPLE_VAULT/daily notes/2025-05-03.md: -------------------------------------------------------------------------------- 1 | --- 2 | steps: 6948 3 | exercise: 11 minutes 4 | learning: 61 minutes 5 | --- 6 | ## Day No 3 in 2025 7 | Good morning! Today is a beautiful day. 8 | I'm going to learn something new today. 9 | 10 | I learned about the history of the Roman Empire. 11 | 12 | What do you think about the Roman Empire? 13 | 14 | I woke up today at [Woke:: 2025-05-03T05:10] 15 | I went to bed today at [Sleep:: 2025-05-03T20:48] 16 | 17 | #### Task tracking example 18 | - [x] Task 1 19 | - [x] Task 2 20 | - [x] Task 3 21 | - [ ] Task 4 22 | - [x] Task 5 23 | 24 | -------------------------------------------------------------------------------- /EXAMPLE_VAULT/daily notes/2025-05-04.md: -------------------------------------------------------------------------------- 1 | --- 2 | steps: 8241 3 | exercise: 66 minutes 4 | learning: 104 minutes 5 | --- 6 | ## Day No 4 in 2025 7 | Good morning! Today is a beautiful day. 8 | I'm going to learn something new today. 9 | 10 | I learned about the history of the Roman Empire. 11 | 12 | What do you think about the Roman Empire? 13 | 14 | I woke up today at [Woke:: 2025-05-04T08:32] 15 | I went to bed today at [Sleep:: 2025-05-04T20:53] 16 | 17 | #### Task tracking example 18 | - [x] Task 1 19 | - [ ] Task 2 20 | - [ ] Task 3 21 | - [ ] Task 4 22 | - [x] Task 5 23 | 24 | -------------------------------------------------------------------------------- /EXAMPLE_VAULT/daily notes/2025-05-05.md: -------------------------------------------------------------------------------- 1 | --- 2 | steps: 3287 3 | exercise: 35 minutes 4 | learning: 34 minutes 5 | --- 6 | ## Day No 5 in 2025 7 | Good morning! Today is a beautiful day. 8 | I'm going to learn something new today. 9 | 10 | I learned about the history of the Roman Empire. 11 | 12 | What do you think about the Roman Empire? 13 | 14 | I woke up today at [Woke:: 2025-05-05T07:33] 15 | I went to bed today at [Sleep:: 2025-05-05T22:07] 16 | 17 | #### Task tracking example 18 | - [x] Task 1 19 | - [x] Task 2 20 | - [x] Task 3 21 | - [x] Task 4 22 | - [x] Task 5 23 | 24 | -------------------------------------------------------------------------------- /EXAMPLE_VAULT/daily notes/2025-05-06.md: -------------------------------------------------------------------------------- 1 | --- 2 | steps: 7522 3 | exercise: 12 minutes 4 | learning: 111 minutes 5 | --- 6 | ## Day No 6 in 2025 7 | Good morning! Today is a beautiful day. 8 | I'm going to learn something new today. 9 | 10 | I learned about the history of the Roman Empire. 11 | 12 | What do you think about the Roman Empire? 13 | 14 | I woke up today at [Woke:: 2025-05-06T06:35] 15 | I went to bed today at [Sleep:: 2025-05-06T20:57] 16 | 17 | #### Task tracking example 18 | - [ ] Task 1 19 | - [x] Task 2 20 | - [x] Task 3 21 | - [ ] Task 4 22 | - [ ] Task 5 23 | 24 | -------------------------------------------------------------------------------- /EXAMPLE_VAULT/daily notes/2025-05-07.md: -------------------------------------------------------------------------------- 1 | --- 2 | steps: 4486 3 | exercise: 53 minutes 4 | learning: 80 minutes 5 | --- 6 | ## Day No 7 in 2025 7 | Good morning! Today is a beautiful day. 8 | I'm going to learn something new today. 9 | 10 | I learned about the history of the Roman Empire. 11 | 12 | What do you think about the Roman Empire? 13 | 14 | I woke up today at [Woke:: 2025-05-07T07:10] 15 | I went to bed today at [Sleep:: 2025-05-07T21:24] 16 | 17 | #### Task tracking example 18 | - [x] Task 1 19 | - [ ] Task 2 20 | - [x] Task 3 21 | - [ ] Task 4 22 | - [ ] Task 5 23 | 24 | -------------------------------------------------------------------------------- /EXAMPLE_VAULT/daily notes/2025-05-08.md: -------------------------------------------------------------------------------- 1 | --- 2 | steps: 3346 3 | exercise: 65 minutes 4 | learning: 46 minutes 5 | --- 6 | ## Day No 8 in 2025 7 | Good morning! Today is a beautiful day. 8 | I'm going to learn something new today. 9 | 10 | I learned about the history of the Roman Empire. 11 | 12 | What do you think about the Roman Empire? 13 | 14 | I woke up today at [Woke:: 2025-05-08T08:17] 15 | I went to bed today at [Sleep:: 2025-05-08T23:15] 16 | 17 | #### Task tracking example 18 | - [x] Task 1 19 | - [x] Task 2 20 | - [ ] Task 3 21 | - [ ] Task 4 22 | - [x] Task 5 23 | 24 | -------------------------------------------------------------------------------- /EXAMPLE_VAULT/daily notes/2025-05-09.md: -------------------------------------------------------------------------------- 1 | --- 2 | steps: 3635 3 | exercise: 60 minutes 4 | learning: 119 minutes 5 | --- 6 | ## Day No 9 in 2025 7 | Good morning! Today is a beautiful day. 8 | I'm going to learn something new today. 9 | 10 | I learned about the history of the Roman Empire. 11 | 12 | What do you think about the Roman Empire? 13 | 14 | I woke up today at [Woke:: 2025-05-09T07:29] 15 | I went to bed today at [Sleep:: 2025-05-09T22:26] 16 | 17 | #### Task tracking example 18 | - [ ] Task 1 19 | - [ ] Task 2 20 | - [x] Task 3 21 | - [x] Task 4 22 | - [ ] Task 5 23 | 24 | -------------------------------------------------------------------------------- /EXAMPLE_VAULT/daily notes/2025-05-10.md: -------------------------------------------------------------------------------- 1 | --- 2 | steps: 2361 3 | exercise: 14 minutes 4 | learning: 42 minutes 5 | --- 6 | ## Day No 10 in 2025 7 | Good morning! Today is a beautiful day. 8 | I'm going to learn something new today. 9 | 10 | I learned about the history of the Roman Empire. 11 | 12 | What do you think about the Roman Empire? 13 | 14 | I woke up today at [Woke:: 2025-05-10T07:04] 15 | I went to bed today at [Sleep:: 2025-05-10T21:02] 16 | 17 | #### Task tracking example 18 | - [ ] Task 1 19 | - [ ] Task 2 20 | - [ ] Task 3 21 | - [ ] Task 4 22 | - [x] Task 5 23 | 24 | -------------------------------------------------------------------------------- /EXAMPLE_VAULT/daily notes/2025-05-11.md: -------------------------------------------------------------------------------- 1 | --- 2 | steps: 5468 3 | exercise: 69 minutes 4 | learning: 90 minutes 5 | --- 6 | ## Day No 11 in 2025 7 | Good morning! Today is a beautiful day. 8 | I'm going to learn something new today. 9 | 10 | I learned about the history of the Roman Empire. 11 | 12 | What do you think about the Roman Empire? 13 | 14 | I woke up today at [Woke:: 2025-05-11T07:27] 15 | I went to bed today at [Sleep:: 2025-05-11T20:38] 16 | 17 | #### Task tracking example 18 | - [ ] Task 1 19 | - [x] Task 2 20 | - [ ] Task 3 21 | - [x] Task 4 22 | - [ ] Task 5 23 | 24 | -------------------------------------------------------------------------------- /EXAMPLE_VAULT/daily notes/2025-05-12.md: -------------------------------------------------------------------------------- 1 | --- 2 | steps: 4781 3 | exercise: 32 minutes 4 | learning: 20 minutes 5 | --- 6 | ## Day No 12 in 2025 7 | Good morning! Today is a beautiful day. 8 | I'm going to learn something new today. 9 | 10 | I learned about the history of the Roman Empire. 11 | 12 | What do you think about the Roman Empire? 13 | 14 | I woke up today at [Woke:: 2025-05-12T06:57] 15 | I went to bed today at [Sleep:: 2025-05-12T21:27] 16 | 17 | #### Task tracking example 18 | - [ ] Task 1 19 | - [ ] Task 2 20 | - [x] Task 3 21 | - [x] Task 4 22 | - [ ] Task 5 23 | 24 | -------------------------------------------------------------------------------- /EXAMPLE_VAULT/daily notes/2025-05-13.md: -------------------------------------------------------------------------------- 1 | --- 2 | steps: 8588 3 | exercise: 59 minutes 4 | learning: 48 minutes 5 | --- 6 | ## Day No 13 in 2025 7 | Good morning! Today is a beautiful day. 8 | I'm going to learn something new today. 9 | 10 | I learned about the history of the Roman Empire. 11 | 12 | What do you think about the Roman Empire? 13 | 14 | I woke up today at [Woke:: 2025-05-13T05:55] 15 | I went to bed today at [Sleep:: 2025-05-13T21:57] 16 | 17 | #### Task tracking example 18 | - [ ] Task 1 19 | - [x] Task 2 20 | - [ ] Task 3 21 | - [ ] Task 4 22 | - [x] Task 5 23 | 24 | -------------------------------------------------------------------------------- /EXAMPLE_VAULT/daily notes/2025-05-14.md: -------------------------------------------------------------------------------- 1 | --- 2 | steps: 2524 3 | exercise: 50 minutes 4 | learning: 25 minutes 5 | --- 6 | ## Day No 14 in 2025 7 | Good morning! Today is a beautiful day. 8 | I'm going to learn something new today. 9 | 10 | I learned about the history of the Roman Empire. 11 | 12 | What do you think about the Roman Empire? 13 | 14 | I woke up today at [Woke:: 2025-05-14T08:48] 15 | I went to bed today at [Sleep:: 2025-05-14T24:59] 16 | 17 | #### Task tracking example 18 | - [x] Task 1 19 | - [ ] Task 2 20 | - [ ] Task 3 21 | - [x] Task 4 22 | - [ ] Task 5 23 | 24 | -------------------------------------------------------------------------------- /EXAMPLE_VAULT/daily notes/2025-05-15.md: -------------------------------------------------------------------------------- 1 | --- 2 | steps: 3368 3 | exercise: 33 minutes 4 | learning: 69 minutes 5 | --- 6 | ## Day No 15 in 2025 7 | Good morning! Today is a beautiful day. 8 | I'm going to learn something new today. 9 | 10 | I learned about the history of the Roman Empire. 11 | 12 | What do you think about the Roman Empire? 13 | 14 | I woke up today at [Woke:: 2025-05-15T04:54] 15 | I went to bed today at [Sleep:: 2025-05-15T23:08] 16 | 17 | #### Task tracking example 18 | - [x] Task 1 19 | - [ ] Task 2 20 | - [x] Task 3 21 | - [x] Task 4 22 | - [x] Task 5 23 | 24 | -------------------------------------------------------------------------------- /EXAMPLE_VAULT/daily notes/2025-05-16.md: -------------------------------------------------------------------------------- 1 | --- 2 | steps: 2420 3 | exercise: 21 minutes 4 | learning: 19 minutes 5 | --- 6 | ## Day No 16 in 2025 7 | Good morning! Today is a beautiful day. 8 | I'm going to learn something new today. 9 | 10 | I learned about the history of the Roman Empire. 11 | 12 | What do you think about the Roman Empire? 13 | 14 | I woke up today at [Woke:: 2025-05-16T07:19] 15 | I went to bed today at [Sleep:: 2025-05-16T23:30] 16 | 17 | #### Task tracking example 18 | - [x] Task 1 19 | - [x] Task 2 20 | - [x] Task 3 21 | - [x] Task 4 22 | - [x] Task 5 23 | 24 | -------------------------------------------------------------------------------- /EXAMPLE_VAULT/daily notes/2025-05-17.md: -------------------------------------------------------------------------------- 1 | --- 2 | steps: 9379 3 | exercise: 68 minutes 4 | learning: 31 minutes 5 | --- 6 | ## Day No 17 in 2025 7 | Good morning! Today is a beautiful day. 8 | I'm going to learn something new today. 9 | 10 | I learned about the history of the Roman Empire. 11 | 12 | What do you think about the Roman Empire? 13 | 14 | I woke up today at [Woke:: 2025-05-17T07:40] 15 | I went to bed today at [Sleep:: 2025-05-17T20:55] 16 | 17 | #### Task tracking example 18 | - [x] Task 1 19 | - [x] Task 2 20 | - [x] Task 3 21 | - [x] Task 4 22 | - [ ] Task 5 23 | 24 | -------------------------------------------------------------------------------- /EXAMPLE_VAULT/daily notes/2025-05-18.md: -------------------------------------------------------------------------------- 1 | --- 2 | steps: 3388 3 | exercise: 50 minutes 4 | learning: 118 minutes 5 | --- 6 | ## Day No 18 in 2025 7 | Good morning! Today is a beautiful day. 8 | I'm going to learn something new today. 9 | 10 | I learned about the history of the Roman Empire. 11 | 12 | What do you think about the Roman Empire? 13 | 14 | I woke up today at [Woke:: 2025-05-18T05:50] 15 | I went to bed today at [Sleep:: 2025-05-18T23:00] 16 | 17 | #### Task tracking example 18 | - [ ] Task 1 19 | - [ ] Task 2 20 | - [x] Task 3 21 | - [ ] Task 4 22 | - [ ] Task 5 23 | 24 | -------------------------------------------------------------------------------- /EXAMPLE_VAULT/daily notes/2025-05-19.md: -------------------------------------------------------------------------------- 1 | --- 2 | steps: 0 3 | exercise: 14 minutes 4 | learning: 129 minutes 5 | --- 6 | ## Day No 19 in 2025 7 | Good morning! Today is a beautiful day. 8 | I'm going to learn something new today. 9 | 10 | I learned about the history of the Roman Empire. 11 | 12 | What do you think about the Roman Empire? 13 | 14 | I woke up today at [Woke:: 2025-05-19T07:09] 15 | I went to bed today at [Sleep:: 2025-05-19T20:58] 16 | 17 | #### Task tracking example 18 | - [ ] Task 1 19 | - [ ] Task 2 20 | - [ ] Task 3 21 | - [x] Task 4 22 | - [ ] Task 5 23 | 24 | -------------------------------------------------------------------------------- /EXAMPLE_VAULT/daily notes/2025-05-20.md: -------------------------------------------------------------------------------- 1 | --- 2 | steps: 8056 3 | exercise: 32 minutes 4 | learning: 68 minutes 5 | --- 6 | ## Day No 20 in 2025 7 | Good morning! Today is a beautiful day. 8 | I'm going to learn something new today. 9 | 10 | I learned about the history of the Roman Empire. 11 | 12 | What do you think about the Roman Empire? 13 | 14 | I woke up today at [Woke:: 2025-05-20T05:26] 15 | I went to bed today at [Sleep:: 2025-05-20T20:31] 16 | 17 | #### Task tracking example 18 | - [ ] Task 1 19 | - [x] Task 2 20 | - [ ] Task 3 21 | - [x] Task 4 22 | - [x] Task 5 23 | 24 | -------------------------------------------------------------------------------- /src/components/icons/MenuIcon.tsx: -------------------------------------------------------------------------------- 1 | export function MenuIcon() { 2 | return ( 3 | 15 | 16 | 17 | 18 | 19 | ); 20 | } 21 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2017", 4 | "module": "ESNext", 5 | "strict": true, 6 | "noImplicitAny": true, 7 | "moduleResolution": "node", 8 | "esModuleInterop": true, 9 | "skipLibCheck": true, 10 | "outDir": "dist", 11 | "sourceMap": true, 12 | "baseUrl": ".", 13 | "paths": { 14 | "src/*": ["src/*"] 15 | }, 16 | "allowSyntheticDefaultImports": true, 17 | "resolveJsonModule": true, 18 | "jsx": "react-jsx" 19 | 20 | }, 21 | "include": ["src/**/*"], 22 | "exclude": ["node_modules", "dist"] 23 | } -------------------------------------------------------------------------------- /EXAMPLE_VAULT/daily notes/2023-05-02.md: -------------------------------------------------------------------------------- 1 | --- 2 | steps: 1228 3 | exercise: 10 minutes 4 | learning: 13 minutes 5 | issue_62: -1 6 | --- 7 | ## Day No 2 in 2023 8 | Good morning! Today is a beautiful day. 9 | I'm going to learn something new today. 10 | 11 | I learned about the history of the Roman Empire. 12 | 13 | What do you think about the Roman Empire? 14 | 15 | I woke up today at [Woke:: 2023-05-02T07:56] 16 | I went to bed today at [Sleep:: 2023-05-02T22:06] 17 | 18 | #### Task tracking example 19 | - [ ] Task 1 20 | - [ ] Task 2 21 | - [ ] Task 3 22 | - [x] Task 4 23 | - [ ] Task 5 24 | 25 | -------------------------------------------------------------------------------- /EXAMPLE_VAULT/daily notes/2023-05-03.md: -------------------------------------------------------------------------------- 1 | --- 2 | steps: 7714 3 | exercise: 39 minutes 4 | learning: 62 minutes 5 | issue_62: 0 6 | --- 7 | ## Day No 3 in 2023 8 | Good morning! Today is a beautiful day. 9 | I'm going to learn something new today. 10 | 11 | I learned about the history of the Roman Empire. 12 | 13 | What do you think about the Roman Empire? 14 | 15 | I woke up today at [Woke:: 2023-05-03T05:54] 16 | I went to bed today at [Sleep:: 2023-05-03T24:13] 17 | 18 | #### Task tracking example 19 | - [x] Task 1 20 | - [ ] Task 2 21 | - [x] Task 3 22 | - [x] Task 4 23 | - [x] Task 5 24 | 25 | -------------------------------------------------------------------------------- /EXAMPLE_VAULT/daily notes/2023-05-04.md: -------------------------------------------------------------------------------- 1 | --- 2 | steps: 10165 3 | exercise: 26 minutes 4 | learning: 99 minutes 5 | issue_62: 1 6 | --- 7 | ## Day No 4 in 2023 8 | Good morning! Today is a beautiful day. 9 | I'm going to learn something new today. 10 | 11 | I learned about the history of the Roman Empire. 12 | 13 | What do you think about the Roman Empire? 14 | 15 | I woke up today at [Woke:: 2023-05-04T06:30] 16 | I went to bed today at [Sleep:: 2023-05-04T21:46] 17 | 18 | #### Task tracking example 19 | - [ ] Task 1 20 | - [x] Task 2 21 | - [ ] Task 3 22 | - [ ] Task 4 23 | - [ ] Task 5 24 | 25 | -------------------------------------------------------------------------------- /EXAMPLE_VAULT/daily notes/2023-05-05.md: -------------------------------------------------------------------------------- 1 | --- 2 | steps: 1329 3 | exercise: 54 minutes 4 | learning: 127 minutes 5 | issue_62: 2 6 | --- 7 | ## Day No 5 in 2023 8 | Good morning! Today is a beautiful day. 9 | I'm going to learn something new today. 10 | 11 | I learned about the history of the Roman Empire. 12 | 13 | What do you think about the Roman Empire? 14 | 15 | I woke up today at [Woke:: 2023-05-05T05:53] 16 | I went to bed today at [Sleep:: 2023-05-05T23:38] 17 | 18 | #### Task tracking example 19 | - [x] Task 1 20 | - [x] Task 2 21 | - [x] Task 3 22 | - [ ] Task 4 23 | - [x] Task 5 24 | 25 | -------------------------------------------------------------------------------- /EXAMPLE_VAULT/daily notes/2023-05-01.md: -------------------------------------------------------------------------------- 1 | --- 2 | steps: 5270 3 | exercise: 46 minutes 4 | learning: 126 minutes 5 | issue_62: -2 6 | --- 7 | ## Day No 1 in 2023 8 | Good morning! Today is a beautiful day. 9 | I'm going to learn something new today. 10 | 11 | I learned about the history of the Roman Empire. 12 | 13 | What do you think about the Roman Empire? 14 | 15 | I woke up today at [Woke:: 2023-05-01T07:58] 16 | I went to bed today at [Sleep:: 2023-05-01T20:44] 17 | 18 | #### Task tracking example 19 | - [x] Task 1 20 | - [x] Task 2 21 | - [ ] Task 3 22 | - [x] Task 4 23 | - [ ] Task 5 24 | 25 | -------------------------------------------------------------------------------- /src/components/icons/ReportBugIcon.tsx: -------------------------------------------------------------------------------- 1 | export function ReportBugIcon() { 2 | return ( 3 | 15 | 16 | 17 | 18 | 19 | ); 20 | } 21 | -------------------------------------------------------------------------------- /src/styles/heatmap-statistics.scss: -------------------------------------------------------------------------------- 1 | /* Heatmap Statistics */ 2 | .heatmap-statistics { 3 | } 4 | 5 | .heatmap-statistics hr { 6 | margin: 8px 0; 7 | } 8 | 9 | .heatmap-statistics__header { 10 | display: grid; 11 | grid-template-columns: 1fr 5fr 1fr; 12 | justify-items: center; 13 | align-items: center; 14 | 15 | font-size: 0.65em; 16 | } 17 | 18 | .heatmap-statistics__header button { 19 | justify-self: start; 20 | } 21 | 22 | .heatmap-statistics__title { 23 | font-size: 1.2em; 24 | text-align: center; 25 | } 26 | 27 | .heatmap-statistics__content { 28 | font-size: 0.8em; 29 | margin-top: 0.6em; 30 | } -------------------------------------------------------------------------------- /src/components/TipOfTheDay/TipOfTheDay.tsx: -------------------------------------------------------------------------------- 1 | const TIPS = [ 2 | { 3 | body: 'Do you want to display legend separately? Use `renderHeatmapTrackerLegend(this.container, trackerData)` to generate it.' 4 | }, 5 | { 6 | body: 'Use `separateMonths` to separate months on the view! It looks great!' 7 | }, 8 | { 9 | body: 'You can enable/disable tip of the day in the settings' 10 | }, 11 | { 12 | body: 'To be honest, it is not the tip of the day. It selects a random tip every time you open Heatmap Tracker :)' 13 | }, 14 | { 15 | body: 'Check settings, there are some really nice features!', 16 | } 17 | ]; -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('ts-jest').JestConfigWithTsJest} */ 2 | module.exports = { 3 | preset: "ts-jest", 4 | testEnvironment: "jsdom", 5 | // Add paths to ignore during testing if needed 6 | testPathIgnorePatterns: ["/node_modules/", "/dist/"], 7 | // Configure code coverage 8 | collectCoverage: true, 9 | collectCoverageFrom: ["src/**/*.ts"], 10 | coverageDirectory: "coverage", 11 | moduleDirectories: ["./node_modules", "./src"], 12 | rootDir: ".", 13 | moduleNameMapper: { 14 | "^src/(.*)$": "/src/$1", 15 | }, 16 | transform: { 17 | "^.+\\.(ts|tsx)$": "ts-jest", 18 | }, 19 | }; 20 | -------------------------------------------------------------------------------- /src/components/icons/HeatmapIcon.tsx: -------------------------------------------------------------------------------- 1 | export function HeatmapIcon() { 2 | return ( 3 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | ); 22 | } 23 | -------------------------------------------------------------------------------- /EXAMPLE_VAULT/test.md: -------------------------------------------------------------------------------- 1 | 2 | ```dataviewjs 3 | 4 | const trackerData = { 5 | heatmapTitle: "Test", 6 | intensityScaleStart: 0, 7 | intensityScaleEnd: 10, 8 | separateMonths: true, 9 | entries: [ 10 | { 11 | "date": "2025-04-04T00:00:00+01:00", 12 | "intensity": '0' 13 | }, 14 | { 15 | "date": "2025-04-15", 16 | "intensity": '5' 17 | }, 18 | { 19 | "date": "2025-04-16", 20 | "intensity": 8 21 | }, 22 | { 23 | "date": "2025-04-17", 24 | "intensity": 10 25 | }, 26 | ] 27 | } 28 | 29 | renderHeatmapTracker(this.container, trackerData) 30 | 31 | ``` 32 | -------------------------------------------------------------------------------- /EXAMPLE_VAULT/Github Issues/2. Add Week Numbers Under the Heatmap.md: -------------------------------------------------------------------------------- 1 | 2 | 1. Should display week numbers correctly when `separateMonths` disabled. 3 | ```heatmap-tracker 4 | 5 | property: steps 6 | year: 2025 7 | ui: 8 | showWeekNums: true 9 | separateMonths: false 10 | 11 | ``` 12 | 13 | 2. Should display week number correctly when `separateMonths` enabled. 14 | ```heatmap-tracker 15 | 16 | property: steps 17 | year: 2025 18 | ui: 19 | showWeekNums: true 20 | separateMonths: true 21 | 22 | ``` 23 | 24 | 3. Should NOT display week numbers. 25 | ```heatmap-tracker 26 | 27 | property: steps 28 | year: 2025 29 | ui: 30 | showWeekNums: false 31 | 32 | ``` -------------------------------------------------------------------------------- /src/components/icons/ShieldXIcon.tsx: -------------------------------------------------------------------------------- 1 | export function ShieldXIcon() { 2 | return ( 3 | 15 | 16 | 17 | 18 | 19 | ); 20 | } -------------------------------------------------------------------------------- /src/utils/tabs.tsx: -------------------------------------------------------------------------------- 1 | import { HeatmapIcon } from "src/components/icons/HeatmapIcon"; 2 | import { StatisticsIcon } from "src/components/icons/StatisticsIcon"; 3 | import { DocumentationIcon } from "src/components/icons/DocumentationIcon"; 4 | import { ReactNode } from "react"; 5 | import { IHeatmapView } from "src/types"; 6 | import { LegendIcon } from "src/components/icons/LegendIcon"; 7 | 8 | export const TabIconForView: Record = { 9 | [IHeatmapView.HeatmapTracker]: , 10 | [IHeatmapView.HeatmapTrackerStatistics]: , 11 | [IHeatmapView.Documentation]: , 12 | [IHeatmapView.Legend]: , 13 | }; 14 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /version-bump.mjs: -------------------------------------------------------------------------------- 1 | import { readFileSync, writeFileSync } from "fs"; 2 | 3 | const targetVersion = process.env.npm_package_version; 4 | 5 | // read minAppVersion from manifest.json and bump version to target version 6 | let manifest = JSON.parse(readFileSync("manifest.json", "utf8")); 7 | const { minAppVersion } = manifest; 8 | manifest.version = targetVersion; 9 | writeFileSync("manifest.json", JSON.stringify(manifest, null, "\t")); 10 | 11 | // update versions.json with target version and minAppVersion from manifest.json 12 | let versions = JSON.parse(readFileSync("versions.json", "utf8")); 13 | versions[targetVersion] = minAppVersion; 14 | writeFileSync("versions.json", JSON.stringify(versions, null, "\t")); 15 | -------------------------------------------------------------------------------- /EXAMPLE_VAULT/Documentation with Examples/3. trackerData parameters/8. basePath.md: -------------------------------------------------------------------------------- 1 | # basePath 2 | 3 | `basePath` is an optional string parameter that specifies the folder where new files should be created when clicking on a heatmap box that doesn't have a corresponding file. 4 | 5 | If `basePath` is set, and you click on a day without a file, the plugin will propose to create a new file at `basePath/YYYY-MM-DD.md`. 6 | 7 | ## Example 8 | 9 | ```dataviewjs 10 | const trackerData = { 11 | entries: [], 12 | basePath: "daily notes", // New files will be created in "daily notes" folder 13 | heatmapTitle: "Heatmap with basePath", 14 | } 15 | 16 | renderHeatmapTracker(this.container, trackerData); 17 | ``` 18 | -------------------------------------------------------------------------------- /EXAMPLE_VAULT/Examples/Examples.md: -------------------------------------------------------------------------------- 1 | # Examples 2 | 3 | Here you can find various examples of how to use the Heatmap Tracker. 4 | 5 | ## 🆕 New Examples 6 | - [[Habit Tracker/Exercise|Habit Tracker (Boolean)]] 7 | - [[Mood Tracker/Daily Mood|Mood Tracker (1-5 Scale)]] 8 | - [[Project Progress/Project Alpha|Project Progress (%)]] 9 | 10 | ## 📚 More Examples 11 | - [[Hours slept example|Hours Slept]] 12 | - [[Meditation example|Meditation]] 13 | - [[Sleep quality example|Sleep Quality]] 14 | - [[Task Tracking example from Reddit|Task Tracking (Reddit Example)]] 15 | - [[Water intake example|Water Intake]] 16 | 17 | > [!TIP] 18 | > You can copy the code blocks from these examples directly into your daily notes or other files. 19 | -------------------------------------------------------------------------------- /src/components/HeatmapBoxesList/HeatmapBoxesList.tsx: -------------------------------------------------------------------------------- 1 | import { Box } from "src/types"; 2 | import { HeatmapBox } from "../HeatmapBox/HeatmapBox"; 3 | import { useHeatmapContext } from "src/context/heatmap/heatmap.context"; 4 | 5 | interface HeatmapBoxesListProps { 6 | boxes: Box[]; 7 | } 8 | 9 | export function HeatmapBoxesList({ boxes }: HeatmapBoxesListProps) { 10 | const { trackerData } = useHeatmapContext(); 11 | 12 | return ( 13 |
18 | {boxes.map((box, index) => { 19 | return ; 20 | })} 21 |
22 | ); 23 | } 24 | -------------------------------------------------------------------------------- /src/components/icons/StatisticsIcon.tsx: -------------------------------------------------------------------------------- 1 | export function StatisticsIcon() { 2 | return ( 3 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | ); 25 | } 26 | -------------------------------------------------------------------------------- /src/components/icons/HandCoinsIcon.tsx: -------------------------------------------------------------------------------- 1 | export function HandCoinsIcon() { 2 | return ( 3 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | ); 22 | } 23 | -------------------------------------------------------------------------------- /EXAMPLE_VAULT/Examples/Task Tracking Example from Reddit/notes/2025-01-02.md: -------------------------------------------------------------------------------- 1 | 2 | --- 3 | [[2025]] / [[2025-Q1|Q1]] / [[2025-01|January]] / [[2025-W01|Week 1]] 4 | 5 | ❮ [[2025-01-01]] | 2025-01-02 | [[2025-01-03]] ❯ 6 | ## Stats 7 | %%Record statistics; alcohol consumption, calories, weight%% 8 | - 9 | 10 | ## Daily Record 11 | %% Record Your Day %% 12 | - 13 | 14 | ## Thoughts 15 | - 16 | 17 | ## Habit 18 | - Physical 19 | - [ ] Brush Teeth 20 | - [ ] Exercise 21 | - [ ] 3L of Water 22 | - 23 | - [ ] Supplements 24 | - Creatine 25 | 26 | - Mental 27 | - [ ] Make Bed 28 | - [ ] Review weekly note 29 | - [ ] Learn Spanish for 10min 30 | - [ ] Journal 31 | - [ ] Read a book for 10min 32 | - [[*curent book*]] 33 | 34 | - Spiritual 35 | - [ ] Pray -------------------------------------------------------------------------------- /src/components/icons/DocumentationIcon.tsx: -------------------------------------------------------------------------------- 1 | export function DocumentationIcon() { 2 | return ( 3 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | ); 23 | } -------------------------------------------------------------------------------- /src/components/icons/LegendIcon.tsx: -------------------------------------------------------------------------------- 1 | export function LegendIcon() { 2 | return ( 3 | 15 | 16 | 17 | 18 | 19 | ); 20 | } 21 | -------------------------------------------------------------------------------- /EXAMPLE_VAULT/Activities Tracking/Gym.md: -------------------------------------------------------------------------------- 1 | --- 2 | class: gym 3 | tracking: true 4 | color: "#FF1199" 5 | --- 6 | - [[2024-11-21]] I'm in the Gym 7 | - [[2024-11-22]] I did some exercises 8 | - [[2024-11-23]] Cardio workout 9 | - [[2024-11-24]] Strength training 10 | - [[2024-11-25]] Rest day 11 | - [[2024-11-26]] Yoga session 12 | - [[2024-11-27]] HIIT workout 13 | - [[2024-11-28]] Leg day 14 | - [[2024-11-29]] Upper body workout 15 | - [[2024-11-30]] Full body workout 16 | - [[2024-12-01]] Swimming 17 | - [[2024-12-02]] Running 18 | - [[2024-12-03]] Cycling 19 | - [[2024-12-04]] Pilates 20 | - [[2024-12-05]] Boxing 21 | - [[2024-12-06]] CrossFit 22 | - [[2024-12-07]] Rest day 23 | - [[2024-12-08]] Stretching exercises 24 | - [[2024-12-09]] Core workout 25 | - [[2024-12-10]] Dance class 26 | - -------------------------------------------------------------------------------- /src/components/HeatmapTabs/HeatmapTabs.tsx: -------------------------------------------------------------------------------- 1 | import { useTranslation } from "react-i18next"; 2 | import HeatmapTab from "../HeatmapTab/HeatmapTab"; 3 | import { IHeatmapView } from "src/types"; 4 | 5 | export function HeatmapTabs() { 6 | const { t } = useTranslation(); 7 | 8 | return ( 9 |
10 | 11 | 15 | 16 | 17 |
18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /src/utils/colors.ts: -------------------------------------------------------------------------------- 1 | import { ColorScheme, ColorsList, Palettes } from "src/types"; 2 | import { isEmpty } from "./core"; 3 | 4 | /** 5 | * Retrieves the color scheme for the tracker data based on the provided settings. 6 | * 7 | * @param trackerData - The data containing the color scheme information. 8 | * @param settingsColors - The available color palettes. 9 | * @returns The list of colors to be used. 10 | */ 11 | export function getColors(colorScheme: ColorScheme, settingsColors: Palettes): ColorsList { 12 | const { paletteName, customColors } = colorScheme ?? {}; 13 | 14 | if (!isEmpty(customColors)) { 15 | return customColors as ColorsList; 16 | } 17 | 18 | return paletteName && settingsColors[paletteName] ? settingsColors[paletteName] : settingsColors['default']; 19 | } -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release Obsidian plugin 2 | 3 | on: 4 | push: 5 | tags: 6 | - "*" 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - uses: actions/checkout@v3 14 | 15 | - name: Use Node.js 16 | uses: actions/setup-node@v3 17 | with: 18 | node-version: "18.x" 19 | 20 | - name: Build plugin 21 | run: | 22 | npm install 23 | npm run build 24 | 25 | - name: Create release 26 | env: 27 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 28 | run: | 29 | tag="${GITHUB_REF#refs/tags/}" 30 | 31 | gh release create "$tag" \ 32 | --title="$tag" \ 33 | --draft \ 34 | ./build/main.js ./build/manifest.json ./build/styles.css -------------------------------------------------------------------------------- /src/components/HeatmapMonthsList/HeatmapMonthsList.tsx: -------------------------------------------------------------------------------- 1 | import { useTranslation } from "react-i18next"; 2 | 3 | export function HeatmapMonthsList() { 4 | const { t } = useTranslation(); 5 | return ( 6 |
7 | {[ 8 | t("monthsShort.January"), 9 | t("monthsShort.February"), 10 | t("monthsShort.March"), 11 | t("monthsShort.April"), 12 | t("monthsShort.May"), 13 | t("monthsShort.June"), 14 | t("monthsShort.July"), 15 | t("monthsShort.August"), 16 | t("monthsShort.September"), 17 | t("monthsShort.October"), 18 | t("monthsShort.November"), 19 | t("monthsShort.December"), 20 | ].map((month) => ( 21 |
{month}
22 | ))} 23 |
24 | ); 25 | } 26 | -------------------------------------------------------------------------------- /EXAMPLE_VAULT/.obsidian/core-plugins-migration.json: -------------------------------------------------------------------------------- 1 | { 2 | "file-explorer": true, 3 | "global-search": true, 4 | "switcher": false, 5 | "graph": false, 6 | "backlink": false, 7 | "outgoing-link": false, 8 | "tag-pane": false, 9 | "page-preview": true, 10 | "daily-notes": true, 11 | "templates": false, 12 | "note-composer": false, 13 | "command-palette": true, 14 | "slash-command": false, 15 | "editor-status": true, 16 | "starred": true, 17 | "markdown-importer": false, 18 | "zk-prefixer": false, 19 | "random-note": false, 20 | "outline": false, 21 | "word-count": false, 22 | "slides": false, 23 | "audio-recorder": false, 24 | "workspaces": true, 25 | "file-recovery": false, 26 | "publish": false, 27 | "sync": false, 28 | "canvas": true, 29 | "properties": false, 30 | "bookmarks": true 31 | } -------------------------------------------------------------------------------- /src/schemas/entry.schema.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | import { NumberLike } from "./common"; 3 | 4 | export const EntrySchema = z.strictObject({ 5 | date: z.string(), 6 | /** Absolute path to the file in the vault (if known). */ 7 | filePath: z.string().optional(), 8 | /** Custom href for this box; takes precedence over filePath. */ 9 | customHref: z.string().optional(), 10 | /** 11 | * This is the mapped intensity. 12 | * The user set intensity, then I recalculate intensity and write here new intensity. User's value write to `value`. 13 | */ 14 | intensity: NumberLike.optional(), 15 | /** 16 | * Initial user intensity (value). 17 | */ 18 | value: z.number().optional(), 19 | customColor: z.string().optional(), 20 | content: z.union([ 21 | z.string(), 22 | z.instanceof(HTMLElement) 23 | ]).optional(), 24 | }); -------------------------------------------------------------------------------- /src/styles/heatmap-tracker-days.scss: -------------------------------------------------------------------------------- 1 | /* Days */ 2 | .heatmap-tracker-days { 3 | text-align: end; 4 | white-space: nowrap; 5 | line-height: var(--heatmap-tracker-box-size); 6 | font-size: 0.65em; 7 | padding-right: 12px; 8 | padding-bottom: 12px; 9 | 10 | &__week-day:not(:last-child) { 11 | padding-bottom: var(--heatmap-tracker-box-gap); 12 | } 13 | 14 | &__filler { 15 | height: var(--heatmap-tracker-months-height); 16 | margin: 2px 0 4px; 17 | } 18 | 19 | &--even { 20 | div:nth-child(odd) { 21 | visibility: hidden; 22 | } 23 | } 24 | 25 | &--odd { 26 | div:nth-child(even) { 27 | visibility: hidden; 28 | } 29 | } 30 | 31 | &--all { 32 | div { 33 | visibility: visible; 34 | } 35 | } 36 | 37 | &--none { 38 | div { 39 | visibility: hidden; 40 | } 41 | } 42 | } -------------------------------------------------------------------------------- /src/styles/heatmap-tracker-week-nums.scss: -------------------------------------------------------------------------------- 1 | /* Week Numbers */ 2 | .heatmap-tracker-week-nums { 3 | display: grid; 4 | grid-auto-flow: column; 5 | grid-template-columns: repeat(53, var(--heatmap-tracker-box-size)); 6 | grid-column: 1; 7 | margin: 4px 0 2px; 8 | grid-gap: 0; 9 | justify-items: center; 10 | font-size: 0.8em; 11 | color: var(--text-muted); 12 | 13 | /* Match the gap from boxes */ 14 | column-gap: var(--heatmap-tracker-box-gap); 15 | } 16 | 17 | .heatmap-tracker-week-nums.separate-months { 18 | grid-template-columns: repeat(64, var(--heatmap-tracker-box-size)); 19 | } 20 | 21 | .heatmap-tracker-week-nums div { 22 | width: var(--heatmap-tracker-box-size); 23 | text-align: center; 24 | /* Ensure number is centered if box size is small */ 25 | display: flex; 26 | justify-content: center; 27 | align-items: center; 28 | } 29 | -------------------------------------------------------------------------------- /EXAMPLE_VAULT/Documentation with Examples/3. trackerData parameters/7. disableFileCreation.md: -------------------------------------------------------------------------------- 1 | Use `disableFileCreation: true` to disable file creation on empty heatmap box click. 2 | 3 | 4 | ```dataviewjs 5 | 6 | const trackerData = { 7 | year: 2024, 8 | entries: [], 9 | heatmapTitle: "Example: disables file creation on empty heatmap box click", 10 | /* ADD THIS */ 11 | disableFileCreation: true 12 | } 13 | 14 | const PATH_TO_FOLDER = '"daily notes"'; 15 | const PARAMETER_NAME = "steps"; 16 | 17 | for(let page of dv.pages(PATH_TO_FOLDER).where(p=>p[PARAMETER_NAME])){ 18 | trackerData.entries.push({ 19 | date: page.file.name, 20 | filePath: page.file.path, 21 | intensity: page[PARAMETER_NAME] 22 | }) 23 | } 24 | 25 | trackerData.basePath = "daily notes"; 26 | 27 | renderHeatmapTracker(this.container, trackerData) 28 | 29 | ``` -------------------------------------------------------------------------------- /EXAMPLE_VAULT/.obsidian/core-plugins.json: -------------------------------------------------------------------------------- 1 | { 2 | "file-explorer": true, 3 | "global-search": true, 4 | "switcher": false, 5 | "graph": false, 6 | "backlink": false, 7 | "outgoing-link": false, 8 | "tag-pane": false, 9 | "page-preview": true, 10 | "daily-notes": true, 11 | "templates": false, 12 | "note-composer": false, 13 | "command-palette": true, 14 | "slash-command": false, 15 | "editor-status": true, 16 | "starred": true, 17 | "markdown-importer": false, 18 | "zk-prefixer": false, 19 | "random-note": false, 20 | "outline": false, 21 | "word-count": false, 22 | "slides": false, 23 | "audio-recorder": false, 24 | "workspaces": true, 25 | "file-recovery": false, 26 | "publish": false, 27 | "sync": false, 28 | "canvas": true, 29 | "properties": false, 30 | "bookmarks": true, 31 | "webviewer": false, 32 | "footnotes": false, 33 | "bases": true 34 | } -------------------------------------------------------------------------------- /src/schemas/ui.schema.ts: -------------------------------------------------------------------------------- 1 | import { IHeatmapView } from "src/types"; 2 | import z from "zod"; 3 | 4 | export const UISchema = z.strictObject({ 5 | /** 6 | * Hides the tabs in the heatmap view. 7 | */ 8 | hideTabs: z.boolean().optional(), 9 | /** 10 | * Hides the year in the heatmap header. 11 | */ 12 | hideYear: z.boolean().optional(), 13 | /** 14 | * Hides the title in the heatmap header. 15 | */ 16 | hideTitle: z.boolean().optional(), 17 | /** 18 | * Hides the subtitle in the heatmap header. 19 | */ 20 | hideSubtitle: z.boolean().optional(), 21 | /** 22 | * Shows week numbers below the heatmap. 23 | */ 24 | showWeekNums: z.boolean().optional(), 25 | /** 26 | * The default view to show when opening the heatmap tracker. 27 | * Default: IHeatmapView.HeatmapTracker 28 | */ 29 | defaultView: z.enum(Object.values(IHeatmapView)).optional(), 30 | }); 31 | -------------------------------------------------------------------------------- /docs/add-new-language.md: -------------------------------------------------------------------------------- 1 | ## Adding a New Language 2 | 3 | Follow these steps to add a new language to the app: 4 | 5 | 1. **Create a new language file** 6 | In the `src/localization/locales/` folder, create a new file named `{LG}.json`, where `{LG}` is the language code (e.g., `fr` for French, `es` for Spanish). 7 | 8 | 2. **Copy English content** 9 | Copy the content from `en.json` into your new `{LG}.json` file. 10 | 11 | 3. **Translate the content** 12 | Replace the English strings in `{LG}.json` with translations in the desired language. 13 | 14 | 4. **Import the new language file** 15 | Open `src/localization/i18n.ts` and import your new `{LG}.json` file. 16 | 17 | 5. **Update the `resources` object** 18 | In `src/localization/i18n.ts`, extend the `resources` object to include the new language. 19 | 20 | 6. **Update the list of the languages** 21 | Add the new language entry to `src/localization/languages.json`. 22 | -------------------------------------------------------------------------------- /src/constants/defaultTrackerData.ts: -------------------------------------------------------------------------------- 1 | import { IHeatmapView, TrackerData } from "src/types"; 2 | import { getCurrentFullYear } from "src/utils/date"; 3 | 4 | export const DEFAULT_TRACKER_DATA: TrackerData = { 5 | year: getCurrentFullYear(), 6 | entries: [], 7 | showCurrentDayBorder: true, 8 | intensityConfig: { 9 | scaleStart: undefined, 10 | scaleEnd: undefined, 11 | defaultIntensity: 4, 12 | showOutOfRange: true, 13 | }, 14 | intensityScaleStart: undefined, 15 | intensityScaleEnd: undefined, 16 | defaultEntryIntensity: 4, 17 | colorScheme: { 18 | paletteName: "default", 19 | }, 20 | insights: [], 21 | disableFileCreation: false, 22 | heatmapTitle: undefined, 23 | heatmapSubtitle: undefined, 24 | basePath: undefined, 25 | ui: { 26 | defaultView: IHeatmapView.HeatmapTracker, 27 | hideTabs: false, 28 | hideYear: false, 29 | hideTitle: false, 30 | hideSubtitle: false, 31 | } 32 | }; 33 | -------------------------------------------------------------------------------- /EXAMPLE_VAULT/Examples/Habit Tracker/Exercise.md: -------------------------------------------------------------------------------- 1 | # Exercise Tracker (Boolean) 2 | 3 | This example tracks whether you exercised or not. It uses a simple boolean check. 4 | 5 | ## How it works 6 | - It looks for pages in the `"daily notes"` folder. 7 | - It checks if the `exercise` field is present and true. 8 | - Green means you exercised! 9 | 10 | ```dataviewjs 11 | const trackerData = { 12 | year: 2024, 13 | entries: [], 14 | heatmapTitle: "🏋️ Exercise Tracker", 15 | heatmapSubtitle: "Did I exercise today?", 16 | colorScheme: { 17 | paletteName: "default" // Green by default 18 | } 19 | } 20 | 21 | for(let page of dv.pages('"daily notes"').where(p => p.exercise)) { 22 | trackerData.entries.push({ 23 | date: page.file.name, 24 | intensity: 1 // 1 for true/done 25 | }) 26 | } 27 | 28 | renderHeatmapTracker(this.container, trackerData) 29 | ``` 30 | 31 | ## Sample Data (Copy to a daily note) 32 | ```yaml 33 | exercise: true 34 | ``` 35 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /EXAMPLE_VAULT/Github Issues/62. Does not change color when value is 0.md: -------------------------------------------------------------------------------- 1 | --- 2 | status: 3 | - done 4 | --- 5 | Link: https://github.com/mokkiebear/heatmap-tracker/issues/62 6 | 7 | **Expected**: 8 | - should display 5 different intensities in a row 9 | 10 | ```dataviewjs 11 | const trackerData = { 12 | year: 2025, 13 | intensityScaleStart: -2, 14 | intensityScaleEnd: 2, 15 | separateMonths: true, 16 | entries: [ 17 | { 18 | "date": "2025-04-14", 19 | "intensity": -2 20 | }, 21 | { 22 | "date": "2025-04-15", 23 | "intensity": -1 24 | }, 25 | { 26 | "date": "2025-04-16", 27 | "intensity": 0 28 | }, 29 | { 30 | "date": "2025-04-17", 31 | "intensity": 1 32 | }, 33 | { 34 | "date": "2025-04-18", 35 | "intensity": 2 36 | }, 37 | ] 38 | } 39 | 40 | renderHeatmapTracker(this.container, trackerData) 41 | 42 | ``` 43 | 44 | ```heatmap-tracker 45 | 46 | property: issue_62 47 | year: 2023 48 | 49 | ``` 50 | 51 | -------------------------------------------------------------------------------- /EXAMPLE_VAULT/Documentation with Examples/1. How to start?/0. I installed plugin, how to see chart?.md: -------------------------------------------------------------------------------- 1 | 1. Copy code below and add it to your page. 2 | 2. Modify code: 3 | 1. Set `PARAMETER_NAME` to the parameter you want to track. 4 | 2. Set `PATH_TO_FOLDER` to the folder with the pages you want to track. 5 | 3. That's all! 6 | 4. To learn how to use it as a pro and learn other features check examples and documentation. 7 | 8 | ```dataviewjs 9 | 10 | const trackerData = { 11 | year: 2024, 12 | entries: [], 13 | heatmapTitle: "Steps Example" 14 | } 15 | 16 | const PATH_TO_FOLDER = "daily notes"; 17 | const PARAMETER_NAME = "steps"; 18 | 19 | for(let page of dv.pages(`"${PATH_TO_FOLDER}"`).where(p=>p[PARAMETER_NAME])){ 20 | trackerData.entries.push({ 21 | date: page.file.name, 22 | filePath: page.file.path, 23 | intensity: page[PARAMETER_NAME] 24 | }) 25 | } 26 | 27 | trackerData.basePath = PATH_TO_FOLDER; 28 | 29 | renderHeatmapTracker(this.container, trackerData) 30 | 31 | ``` 32 | -------------------------------------------------------------------------------- /EXAMPLE_VAULT/Examples/Mood Tracker/Daily Mood.md: -------------------------------------------------------------------------------- 1 | # Daily Mood Tracker (1-5 Scale) 2 | 3 | This example tracks your daily mood on a scale of 1 to 5. 4 | 5 | ## How it works 6 | - It looks for pages in the `"daily notes"` folder. 7 | - It reads the `mood` field (number 1-5). 8 | - Colors range from low intensity (bad mood) to high intensity (good mood). 9 | 10 | ```dataviewjs 11 | const trackerData = { 12 | year: 2024, 13 | entries: [], 14 | heatmapTitle: "😊 Daily Mood", 15 | heatmapSubtitle: "1: 😢, 2: 😕, 3: 😐, 4: 🙂, 5: 🤩", 16 | intensityScaleStart: 1, 17 | intensityScaleEnd: 5, 18 | colorScheme: { 19 | paletteName: "winter" // Blue/Purple scale 20 | } 21 | } 22 | 23 | for(let page of dv.pages('"daily notes"').where(p => p.mood)) { 24 | trackerData.entries.push({ 25 | date: page.file.name, 26 | intensity: page.mood 27 | }) 28 | } 29 | 30 | renderHeatmapTracker(this.container, trackerData) 31 | ``` 32 | 33 | ## Sample Data (Copy to a daily note) 34 | ```yaml 35 | mood: 4 36 | ``` 37 | -------------------------------------------------------------------------------- /EXAMPLE_VAULT/Examples/Project Progress/Project Alpha.md: -------------------------------------------------------------------------------- 1 | # Project Alpha Progress (%) 2 | 3 | This example tracks the progress of a project as a percentage (0-100). 4 | 5 | ## How it works 6 | - It looks for pages in the `"daily notes"` folder. 7 | - It reads the `project_alpha` field (percentage). 8 | - Shows progress intensity. 9 | 10 | ```dataviewjs 11 | const trackerData = { 12 | year: 2024, 13 | entries: [], 14 | heatmapTitle: "🚀 Project Alpha Progress", 15 | heatmapSubtitle: "Daily progress percentage", 16 | intensityScaleStart: 0, 17 | intensityScaleEnd: 100, 18 | colorScheme: { 19 | paletteName: "warm" // Red/Orange/Yellow 20 | } 21 | } 22 | 23 | for(let page of dv.pages('"daily notes"').where(p => p.project_alpha)) { 24 | trackerData.entries.push({ 25 | date: page.file.name, 26 | intensity: page.project_alpha 27 | }) 28 | } 29 | 30 | renderHeatmapTracker(this.container, trackerData) 31 | ``` 32 | 33 | ## Sample Data (Copy to a daily note) 34 | ```yaml 35 | project_alpha: 75 36 | ``` 37 | -------------------------------------------------------------------------------- /src/components/HeatmapTab/HeatmapTab.tsx: -------------------------------------------------------------------------------- 1 | import { useHeatmapContext } from "src/context/heatmap/heatmap.context"; 2 | import { IHeatmapView } from "src/types"; 3 | import { memo } from "react"; 4 | import { TabIconForView } from "src/utils/tabs"; 5 | 6 | interface HeatmapTabProps { 7 | view: IHeatmapView; 8 | label: string; 9 | disabled?: boolean; 10 | } 11 | 12 | function HeatmapTab({ view, label, disabled }: HeatmapTabProps) { 13 | const { view: selectedView, setView, settings } = useHeatmapContext(); 14 | 15 | const isSelected = view === selectedView; 16 | 17 | function handleClick() { 18 | setView(view); 19 | } 20 | 21 | if (!settings.viewTabsVisibility[view]) { 22 | return null; 23 | } 24 | 25 | return ( 26 | 36 | ); 37 | } 38 | 39 | export default memo(HeatmapTab); 40 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # vscode 2 | .vscode 3 | 4 | # Intellij 5 | *.iml 6 | .idea 7 | 8 | # npm 9 | node_modules 10 | 11 | # ignore .obsidian folder in root directory of repo. it sometimes gets created by mistake. 12 | .obsidian 13 | !EXAMPLE_VAULT/.obsidian 14 | 15 | # Don't include the compiled main.js file in the repo. 16 | # They should be uploaded to GitHub releases instead. 17 | /main.js 18 | 19 | # Exclude sourcemaps 20 | *.map 21 | 22 | # obsidian 23 | data.json 24 | 25 | # Exclude macOS Finder (System Explorer) View States 26 | .DS_Store 27 | 28 | # exclude workspace.json, as it clutters up the git log as its contents change frequently 29 | EXAMPLE_VAULT/.obsidian/workspace.json 30 | 31 | # exclude plugin contents 32 | EXAMPLE_VAULT/.obsidian/plugins/* 33 | !EXAMPLE_VAULT/.obsidian/plugins/hot-reload 34 | # include heatmap-tracker plugin folder, but ignore contents except for .hotreload file 35 | !EXAMPLE_VAULT/.obsidian/plugins/heatmap-tracker 36 | EXAMPLE_VAULT/.obsidian/plugins/heatmap-tracker/* 37 | !EXAMPLE_VAULT/.obsidian/plugins/heatmap-tracker/.hotreload 38 | 39 | coverage 40 | build -------------------------------------------------------------------------------- /src/constants/defaultSettings.ts: -------------------------------------------------------------------------------- 1 | import { IHeatmapView, TrackerSettings } from "src/types"; 2 | 3 | export const DEFAULT_SETTINGS: TrackerSettings = { 4 | palettes: { 5 | default: ["#c6e48b", "#7bc96f", "#49af5d", "#2e8840", "#196127"], 6 | danger: ["#fff33b", "#fdc70c", "#f3903f", "#ed683c", "#e93e3a"], 7 | obsidianTheme: [ 8 | "var(--color-base-00)", 9 | "var(--color-base-05)", 10 | "var(--color-base-10)", 11 | "var(--color-base-20)", 12 | "var(--color-base-25)", 13 | "var(--color-base-30)", 14 | "var(--color-base-35)", 15 | "var(--color-base-40)", 16 | "var(--color-base-50)", 17 | "var(--color-base-60)", 18 | "var(--color-base-70)", 19 | "var(--color-base-100)", 20 | ], 21 | }, 22 | weekStartDay: 1, 23 | showWeekNums: false, 24 | weekDisplayMode: "even", 25 | separateMonths: true, 26 | language: "en", 27 | viewTabsVisibility: { 28 | [IHeatmapView.Documentation]: true, 29 | [IHeatmapView.HeatmapTracker]: true, 30 | [IHeatmapView.HeatmapTrackerStatistics]: true, 31 | [IHeatmapView.Legend]: true, 32 | }, 33 | }; 34 | -------------------------------------------------------------------------------- /EXAMPLE_VAULT/Start Here.md: -------------------------------------------------------------------------------- 1 | # Welcome to the Heatmap Tracker Example Vault! 2 | 3 | This vault is designed to help you understand and use the **Heatmap Tracker** plugin for Obsidian. 4 | 5 | ## 🚀 Getting Started 6 | 7 | 1. **Install Prerequisites**: Ensure you have both **Dataview** and **Heatmap Tracker** plugins installed and enabled in *Settings -> Community Plugins*. 8 | 2. **Explore Examples**: Check out the [[Examples]] folder to see different ways to use the tracker. 9 | 3. **Read the Docs**: The [[Documentation with Examples/index|Documentation]] folder contains detailed information on how to configure your trackers. 10 | 11 | ## 📂 Vault Structure 12 | 13 | - **[[Start Here]]**: This file. 14 | - **[[Showcase/Showcase|Showcase]]**: Comprehensive overview files showing what's possible. 15 | - **[[Documentation with Examples/index|Documentation]]**: Detailed guides on features and parameters. 16 | - **[[Examples/Examples|Examples]]**: Ready-to-use examples for various use cases (Habits, Mood, Projects, etc.). 17 | 18 | ## 💡 Quick Tip 19 | 20 | You can copy the code blocks from the examples directly into your own vault to get started quickly! 21 | 22 | --- 23 | *Happy Tracking!* 24 | -------------------------------------------------------------------------------- /EXAMPLE_VAULT/Documentation with Examples/3. trackerData parameters/9. intensityConfig.md: -------------------------------------------------------------------------------- 1 | # intensityConfig 2 | 3 | `intensityConfig` is an object that allows you to configure the intensity scale and default values. It replaces the deprecated `defaultEntryIntensity`, `intensityScaleStart`, and `intensityScaleEnd`. 4 | 5 | ## Properties 6 | 7 | - `scaleStart` (optional): The minimum value for the intensity scale. 8 | - `scaleEnd` (optional): The maximum value for the intensity scale. 9 | - `defaultIntensity` (required): The default intensity assigned to new data entries if no intensity is explicitly specified. 10 | - `showOutOfRange` (required): Whether to show values that are outside the defined scale. 11 | 12 | ## Example 13 | 14 | ```dataviewjs 15 | const trackerData = { 16 | entries: [ 17 | { date: "2025-01-01", intensity: 10 }, 18 | { date: "2025-01-02", intensity: 1 }, 19 | ], 20 | intensityConfig: { 21 | scaleStart: 1, 22 | scaleEnd: 10, 23 | defaultIntensity: 5, 24 | showOutOfRange: true 25 | }, 26 | heatmapTitle: "Heatmap with intensityConfig", 27 | } 28 | 29 | renderHeatmapTracker(this.container, trackerData); 30 | ``` 31 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | The heatmap-tracker plugin is actively maintained, and I support the **latest version** only. Older versions may work but will not receive security updates or bug fixes. 6 | 7 | | Version | Supported | 8 | | ------------ | ------------------ | 9 | | Latest (1.10.x or newer) | :white_check_mark: | 10 | | Older versions | :x: | 11 | 12 | > **Note:** While older versions are expected to function, I recommend always using the latest version to ensure you benefit from the latest features, improvements, and security updates. 13 | 14 | ## Reporting a Vulnerability 15 | 16 | To report a vulnerability, please follow these steps: 17 | 18 | 1. Submit an issue or contact me directly via [contact method/email/issue tracker]. 19 | 2. Expect an acknowledgment of your report within 48 hours. 20 | 3. Updates will be provided as the issue is investigated, typically within one week. 21 | 4. If the vulnerability is accepted, a fix will be implemented in the latest version, and you will be notified of the resolution. If declined, reasons will be provided. 22 | 23 | Thank you for helping improve the heatmap-tracker plugin! 24 | -------------------------------------------------------------------------------- /src/utils/__tests__/notify.spec.ts: -------------------------------------------------------------------------------- 1 | import { Notice } from "obsidian"; 2 | import { notify } from "../notify"; 3 | 4 | jest.mock("obsidian", () => ({ 5 | Notice: jest.fn(), 6 | })); 7 | 8 | const getMockedNotice = () => Notice as jest.MockedClass; 9 | 10 | describe("notify", () => { 11 | beforeEach(() => { 12 | getMockedNotice().mockClear(); 13 | }); 14 | 15 | it("should create a Notice with the provided message and duration", () => { 16 | const mockedNotice = {} as unknown as Notice; 17 | getMockedNotice().mockReturnValue(mockedNotice); 18 | 19 | const message = "Custom message"; 20 | const duration = 5000; 21 | const result = notify(message, duration); 22 | 23 | expect(getMockedNotice()).toHaveBeenCalledWith(message, duration); 24 | expect(result).toBe(mockedNotice); 25 | }); 26 | 27 | it("should fall back to the default duration when not provided", () => { 28 | const mockedNotice = {} as unknown as Notice; 29 | getMockedNotice().mockReturnValue(mockedNotice); 30 | 31 | const message = "Default duration message"; 32 | notify(message); 33 | 34 | expect(getMockedNotice()).toHaveBeenCalledWith(message, 3000); 35 | }); 36 | }); 37 | -------------------------------------------------------------------------------- /EXAMPLE_VAULT/Documentation with Examples/3. trackerData parameters/10. colorScheme.md: -------------------------------------------------------------------------------- 1 | # colorScheme 2 | 3 | `colorScheme` allows you to define the colors used in the heatmap. You can use a predefined palette or specify custom colors. 4 | 5 | ## Properties 6 | 7 | - `paletteName` (optional): The name of a predefined palette (e.g., "default", "github", "winter", "warm"). 8 | - `customColors` (optional): An array of hex color strings to use as the color scale. 9 | 10 | ## Example 1: Using a predefined palette 11 | 12 | ```dataviewjs 13 | const trackerData = { 14 | entries: [], 15 | colorScheme: { 16 | paletteName: "winter" 17 | }, 18 | heatmapTitle: "Winter Palette", 19 | } 20 | 21 | renderHeatmapTracker(this.container, trackerData); 22 | ``` 23 | 24 | ## Example 2: Using custom colors 25 | 26 | ```dataviewjs 27 | const trackerData = { 28 | entries: [], 29 | colorScheme: { 30 | customColors: [ 31 | "#ebedf0", // 0 intensity (background) 32 | "#9be9a8", 33 | "#40c463", 34 | "#30a14e", 35 | "#216e39" // Max intensity 36 | ] 37 | }, 38 | heatmapTitle: "Custom Colors (GitHub style)", 39 | } 40 | 41 | renderHeatmapTracker(this.container, trackerData); 42 | ``` 43 | -------------------------------------------------------------------------------- /src/components/HeatmapFooter/HeatmapFooter.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | import { ShieldXIcon } from "../icons/ShieldXIcon"; 3 | import { IHeatmapView } from "src/types"; 4 | import HeatmapTab from "../HeatmapTab/HeatmapTab"; 5 | import { useHeatmapContext } from "src/context/heatmap/heatmap.context"; 6 | 7 | function HeatmapFooter() { 8 | const { trackerData } = useHeatmapContext(); 9 | 10 | const [isActionRequired, setIsActionRequired] = useState(false); 11 | 12 | useEffect(() => { 13 | if ( 14 | (!isActionRequired && typeof (trackerData as any)?.colors === "string") || 15 | (trackerData as any)?.colors 16 | ) { 17 | setIsActionRequired(true); 18 | } 19 | }, [trackerData]); 20 | 21 | return ( 22 |
23 | {isActionRequired && ( 24 |
25 | 26 | Actions Required: 27 | 28 | Please check documentation and update heatmapTracker object 29 | 30 | 31 |
32 | )} 33 |
34 | ); 35 | } 36 | 37 | export default HeatmapFooter; 38 | -------------------------------------------------------------------------------- /update-version.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Exit on errors 4 | set -e 5 | 6 | # Check if version parameter is provided 7 | if [ -z "$1" ]; then 8 | echo "Usage: $0 " 9 | exit 1 10 | fi 11 | 12 | VERSION=$1 13 | 14 | # Step 1: Commit all changes 15 | echo "1. Committing all changes..." 16 | git add -A 17 | git commit -m "chore: commit all changes before version bump" || echo "No changes to commit." 18 | 19 | # Step 2: Update version in package.json using npm version 20 | echo "2. Updating package.json to version $VERSION..." 21 | npm version --no-git-tag-version $VERSION 22 | 23 | # Step 3: Run npm version script (if applicable) 24 | if npm run | grep -q 'version'; then 25 | echo "3. Running npm version script..." 26 | npm run version 27 | else 28 | echo "No npm version script found, skipping." 29 | fi 30 | 31 | # Step 4: Commit updated files 32 | echo "4. Committing updated files..." 33 | git add package.json package-lock.json 34 | git commit -m "chore(release): v$VERSION" 35 | 36 | # Step 5: Tag the new version 37 | echo "5. Creating git tag for version $VERSION..." 38 | git tag "$VERSION" 39 | 40 | # Step 6: Push changes and tags 41 | # echo "Pushing changes to the repository..." 42 | git push 43 | git push --tags 44 | 45 | echo "Version $VERSION updated and pushed successfully!" -------------------------------------------------------------------------------- /src/styles/heatmap-tracker-boxes.scss: -------------------------------------------------------------------------------- 1 | /* Boxes */ 2 | .heatmap-tracker-boxes { 3 | display: grid; 4 | grid-auto-flow: column; 5 | grid-template-columns: repeat(53, var(--heatmap-tracker-box-size)); 6 | grid-template-rows: repeat(7, var(--heatmap-tracker-box-size)); 7 | grid-column: 1; 8 | row-gap: var(--heatmap-tracker-box-gap); 9 | column-gap: var(--heatmap-tracker-box-gap); 10 | } 11 | 12 | /* Separate Months */ 13 | 14 | .heatmap-tracker-boxes.separate-months { 15 | grid-auto-flow: column; 16 | grid-template-columns: repeat(64, var(--heatmap-tracker-box-size)); 17 | } 18 | 19 | .heatmap-tracker-boxes .space-between-box { 20 | background-color: transparent; 21 | 22 | &:hover { 23 | transform: unset; 24 | border: unset; 25 | cursor: default; 26 | } 27 | } 28 | 29 | .heatmap-tracker-boxes .internal-link { 30 | text-decoration: none; 31 | position: absolute; 32 | width: 100%; 33 | height: 100%; 34 | text-align: center; 35 | } 36 | 37 | .heatmap-tracker-boxes .today { 38 | } 39 | 40 | .heatmap-tracker-boxes .with-border { 41 | border: solid var(--border-width) rgb(61, 61, 61); 42 | } 43 | 44 | .theme-dark .heatmap-tracker-boxes .with-border { 45 | border: solid var(--border-width) white; 46 | } 47 | 48 | .theme-dark .heatmap-tracker-boxes .isEmpty { 49 | background: #333; 50 | } -------------------------------------------------------------------------------- /EXAMPLE_VAULT/Documentation with Examples/4. Insights/1. Days Achieved Step Goal (8,000 steps).md: -------------------------------------------------------------------------------- 1 | ``` 2 | { 3 | name: "Days Achieved Step Goal (8,000 steps)", 4 | calculate: ({ yearEntries }) => { 5 | const stepGoal = 8000; 6 | const daysAchieved = yearEntries.filter((entry) => entry.value >= stepGoal).length; 7 | return daysAchieved.toString(); 8 | }, 9 | } 10 | ``` 11 | 12 | ```dataviewjs 13 | 14 | const trackerData = { 15 | year: 2025, // optional, remove this line to autoswitch year 16 | entries: [], 17 | heatmapTitle: "👣 Steps Tracker 👣", 18 | insights: [{ 19 | name: "Days Achieved Step Goal (8,000 steps)", 20 | calculate: ({ yearEntries }) => { 21 | const stepGoal = 8000; 22 | const daysAchieved = yearEntries.filter((entry) => entry.value >= stepGoal).length; 23 | return daysAchieved.toString(); 24 | }, 25 | }] 26 | } 27 | 28 | 29 | for(let page of dv.pages('"daily notes"').where(p=>p.steps)){ 30 | 31 | trackerData.entries.push({ 32 | date: page.file.name, 33 | filePath: page.file.path, 34 | intensity: page.steps, 35 | }) 36 | } 37 | 38 | trackerData.basePath = 'daily notes'; 39 | 40 | renderHeatmapTrackerStatistics(this.container, trackerData) 41 | renderHeatmapTracker(this.container, trackerData) 42 | 43 | ``` 44 | -------------------------------------------------------------------------------- /EXAMPLE_VAULT/Documentation with Examples/2. Features/Display statistics separately.md: -------------------------------------------------------------------------------- 1 | Since `1.19.2` use `ui` and `defaultView` to display statistics separately. Example: 2 | ```dataviewjs 3 | 4 | var trackerData = { 5 | year: 2024, // optional, remove this line to autoswitch year 6 | entries: [], 7 | heatmapTitle: "👣 Steps Tracker 👣" 8 | } 9 | 10 | 11 | for(let page of dv.pages('"daily notes"').where(p=>p.steps)){ 12 | 13 | trackerData.entries.push({ 14 | date: page.file.name, 15 | filePath: page.file.path, 16 | intensity: page.steps 17 | }) 18 | } 19 | 20 | trackerData.basePath = 'daily notes'; 21 | 22 | renderHeatmapTracker(this.container, trackerData) 23 | ``` 24 | 25 | ```dataviewjs 26 | 27 | var trackerData = { 28 | year: 2024, // optional, remove this line to autoswitch year 29 | entries: [], 30 | heatmapTitle: "👣 Steps Tracker 👣", 31 | ui: { 32 | defaultView: 'heatmap-tracker-statistics', 33 | hideTabs: true, 34 | hideSubtitle: true 35 | } 36 | } 37 | 38 | 39 | for(let page of dv.pages('"daily notes"').where(p=>p.steps)){ 40 | 41 | trackerData.entries.push({ 42 | date: page.file.name, 43 | filePath: page.file.path, 44 | intensity: page.steps 45 | }) 46 | } 47 | 48 | trackerData.basePath = 'daily notes'; 49 | 50 | renderHeatmapTracker(this.container, trackerData) 51 | ``` -------------------------------------------------------------------------------- /EXAMPLE_VAULT/Documentation with Examples/3. trackerData parameters/1. heatmapTitle.md: -------------------------------------------------------------------------------- 1 | To add a title to your heatmap use property `heatmapTitle`. 2 | 3 | ```dataviewjs 4 | 5 | const trackerData = { 6 | year: 2024, 7 | entries: [], 8 | heatmapTitle: "This is a title 😊" 9 | } 10 | 11 | const PATH_TO_FOLDER = '"daily notes"'; 12 | const PARAMETER_NAME = "steps"; 13 | 14 | for(let page of dv.pages(PATH_TO_FOLDER).where(p=>p[PARAMETER_NAME])){ 15 | trackerData.entries.push({ 16 | date: page.file.name, 17 | filePath: page.file.path, 18 | intensity: page[PARAMETER_NAME] 19 | }) 20 | } 21 | 22 | trackerData.basePath = "daily notes"; 23 | 24 | renderHeatmapTracker(this.container, trackerData) 25 | 26 | ``` 27 | 28 | You also can use `html` to style title: 29 | ```dataviewjs 30 | 31 | const trackerData = { 32 | year: 2024, 33 | entries: [], 34 | heatmapTitle: "This is a title 😊" 35 | } 36 | 37 | const PATH_TO_FOLDER = '"daily notes"'; 38 | const PARAMETER_NAME = "steps"; 39 | 40 | for(let page of dv.pages(PATH_TO_FOLDER).where(p=>p[PARAMETER_NAME])){ 41 | trackerData.entries.push({ 42 | date: page.file.name, 43 | filePath: page.file.path, 44 | intensity: page[PARAMETER_NAME] 45 | }) 46 | } 47 | 48 | trackerData.basePath = "daily notes"; 49 | 50 | renderHeatmapTracker(this.container, trackerData) 51 | 52 | ``` 53 | -------------------------------------------------------------------------------- /ROADMAP.md: -------------------------------------------------------------------------------- 1 | # 📍 Heatmap Plugin Roadmap 2 | 3 | This roadmap outlines planned features and improvements for the Heatmap plugin. Suggestions and contributions are always welcome! 😊 4 | 5 | --- 6 | 7 | ## 🛠️ Planned Features and Improvements 8 | 9 | ### 1. 📚 **Documentation** 10 | - ✍️ Update the README to provide clearer instructions and examples. 11 | 12 | ### 2. 🎨 **Visual Interface** 13 | - 🖼️ Add a user-friendly interface for creating and customizing heatmaps. 14 | 15 | ### 3. 📊 **Enhanced Metrics** 16 | - 📈 Incorporate advanced statistical metrics for deeper insights. 17 | 18 | ### 4. 🌍 **Localization** 19 | - 🌐 Add support for more languages to reach a global audience. 20 | 21 | ### 5. ✅ **Improved Testing** 22 | - 🧪 Expand test coverage to ensure plugin stability and reliability. 23 | 24 | ### 6. 🛠️ **Customization Options** 25 | - 🖋️ Allow users to change font styles, layout, and other UI elements. 26 | 27 | ### 7. 🌟 **New Views** 28 | - 🗓️ **Calendar View**: Visualize data on a calendar interface. 29 | - ⚙️ **Settings View**: A dedicated section for configuring plugin options. 30 | - 🏆 **Achievements View**: Track user milestones and progress. 31 | - ☕ **Buy Me a Coffee View**: Show appreciation and support the developer. 32 | 33 | --- 34 | 35 | ## 🚀 Got Ideas? 36 | Feel free to open an issue or submit a pull request if you have suggestions or want to contribute to the roadmap. Let's build this together! 🙌 37 | -------------------------------------------------------------------------------- /src/components/HeatmapWeekDays/HeatmapWeekDays.tsx: -------------------------------------------------------------------------------- 1 | import { useMemo } from "react"; 2 | import { useTranslation } from "react-i18next"; 3 | import { useHeatmapContext } from "src/context/heatmap/heatmap.context"; 4 | import { getShiftedWeekdays } from "src/utils/date"; 5 | 6 | export function HeatmapWeekDays() { 7 | const { settings } = useHeatmapContext(); 8 | const { t, i18n } = useTranslation(); 9 | 10 | const weekDays = useMemo(() => { 11 | return getShiftedWeekdays( 12 | [ 13 | t("weekdaysShort.Sunday"), 14 | t("weekdaysShort.Monday"), 15 | t("weekdaysShort.Tuesday"), 16 | t("weekdaysShort.Wednesday"), 17 | t("weekdaysShort.Thursday"), 18 | t("weekdaysShort.Friday"), 19 | t("weekdaysShort.Saturday"), 20 | ], 21 | settings.weekStartDay 22 | ); 23 | }, [settings.weekStartDay, i18n.language]); 24 | 25 | const classNames = useMemo(() => { 26 | return `heatmap-tracker-days heatmap-tracker-days--${settings.weekDisplayMode}`; 27 | }, [settings.weekDisplayMode]); 28 | 29 | return ( 30 |
31 | {/* This empty filler is needed to position the week days correctly */} 32 |
33 | {weekDays.map((day) => ( 34 |
35 | {day} 36 |
37 | ))} 38 |
39 | ); 40 | } 41 | -------------------------------------------------------------------------------- /EXAMPLE_VAULT/Documentation with Examples/3. trackerData parameters/11. entries.md: -------------------------------------------------------------------------------- 1 | # entries 2 | 3 | `entries` is an array of objects representing the data points on the heatmap. 4 | 5 | ## Entry Properties 6 | 7 | - `date` (required): The date of the entry in "YYYY-MM-DD" format. 8 | - `intensity` (optional): The numeric value representing the intensity. 9 | - `content` (optional): Text or HTML content to display in the tooltip or box. 10 | - `customColor` (optional): A specific hex color for this entry, overriding the intensity color. 11 | - `filePath` (optional): Absolute path to a file to open when clicked. 12 | - `customHref` (optional): A custom URL to open when clicked (takes precedence over `filePath`). 13 | 14 | ## Example 15 | 16 | ```dataviewjs 17 | const trackerData = { 18 | entries: [ 19 | { 20 | date: "2025-01-01", 21 | intensity: 5, 22 | content: "Happy New Year!" 23 | }, 24 | { 25 | date: "2025-01-05", 26 | intensity: 3, 27 | customColor: "#ff0000", // Red color for this specific day 28 | content: "Important event" 29 | }, 30 | { 31 | date: "2025-01-10", 32 | intensity: 8, 33 | customHref: "https://google.com", // Opens Google 34 | content: "Link to Google" 35 | } 36 | ], 37 | heatmapTitle: "Entries Example", 38 | } 39 | 40 | renderHeatmapTracker(this.container, trackerData); 41 | ``` 42 | -------------------------------------------------------------------------------- /src/views/HeatmapTrackerView/HeatmapTrackerView.tsx: -------------------------------------------------------------------------------- 1 | import { useRef, useState } from "react"; 2 | import { useEffect } from "react"; 3 | import { HeatmapBoxesList } from "src/components/HeatmapBoxesList/HeatmapBoxesList"; 4 | import { HeatmapMonthsList } from "src/components/HeatmapMonthsList/HeatmapMonthsList"; 5 | import { HeatmapWeekDays } from "src/components/HeatmapWeekDays/HeatmapWeekDays"; 6 | import { HeatmapWeekNums } from "src/components/HeatmapWeekNums/HeatmapWeekNums"; 7 | import { useHeatmapContext } from "src/context/heatmap/heatmap.context"; 8 | 9 | function HeatmapTrackerView() { 10 | const { boxes } = useHeatmapContext(); 11 | 12 | const graphRef = useRef(null); 13 | const [isLoading, setIsLoading] = useState(true); 14 | 15 | useEffect(() => { 16 | graphRef.current?.scrollTo?.({ 17 | top: 0, 18 | left: 19 | (graphRef.current?.querySelector(".today") as HTMLElement)?.offsetLeft - 20 | graphRef.current?.offsetWidth / 2, 21 | }); 22 | 23 | setIsLoading(false); 24 | }, [boxes]); 25 | 26 | return ( 27 |
32 | 33 |
34 | 35 | 36 | 37 | 38 |
39 |
40 | ); 41 | } 42 | 43 | export default HeatmapTrackerView; 44 | -------------------------------------------------------------------------------- /EXAMPLE_VAULT/Documentation with Examples/2. Features/Display legend separately.md: -------------------------------------------------------------------------------- 1 | 2 | Since `1.19.2` use `ui` and `defaultView` to display legend separately. Example: 3 | 4 | ```dataviewjs 5 | 6 | var trackerData = { 7 | year: 2024, // optional, remove this line to autoswitch year 8 | entries: [], 9 | heatmapTitle: "👣 Steps Tracker 👣", 10 | } 11 | 12 | const PATH_TO_FOLDER = "daily notes"; 13 | 14 | for(let page of dv.pages(`"${PATH_TO_FOLDER}"`).where(p=>p.steps)){ 15 | trackerData.entries.push({ 16 | date: page.file.name, 17 | filePath: page.file.path, 18 | intensity: page.steps 19 | }) 20 | } 21 | 22 | trackerData.basePath = PATH_TO_FOLDER; 23 | 24 | renderHeatmapTracker(this.container, trackerData); 25 | renderHeatmapTrackerLegend(this.container, trackerData); 26 | ``` 27 | 28 | ```dataviewjs 29 | 30 | var trackerData = { 31 | year: 2024, // optional, remove this line to autoswitch year 32 | entries: [], 33 | heatmapTitle: "👣 Steps Tracker 👣", 34 | ui: { 35 | defaultView: 'legend', 36 | hideTabs: true, 37 | hideSubtitle: true 38 | } 39 | } 40 | 41 | const PATH_TO_FOLDER = "daily notes"; 42 | 43 | for(let page of dv.pages(`"${PATH_TO_FOLDER}"`).where(p=>p.steps)){ 44 | trackerData.entries.push({ 45 | date: page.file.name, 46 | filePath: page.file.path, 47 | intensity: page.steps 48 | }) 49 | } 50 | 51 | trackerData.basePath = PATH_TO_FOLDER; 52 | 53 | renderHeatmapTracker(this.container, trackerData) 54 | ``` -------------------------------------------------------------------------------- /EXAMPLE_VAULT/Documentation with Examples/3. trackerData parameters/5. showCurrentDayBorder.md: -------------------------------------------------------------------------------- 1 | Use `showCurrentDayBorder` to higlight current day on the heatmap. 2 | 3 | 4 | > [!NOTE] Default value 5 | > `showCurrentDayBorder` is `true` by default. So, set to `false` if you don't want to highlight current day 6 | 7 | ```dataviewjs 8 | 9 | const trackerData = { 10 | entries: [], 11 | heatmapTitle: "showCurrentDayBorder is enabled", 12 | showCurrentDayBorder: true 13 | } 14 | 15 | const PATH_TO_FOLDER = '"daily notes"'; 16 | const PARAMETER_NAME = "steps"; 17 | 18 | for(let page of dv.pages(PATH_TO_FOLDER).where(p=>p[PARAMETER_NAME])){ 19 | trackerData.entries.push({ 20 | date: page.file.name, 21 | filePath: page.file.path, 22 | intensity: page[PARAMETER_NAME] 23 | }) 24 | } 25 | 26 | trackerData.basePath = "daily notes"; 27 | 28 | renderHeatmapTracker(this.container, trackerData) 29 | 30 | ``` 31 | ```dataviewjs 32 | 33 | const trackerData = { 34 | entries: [], 35 | heatmapTitle: "showCurrentDayBorder is disabled", 36 | showCurrentDayBorder: false 37 | } 38 | 39 | const PATH_TO_FOLDER = '"daily notes"'; 40 | const PARAMETER_NAME = "steps"; 41 | 42 | for(let page of dv.pages(PATH_TO_FOLDER).where(p=>p[PARAMETER_NAME])){ 43 | trackerData.entries.push({ 44 | date: page.file.name, 45 | filePath: page.file.path, 46 | intensity: page[PARAMETER_NAME] 47 | }) 48 | } 49 | 50 | trackerData.basePath = "daily notes"; 51 | 52 | renderHeatmapTracker(this.container, trackerData) 53 | 54 | ``` 55 | -------------------------------------------------------------------------------- /EXAMPLE_VAULT/Documentation with Examples/3. trackerData parameters/3. year.md: -------------------------------------------------------------------------------- 1 | By default if you don't set `year` property the current year will be displayed. 2 | 3 | ```dataviewjs 4 | 5 | const trackerData = { 6 | entries: [], 7 | heatmapTitle: "The current year is displayed" 8 | } 9 | 10 | const PATH_TO_FOLDER = '"daily notes"'; 11 | const PARAMETER_NAME = "steps"; 12 | 13 | for(let page of dv.pages(PATH_TO_FOLDER).where(p=>p[PARAMETER_NAME])){ 14 | trackerData.entries.push({ 15 | date: page.file.name, 16 | filePath: page.file.path, 17 | intensity: page[PARAMETER_NAME] 18 | }) 19 | } 20 | 21 | trackerData.basePath = "daily notes"; 22 | 23 | renderHeatmapTracker(this.container, trackerData) 24 | 25 | ``` 26 | 27 | But if you want to see data by specific year (by default), you can set `year` property. In example below I set `year` to `2021`. 28 | 29 | ```dataviewjs 30 | 31 | const trackerData = { 32 | year: 2021, 33 | entries: [], 34 | heatmapTitle: "Data for 2021" 35 | } 36 | 37 | const PATH_TO_FOLDER = '"daily notes"'; 38 | const PARAMETER_NAME = "steps"; 39 | 40 | for(let page of dv.pages(PATH_TO_FOLDER).where(p=>p[PARAMETER_NAME])){ 41 | trackerData.entries.push({ 42 | date: page.file.name, 43 | filePath: page.file.path, 44 | intensity: page[PARAMETER_NAME] 45 | }) 46 | } 47 | 48 | trackerData.basePath = "daily notes"; 49 | 50 | renderHeatmapTracker(this.container, trackerData) 51 | 52 | ``` 53 | 54 | > [!Why should you use year property?] 55 | > You have arrows to move from one year to another, but setting `year` property helps to open required year by default. 56 | -------------------------------------------------------------------------------- /EXAMPLE_VAULT/Documentation with Examples/3. trackerData parameters/4. separateMonths.md: -------------------------------------------------------------------------------- 1 | You can set `separateMonths` to `true` to separate months on the heatmap. 2 | 3 | 4 | ```dataviewjs 5 | 6 | const trackerData = { 7 | entries: [], 8 | heatmapTitle: "Example: separateMonths: true", 9 | separateMonths: true 10 | } 11 | 12 | const PATH_TO_FOLDER = '"daily notes"'; 13 | const PARAMETER_NAME = "steps"; 14 | 15 | for(let page of dv.pages(PATH_TO_FOLDER).where(p=>p[PARAMETER_NAME])){ 16 | trackerData.entries.push({ 17 | date: page.file.name, 18 | filePath: page.file.path, 19 | intensity: page[PARAMETER_NAME] 20 | }) 21 | } 22 | 23 | trackerData.basePath = "daily notes"; 24 | 25 | renderHeatmapTracker(this.container, trackerData) 26 | 27 | ``` 28 | 29 | ```dataviewjs 30 | 31 | const trackerData = { 32 | entries: [], 33 | heatmapTitle: "Example: separateMonths: false", 34 | separateMonths: false 35 | } 36 | 37 | const PATH_TO_FOLDER = '"daily notes"'; 38 | const PARAMETER_NAME = "steps"; 39 | 40 | for(let page of dv.pages(PATH_TO_FOLDER).where(p=>p[PARAMETER_NAME])){ 41 | trackerData.entries.push({ 42 | date: page.file.name, 43 | filePath: page.file.path, 44 | intensity: page[PARAMETER_NAME] 45 | }) 46 | } 47 | 48 | trackerData.basePath = "daily notes"; 49 | 50 | renderHeatmapTracker(this.container, trackerData) 51 | 52 | ``` 53 | 54 | 55 | > [!Note] Default value in Heatmap Tracker Plugin settings 56 | > In Heatmap Tracker Plugin settings you can set default value from `separateMonths` property to enable/disable it for all your heatmaps. 57 | -------------------------------------------------------------------------------- /src/schemas/trackerData.schema.ts: -------------------------------------------------------------------------------- 1 | import z from "zod"; 2 | 3 | import { EntrySchema } from "./entry.schema"; 4 | import { IntensityConfigSchema } from "./intensityConfig.schema"; 5 | import { InsightSchema } from "./insight.schema"; 6 | import { ColorSchemeSchema } from "./colorScheme.schema"; 7 | import { UISchema } from "./ui.schema"; 8 | 9 | // TODO: change to strict when I know how to handle `property` and `path`. 10 | // Issue: https://github.com/mokkiebear/heatmap-tracker/issues/64 11 | export const TrackerDataSchema = z.object({ 12 | year: z.number(), 13 | colorScheme: ColorSchemeSchema, 14 | entries: z.array(EntrySchema), 15 | showCurrentDayBorder: z.boolean(), 16 | /** Base folder used to collect entries (if applicable). */ 17 | basePath: z.string().optional(), 18 | 19 | /** 20 | * @deprecated The default intensity value for an entry. 21 | */ 22 | defaultEntryIntensity: z.number(), 23 | /** 24 | * @deprecated The starting value for the intensity scale. 25 | */ 26 | intensityScaleStart: z.number().optional(), 27 | /** 28 | * @deprecated The ending value for the intensity scale. 29 | */ 30 | intensityScaleEnd: z.number().optional(), 31 | 32 | intensityConfig: IntensityConfigSchema, 33 | separateMonths: z.boolean().optional(), 34 | heatmapTitle: z.string().or(z.number()).optional(), 35 | heatmapSubtitle: z.string().or(z.number()).optional(), 36 | insights: z.array(InsightSchema), 37 | /** 38 | * Disables the creation of a new file when clicking on a heatmap box that doesn't have a corresponding file. 39 | */ 40 | disableFileCreation: z.boolean().optional(), 41 | ui: UISchema.optional(), 42 | }); 43 | -------------------------------------------------------------------------------- /EXAMPLE_VAULT/Documentation with Examples/3. trackerData parameters/2. heatmapSubtitle (Description).md: -------------------------------------------------------------------------------- 1 | To add subtitle or description to your heatmap use property `heatmapSubtitle`. 2 | 3 | ```dataviewjs 4 | 5 | const trackerData = { 6 | year: 2024, 7 | entries: [], 8 | heatmapTitle: "This is a title", 9 | heatmapSubtitle: "This is a subtitile. You can also use it as a description to add details about what you're tracking" 10 | } 11 | 12 | const PATH_TO_FOLDER = '"daily notes"'; 13 | const PARAMETER_NAME = "steps"; 14 | 15 | for(let page of dv.pages(PATH_TO_FOLDER).where(p=>p[PARAMETER_NAME])){ 16 | trackerData.entries.push({ 17 | date: page.file.name, 18 | filePath: page.file.path, 19 | intensity: page[PARAMETER_NAME] 20 | }) 21 | } 22 | 23 | trackerData.basePath = "daily notes"; 24 | 25 | renderHeatmapTracker(this.container, trackerData) 26 | 27 | ``` 28 | You also can use `html` to style `heatmapSubtitle`: 29 | 30 | ```dataviewjs 31 | 32 | const trackerData = { 33 | year: 2024, 34 | entries: [], 35 | heatmapTitle: "This is a title", 36 | heatmapSubtitle: "This is a subtitile. You can also use it as a description to add details about what you're tracking" 37 | } 38 | 39 | const PATH_TO_FOLDER = '"daily notes"'; 40 | const PARAMETER_NAME = "steps"; 41 | 42 | for(let page of dv.pages(PATH_TO_FOLDER).where(p=>p[PARAMETER_NAME])){ 43 | trackerData.entries.push({ 44 | date: page.file.name, 45 | filePath: page.file.path, 46 | intensity: page[PARAMETER_NAME] 47 | }) 48 | } 49 | 50 | trackerData.basePath = "daily notes"; 51 | 52 | renderHeatmapTracker(this.container, trackerData) 53 | -------------------------------------------------------------------------------- /src/views/LegendView/LegendView.tsx: -------------------------------------------------------------------------------- 1 | import { useHeatmapContext } from "src/context/heatmap/heatmap.context"; 2 | import { getIntensitiesInfo, getEntriesIntensities } from "src/utils/intensity"; 3 | 4 | function LegendView() { 5 | const { trackerData, colorsList, intensityConfig } = useHeatmapContext(); 6 | 7 | const intensities = getEntriesIntensities(trackerData.entries); 8 | 9 | const intensitiesInfo = getIntensitiesInfo( 10 | intensities, 11 | intensityConfig, 12 | colorsList ?? [] 13 | ); 14 | 15 | return ( 16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | {intensitiesInfo.map((intensityInfo, index) => ( 27 | 28 | 29 | 32 | 43 | 44 | ))} 45 | 46 |
IntensityRangeColor
{intensityInfo.intensity} 30 | {intensityInfo.min.toFixed(2)} - {intensityInfo.max.toFixed(2)} 31 | 33 |
41 | {colorsList[index]} 42 |
47 |
48 | ); 49 | } 50 | 51 | export default LegendView; 52 | -------------------------------------------------------------------------------- /src/localization/i18n.ts: -------------------------------------------------------------------------------- 1 | import i18n from 'i18next'; 2 | import { initReactI18next } from 'react-i18next'; 3 | import en from './locales/en.json'; 4 | import ru from './locales/ru.json'; 5 | import de from './locales/de.json'; 6 | import es from './locales/es.json'; 7 | import fr from './locales/fr.json'; 8 | import hi from './locales/hi.json'; 9 | import zh from './locales/zh.json'; 10 | import pt from './locales/pt.json'; 11 | import pl from './locales/pl.json'; 12 | 13 | import languages from './languages.json'; 14 | 15 | // don't want to use this? 16 | // have a look at the Quick start guide 17 | // for passing in lng and translations on init 18 | 19 | i18n 20 | // pass the i18n instance to react-i18next. 21 | .use(initReactI18next) 22 | // init i18next 23 | // for all options read: https://www.i18next.com/overview/configuration-options 24 | .init({ 25 | fallbackLng: 'en', 26 | debug: false, 27 | 28 | interpolation: { 29 | escapeValue: false, // not needed for react as it escapes by default 30 | }, 31 | supportedLngs: Object.keys(languages), 32 | resources: { 33 | en: { 34 | translation: en, 35 | }, 36 | ru: { 37 | translation: ru, 38 | }, 39 | de: { 40 | translation: de, 41 | }, 42 | es: { 43 | translation: es, 44 | }, 45 | fr: { 46 | translation: fr, 47 | }, 48 | pt: { 49 | translation: pt, 50 | }, 51 | pl: { 52 | translation: pl, 53 | }, 54 | hi: { 55 | translation: hi, 56 | }, 57 | zh: { 58 | translation: zh, 59 | }, 60 | }, 61 | }); 62 | 63 | 64 | export default i18n; 65 | -------------------------------------------------------------------------------- /EXAMPLE_VAULT/Documentation with Examples/index.md: -------------------------------------------------------------------------------- 1 | # Documentation 2 | 3 | ## 🚀 Getting Started 4 | - [[1. How to start?/0. I installed plugin, how to see chart?|How to see the chart?]] 5 | - [[1. How to start?/1. How to add a heatmap to your obsidian page?|How to add a heatmap?]] 6 | 7 | ## ⚙️ Configuration 8 | - [[3. trackerData parameters/1. heatmapTitle|Heatmap Title]] 9 | - [[3. trackerData parameters/2. heatmapSubtitle (Description)|Subtitle]] 10 | - [[3. trackerData parameters/3. year|Year]] 11 | - [[3. trackerData parameters/4. separateMonths|Separate Months]] 12 | - [[3. trackerData parameters/5. showCurrentDayBorder|Current Day Border]] 13 | - [[3. trackerData parameters/6. insights|Insights]] 14 | - [[3. trackerData parameters/7. disableFileCreation|Disable File Creation]] 15 | 16 | ## ✨ Features 17 | - [[2. Features/Display Legend under Heatmap|Display Legend under Heatmap]] 18 | - [[2. Features/Display legend separately|Display Legend Separately]] 19 | - [[2. Features/Display statistics separately|Display Statistics Separately]] 20 | 21 | ## 📊 Insights Examples 22 | - [[4. Insights/1. Days Achieved Step Goal (8,000 steps)|Days Achieved Goal]] 23 | - [[4. Insights/2. Longest Streak of Meeting Step Goal|Longest Streak]] 24 | - [[4. Insights/3. Total Pages Read|Total Pages Read]] 25 | - [[4. Insights/4. Most Pages Read in a Single Day|Most Pages Read]] 26 | - [[4. Insights/5. Average Headache Intensity|Average Headache Intensity]] 27 | - [[4. Insights/6. Total Hours Slept|Total Hours Slept]] 28 | - [[4. Insights/7. Average Sleep Per Night|Average Sleep Per Night]] 29 | - [[4. Insights/8. Activity Intensity by Day|Activity Intensity by Day]] 30 | 31 | ## 💡 Usage Examples 32 | Check the [[Examples]] folder for complete copy-paste examples! -------------------------------------------------------------------------------- /EXAMPLE_VAULT/Documentation with Examples/4. Insights/2. Longest Streak of Meeting Step Goal.md: -------------------------------------------------------------------------------- 1 | ``` 2 | { 3 | name: "Longest Streak of Meeting Step Goal", 4 | calculate: ({ yearEntries }) => { 5 | const stepGoal = 5000; 6 | let streak = 0, maxStreak = 0; 7 | 8 | yearEntries.forEach((entry) => { 9 | if (entry.value >= stepGoal) { 10 | streak++; 11 | maxStreak = Math.max(maxStreak, streak); 12 | } else { 13 | streak = 0; 14 | } 15 | }); 16 | 17 | return maxStreak.toString(); 18 | }, 19 | } 20 | ``` 21 | 22 | ```dataviewjs 23 | 24 | const trackerData = { 25 | year: 2025, // optional, remove this line to autoswitch year 26 | entries: [], 27 | heatmapTitle: "👣 Steps Tracker 👣", 28 | insights: [{ 29 | name: "Longest Streak of Meeting Step Goal (5,000 steps)", 30 | calculate: ({ yearEntries }) => { 31 | const stepGoal = 5000; 32 | let streak = 0, maxStreak = 0; 33 | 34 | yearEntries.forEach((entry) => { 35 | if (entry.value >= stepGoal) { 36 | streak++; 37 | maxStreak = Math.max(maxStreak, streak); 38 | } else { 39 | streak = 0; 40 | } 41 | }); 42 | 43 | return maxStreak.toString(); 44 | }, 45 | }] 46 | } 47 | 48 | 49 | for(let page of dv.pages('"daily notes"').where(p=>p.steps)){ 50 | 51 | trackerData.entries.push({ 52 | date: page.file.name, 53 | filePath: page.file.path, 54 | intensity: page.steps, 55 | }) 56 | } 57 | 58 | trackerData.basePath = 'daily notes'; 59 | 60 | renderHeatmapTrackerStatistics(this.container, trackerData) 61 | renderHeatmapTracker(this.container, trackerData) 62 | 63 | ``` 64 | -------------------------------------------------------------------------------- /src/components/HeatmapWeekNums/HeatmapWeekNums.tsx: -------------------------------------------------------------------------------- 1 | import { useHeatmapContext } from "src/context/heatmap/heatmap.context"; 2 | import { getISOWeekNumber } from "src/utils/date"; 3 | 4 | export function HeatmapWeekNums() { 5 | const { trackerData, boxes, settings } = useHeatmapContext(); 6 | 7 | const showWeekNums = trackerData.ui?.showWeekNums ?? settings.showWeekNums; 8 | 9 | if (!showWeekNums) { 10 | return null; 11 | } 12 | 13 | const columns = []; 14 | // The grid fills column by column, 7 rows per column 15 | for (let i = 0; i < boxes.length; i += 7) { 16 | const chunk = boxes.slice(i, i + 7); 17 | const firstBoxWithDate = chunk.find((b) => b.date); 18 | 19 | if (firstBoxWithDate && firstBoxWithDate.date) { 20 | const weekNum = getISOWeekNumber(new Date(firstBoxWithDate.date)); 21 | columns.push(weekNum); 22 | } else { 23 | columns.push(null); 24 | } 25 | } 26 | 27 | let lastWeekNum: number | null = null; 28 | 29 | return ( 30 |
35 | {columns.map((weekNum, index) => { 36 | // If empty column, render nothing 37 | if (weekNum === null) { 38 | lastWeekNum = null; // Reset if we hit a pure gap 39 | return
; 40 | } 41 | 42 | // If same as last week (split week), render nothing to avoid duplicate 43 | if (weekNum === lastWeekNum) { 44 | return
; 45 | } 46 | 47 | lastWeekNum = weekNum; 48 | return
{weekNum}
; 49 | })} 50 |
51 | ); 52 | } 53 | -------------------------------------------------------------------------------- /src/styles/heatmap-tracker-header.scss: -------------------------------------------------------------------------------- 1 | .heatmap-tracker-header { 2 | display: grid; 3 | grid-template-columns: 1fr; 4 | grid-auto-rows: 1fr; 5 | align-items: center; 6 | } 7 | 8 | .heatmap-tracker-header__main-row { 9 | display: grid; 10 | grid-template-columns: 1fr auto 1fr; 11 | align-items: center; 12 | font-size: 0.65em; 13 | } 14 | 15 | .heatmap-tracker-header__sub-row { 16 | display: grid; 17 | grid-template-columns: 1fr 5fr 1fr; 18 | align-items: center; 19 | } 20 | 21 | .heatmap-tracker-header__subtitle { 22 | grid-column: 2/3; 23 | font-size: 0.7em; 24 | text-align: center; 25 | } 26 | 27 | .heatmap-tracker-header__navigation { 28 | display: flex; 29 | align-items: center; 30 | } 31 | 32 | .heatmap-tracker-header__title { 33 | font-size: 1.2em; 34 | text-align: center; 35 | } 36 | 37 | @media screen and (max-width: 750px) { 38 | .heatmap-tracker-header__main-row { 39 | grid-template-rows: 1fr 1fr 1fr; 40 | grid-template-columns: 1fr 1fr 1fr; 41 | justify-items: center; 42 | } 43 | 44 | .heatmap-tracker-header__navigation { 45 | grid-row: 3; 46 | grid-column: 2; 47 | } 48 | 49 | .heatmap-tracker-header__title { 50 | grid-column: 1/-1; 51 | grid-row: 2; 52 | } 53 | 54 | .heatmap-tracker-header__tabs { 55 | grid-row: 1; 56 | grid-column: 2; 57 | } 58 | 59 | .heatmap-tracker-header__subtitle { 60 | grid-column: 1/-1; 61 | font-size: 0.7em; 62 | text-align: center; 63 | } 64 | } 65 | 66 | /* Tabs */ 67 | .heatmap-tracker-header__tabs { 68 | display: flex; 69 | justify-self: flex-end; 70 | position: relative; 71 | } 72 | 73 | .heatmap-tracker-header__tabs > *:not(:last-child) { 74 | margin-right: 0.2em; 75 | } 76 | 77 | .heatmap-tracker-tab { 78 | cursor: pointer; 79 | } 80 | -------------------------------------------------------------------------------- /src/__mocks__/obsidian.ts: -------------------------------------------------------------------------------- 1 | export class App { 2 | // Mock implementation of App class 3 | } 4 | 5 | export class Plugin { 6 | // Mock implementation of Plugin class 7 | } 8 | 9 | export class PluginSettingTab { 10 | // Mock implementation of PluginSettingTab class 11 | } 12 | 13 | export class Setting { 14 | // Mock implementation of Setting class 15 | } 16 | 17 | export class TFile { 18 | // Mock implementation of TFile class 19 | } 20 | 21 | export class TFolder { 22 | // Mock implementation of TFolder class 23 | } 24 | 25 | export class Vault { 26 | // Mock implementation of Vault class 27 | } 28 | 29 | export class Workspace { 30 | // Mock implementation of Workspace class 31 | } 32 | 33 | export class WorkspaceLeaf { 34 | // Mock implementation of WorkspaceLeaf class 35 | } 36 | 37 | export class MarkdownView { 38 | // Mock implementation of MarkdownView class 39 | } 40 | 41 | export class Notice { 42 | constructor(message: string, timeout?: number) { 43 | // Mock implementation of Notice constructor 44 | } 45 | } 46 | 47 | export class Modal { 48 | constructor(app: App) { 49 | // Mock implementation of Modal constructor 50 | } 51 | 52 | open() { 53 | // Mock implementation of open method 54 | } 55 | 56 | close() { 57 | // Mock implementation of close method 58 | } 59 | } 60 | 61 | declare global { 62 | function createDiv(): HTMLDivElement; 63 | } 64 | 65 | export function createDiv(): HTMLDivElement { 66 | return document.createElement('div'); 67 | } 68 | 69 | (global as any).createDiv = createDiv; 70 | declare global { 71 | function createSpan(): HTMLSpanElement; 72 | } 73 | 74 | export function createSpan(): HTMLSpanElement { 75 | return document.createElement('span'); 76 | } 77 | 78 | (global as any).createSpan = createSpan; -------------------------------------------------------------------------------- /EXAMPLE_VAULT/Documentation with Examples/4. Insights/8. Activity Intensity by Day.md: -------------------------------------------------------------------------------- 1 | 2 | ``` 3 | { 4 | name: "Activity Intensity by Day", 5 | calculate: ({ yearEntries }) => { 6 | const dayCounts = {}; 7 | 8 | yearEntries.forEach((entry) => { 9 | const day = new Date(entry.date).toLocaleDateString("en-US", { weekday: "long" }); 10 | dayCounts[day] = (dayCounts[day] || 0) + (entry.intensity || 0); 11 | }); 12 | 13 | return Object.entries(dayCounts) 14 | .map(([day, intensity]) => `${day}: ${intensity}`) 15 | .join(", "); 16 | }, 17 | } 18 | ``` 19 | 20 | ### Example: 21 | ```dataviewjs 22 | 23 | const trackerData = { 24 | year: 2024, // optional, remove this line to autoswitch year 25 | entries: [], 26 | heatmapTitle: "👣 Steps Tracker 👣", 27 | insights: [{ 28 | name: "Activity Intensity by Day", 29 | calculate: ({ yearEntries }) => { 30 | const dayCounts = {}; 31 | 32 | yearEntries.forEach((entry) => { 33 | const day = new Date(entry.date).toLocaleDateString("en-US", { weekday: "long" }); 34 | dayCounts[day] = (dayCounts[day] || 0) + (entry.intensity || 0); 35 | }); 36 | 37 | return Object.entries(dayCounts) 38 | .map(([day, intensity]) => `${day}: ${intensity}`) 39 | .join(", "); 40 | }, 41 | }] 42 | } 43 | 44 | 45 | for(let page of dv.pages('"daily notes"').where(p=>p.steps)){ 46 | 47 | trackerData.entries.push({ 48 | date: page.file.name, 49 | filePath: page.file.path, 50 | intensity: page.steps 51 | }) 52 | } 53 | 54 | trackerData.basePath = 'daily notes'; 55 | 56 | renderHeatmapTrackerStatistics(this.container, trackerData) 57 | renderHeatmapTracker(this.container, trackerData) 58 | 59 | ``` 60 | -------------------------------------------------------------------------------- /src/App.tsx: -------------------------------------------------------------------------------- 1 | import { IHeatmapView } from "./types"; 2 | import { useHeatmapContext } from "./context/heatmap/heatmap.context"; 3 | import React, { lazy, Suspense, useEffect } from "react"; 4 | import { useTranslation } from "react-i18next"; 5 | 6 | import { HeatmapHeader } from "./components/HeatmapHeader/HeatmapHeader"; 7 | 8 | import HeatmapFooter from "./components/HeatmapFooter/HeatmapFooter"; 9 | 10 | const HeatmapTrackerView = lazy( 11 | () => import("./views/HeatmapTrackerView/HeatmapTrackerView") 12 | ); 13 | const StatisticsView = lazy( 14 | () => import("./views/StatisticsView/StatisticsView") 15 | ); 16 | const DocumentationView = lazy( 17 | () => import("./views/DocumentationView/DocumentationView") 18 | ); 19 | 20 | const LegendView = lazy(() => import("./views/LegendView/LegendView")); 21 | 22 | function ReactApp() { 23 | const { i18n } = useTranslation(); 24 | const { currentYear, settings, view } = useHeatmapContext(); 25 | 26 | useEffect(() => { 27 | i18n.changeLanguage(settings.language); 28 | }, [settings]); 29 | 30 | let content; 31 | switch (view) { 32 | case IHeatmapView.HeatmapTracker: 33 | content = ; 34 | break; 35 | case IHeatmapView.HeatmapTrackerStatistics: 36 | content = ; 37 | break; 38 | case IHeatmapView.Documentation: 39 | content = ; 40 | break; 41 | case IHeatmapView.Legend: 42 | content = ; 43 | break; 44 | default: 45 | content = null; 46 | } 47 | 48 | if (!currentYear) { 49 | return null; 50 | } 51 | 52 | return ( 53 |
54 | 55 | {content} 56 | 57 |
58 | ); 59 | } 60 | 61 | export default React.memo(ReactApp); 62 | -------------------------------------------------------------------------------- /src/utils/__tests__/colors.spec.ts: -------------------------------------------------------------------------------- 1 | import { getColors } from "../colors"; 2 | 3 | describe("getColors", () => { 4 | test("should return palette colors in case when paletteName is provided", () => { 5 | const colorScheme = { 6 | paletteName: "warm" 7 | }; 8 | 9 | const settingsColors = { 10 | warm: ["#FF5733", "#FFBD33", "#FF8D1A"], 11 | default: ["#FFFFFF", "#000000"], 12 | }; 13 | 14 | const colors = getColors(colorScheme, settingsColors); 15 | 16 | expect(colors).toEqual(settingsColors.warm); 17 | }); 18 | 19 | test( 20 | "should return custom colors in case when customColors is provided", () => { 21 | const colorScheme = { 22 | customColors: ["#FF5733", "#FFBD33"], 23 | }; 24 | 25 | const settingsColors = { 26 | warm: ["#FF5733", "#FFBD33", "#FF8D1A"], 27 | default: ["#FFFFFF", "#000000"], 28 | }; 29 | 30 | const colors = getColors(colorScheme, settingsColors); 31 | 32 | expect(colors).toEqual(colorScheme.customColors); 33 | } 34 | ); 35 | 36 | test("should return customColors in case when paletteName and customColors are provided", () => { 37 | const colorScheme = { 38 | paletteName: "warm", 39 | customColors: ["#FF5733", "#FFBD33"], 40 | }; 41 | 42 | const settingsColors = { 43 | warm: ["#FF5733", "#FFBD33", "#FF8D1A"], 44 | default: ["#FFFFFF", "#000000"], 45 | }; 46 | 47 | const colors = getColors(colorScheme, settingsColors); 48 | 49 | expect(colors).toEqual(colorScheme.customColors); 50 | }); 51 | 52 | test('should return default palette colors in case when paletteName and customColors are not provided', () => { 53 | const colorScheme = {}; 54 | 55 | const settingsColors = { 56 | warm: ["#FF5733", "#FFBD33", "#FF8D1A"], 57 | default: ["#FFFFFF", "#000000"], 58 | }; 59 | 60 | const colors = getColors(colorScheme, settingsColors); 61 | 62 | expect(colors).toEqual(settingsColors.default); 63 | }); 64 | }); 65 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | import z from "zod"; 2 | 3 | import { EntrySchema } from "./schemas/entry.schema"; 4 | import { TrackerDataSchema } from "./schemas/trackerData.schema"; 5 | import { IntensityConfigSchema } from "./schemas/intensityConfig.schema"; 6 | import { InsightSchema } from "./schemas/insight.schema"; 7 | import { ColorsListSchema } from "./schemas/colorsList.schema"; 8 | import { ColorSchemeSchema } from "./schemas/colorScheme.schema"; 9 | import { PalettesSchema } from "./schemas/palettes.schema"; 10 | 11 | export type Entry = z.infer; 12 | 13 | export type ColorsList = z.infer; 14 | 15 | export type ColorScheme = z.infer; 16 | 17 | export type Palettes = z.infer; 18 | 19 | export type Insight = z.infer; 20 | 21 | export type IntensityConfig = z.infer; 22 | 23 | export type TrackerData = z.infer; 24 | 25 | export interface TrackerSettings { 26 | palettes: Palettes; 27 | weekStartDay: number; 28 | weekDisplayMode: WeekDisplayMode; 29 | separateMonths: boolean; 30 | showWeekNums: boolean; 31 | language: string; 32 | viewTabsVisibility: Partial>; 33 | } 34 | 35 | export interface Box { 36 | backgroundColor?: string; 37 | date?: string; 38 | /** Absolute path to the file in the vault (if known). */ 39 | filePath?: string; 40 | /** Custom href for this box; takes precedence over filePath. */ 41 | customHref?: string; 42 | content?: string | HTMLElement; 43 | isToday?: boolean; 44 | name?: string; 45 | showBorder?: boolean; 46 | hasData?: boolean; 47 | isSpaceBetweenBox?: boolean; 48 | } 49 | 50 | export enum IHeatmapView { 51 | HeatmapTracker = "heatmap-tracker", 52 | HeatmapTrackerStatistics = "heatmap-tracker-statistics", 53 | Documentation = "documentation", 54 | Legend = "legend", 55 | } 56 | 57 | export type WeekDisplayMode = "even" | "odd" | "none" | "all"; 58 | 59 | export interface TrackerParams { 60 | path?: string; 61 | property: string | string[]; 62 | } 63 | -------------------------------------------------------------------------------- /EXAMPLE_VAULT/Documentation with Examples/3. trackerData parameters/6. insights.md: -------------------------------------------------------------------------------- 1 | `insights` - is a powerful instrument for calculating and displaying your own insights in `Statistics`. 2 | 3 | > [!NOTE] 4 | > For more examples check `4. Insights` folder. 5 | 6 | ### Example 7 | You're tracking steps per day and you want to know how many days per this year you achieved your goal - 10000 steps per day. 8 | 9 | Let's look at the code below: 10 | - `name` - the name of the metric you want to see (it will be displayed in `Statistics`) 11 | - `calculate` - function that has `yearEntries` as input (in the future more data will be added), and should have `string` or `number` as output. 12 | 13 | In our example it has `10000` as a goal and we filter `entries` take only those, where `value` is more then our target (10000). 14 | 15 | ``` 16 | { 17 | name: "Days Achieved Step Goal (10,000 steps)", 18 | calculate: ({ yearEntries }) => { 19 | const stepGoal = 10000; 20 | const daysAchieved = yearEntries.filter((entry) => entry.value >= stepGoal).length; 21 | return daysAchieved.toString(); 22 | }, 23 | } 24 | ``` 25 | 26 | That's how it will look: 27 | 28 | ```dataviewjs 29 | 30 | const trackerData = { 31 | year: 2024, // optional, remove this line to autoswitch year 32 | entries: [], 33 | heatmapTitle: "👣 Steps Tracker 👣", 34 | insights: [{ 35 | name: "Days Achieved Step Goal (10,000 steps)", 36 | calculate: ({ yearEntries }) => { 37 | const stepGoal = 10000; 38 | const daysAchieved = yearEntries.filter((entry) => entry.value >= stepGoal).length; 39 | return daysAchieved.toString(); 40 | }, 41 | }] 42 | } 43 | 44 | 45 | for(let page of dv.pages('"daily notes"').where(p=>p.steps)){ 46 | 47 | trackerData.entries.push({ 48 | date: page.file.name, 49 | filePath: page.file.path, 50 | intensity: page.steps, 51 | }) 52 | } 53 | 54 | trackerData.basePath = 'daily notes'; 55 | 56 | renderHeatmapTrackerStatistics(this.container, trackerData) 57 | renderHeatmapTracker(this.container, trackerData) 58 | 59 | ``` 60 | -------------------------------------------------------------------------------- /src/render.tsx: -------------------------------------------------------------------------------- 1 | import { App } from "obsidian"; 2 | 3 | import { createRoot } from "react-dom/client"; 4 | import { StrictMode } from "react"; 5 | 6 | import { TrackerData, TrackerSettings } from "./types"; 7 | import ReactApp from "./App"; 8 | import { HeatmapProvider } from "./context/heatmap/heatmap.context"; 9 | 10 | import { mergeTrackerData } from "./utils/core"; 11 | 12 | import { notify } from "./utils/notify"; 13 | import { validateTrackerData } from "./schemas/validation"; 14 | import { AppContext } from "./context/app/app.context"; 15 | import { DEFAULT_TRACKER_DATA } from "./constants/defaultTrackerData"; 16 | 17 | export function renderApp( 18 | container: HTMLDivElement, 19 | app: App, 20 | pluginSettings: TrackerSettings, 21 | inputTrackerData: unknown, 22 | component: React.JSX.Element 23 | ) { 24 | const root = createRoot(container); 25 | 26 | let trackerData = mergeTrackerData( 27 | DEFAULT_TRACKER_DATA, 28 | inputTrackerData as TrackerData 29 | ); 30 | 31 | try { 32 | trackerData = validateTrackerData(trackerData) as TrackerData; 33 | } catch (e) { 34 | notify((e as Error).message, 0); 35 | } 36 | 37 | root.render( 38 | 39 | 40 | 44 | {component} 45 | 46 | 47 | 48 | ); 49 | 50 | return container; 51 | } 52 | 53 | export function getRenderHeatmapTracker( 54 | app: App, 55 | pluginSettings: TrackerSettings 56 | ) { 57 | return function renderHeatmapTracker( 58 | el: HTMLElement, 59 | inputTrackerData: unknown = DEFAULT_TRACKER_DATA, 60 | settings: TrackerSettings = pluginSettings 61 | ) { 62 | const container = el.createDiv({ 63 | cls: "heatmap-tracker-container", 64 | attr: { 65 | "data-htp-name": (inputTrackerData as TrackerData)?.heatmapTitle ?? "", 66 | }, 67 | }); 68 | 69 | return renderApp(container, app, settings, inputTrackerData, ); 70 | }; 71 | } 72 | -------------------------------------------------------------------------------- /generate_examples.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Set the output directory 4 | OUTPUT_DIR="./EXAMPLE_VAULT/daily notes" 5 | 6 | # Create the output directory if it doesn't exist 7 | mkdir -p "$OUTPUT_DIR" 8 | 9 | for YEAR in 2023 2024 2025 10 | do 11 | for DAY in {1..20} 12 | do 13 | DAY_PADDED=$(printf "%02d" $DAY) 14 | FILENAME="${OUTPUT_DIR}/${YEAR}-05-${DAY_PADDED}.md" 15 | 16 | # Random values for steps, exercise, and learning 17 | STEPS=$((RANDOM % 10000 + 1000)) 18 | EXERCISE=$((RANDOM % 60 + 10)) 19 | LEARNING=$((RANDOM % 120 + 10)) 20 | 21 | # Random times for wake-up and bedtime 22 | WAKE_HOUR=$((RANDOM % 5 + 4)) # 4 am to 9 am 23 | WAKE_MINUTE=$((RANDOM % 60)) 24 | SLEEP_HOUR=$((RANDOM % 5 + 20)) # 8 pm to midnight 25 | SLEEP_MINUTE=$((RANDOM % 60)) 26 | 27 | # Zero-pad minutes and hours manually 28 | WAKE_TIME="${YEAR}-05-${DAY_PADDED}T$(printf "%02d" $WAKE_HOUR):$(printf "%02d" $WAKE_MINUTE)" 29 | SLEEP_TIME="${YEAR}-05-${DAY_PADDED}T$(printf "%02d" $SLEEP_HOUR):$(printf "%02d" $SLEEP_MINUTE)" 30 | 31 | # Randomly generate tasks as complete or incomplete 32 | TASKS="" 33 | for TASK_NUM in {1..5} 34 | do 35 | if (( RANDOM % 2 == 0 )); then 36 | TASKS+="- [x] Task ${TASK_NUM}\n" 37 | else 38 | TASKS+="- [ ] Task ${TASK_NUM}\n" 39 | fi 40 | done 41 | 42 | # Create Markdown file with random content 43 | { 44 | echo "---" 45 | echo "steps: $STEPS" 46 | echo "exercise: $EXERCISE minutes" 47 | echo "learning: $LEARNING minutes" 48 | echo "---" 49 | echo "## Day No ${DAY} in ${YEAR}" 50 | echo "Good morning! Today is a beautiful day." 51 | echo "I'm going to learn something new today." 52 | echo "" 53 | echo "I learned about the history of the Roman Empire." 54 | echo "" 55 | echo "What do you think about the Roman Empire?" 56 | echo "" 57 | echo "I woke up today at [Woke:: $WAKE_TIME]" 58 | echo "I went to bed today at [Sleep:: $SLEEP_TIME]" 59 | echo "" 60 | echo "#### Task tracking example" 61 | echo -e "$TASKS" 62 | } > "$FILENAME" 63 | done 64 | done -------------------------------------------------------------------------------- /versions.json: -------------------------------------------------------------------------------- 1 | { 2 | "1.0.0": "0.1.0", 3 | "1.1.0": "0.1.0", 4 | "1.1.1": "0.1.0", 5 | "1.1.2": "0.1.0", 6 | "1.1.3": "0.1.0", 7 | "1.1.4": "0.1.0", 8 | "1.1.5": "0.1.0", 9 | "1.1.6": "0.1.0", 10 | "1.1.7": "0.1.0", 11 | "1.1.8": "0.1.0", 12 | "1.2.1": "0.1.0", 13 | "1.3.0": "0.1.0", 14 | "1.4.0": "0.1.0", 15 | "1.4.1": "0.1.0", 16 | "1.4.2": "0.1.0", 17 | "1.4.3": "0.1.0", 18 | "1.4.4": "0.1.0", 19 | "1.5.0": "0.1.0", 20 | "1.6.0": "0.1.0", 21 | "1.6.1": "0.1.0", 22 | "1.6.2": "0.1.0", 23 | "1.6.3": "0.1.0", 24 | "1.6.4": "0.1.0", 25 | "1.7.0": "0.1.0", 26 | "1.7.1": "0.1.0", 27 | "1.7.2": "0.1.0", 28 | "1.7.3": "0.1.0", 29 | "1.7.4": "0.1.0", 30 | "1.7.5": "0.1.0", 31 | "1.8.1": "0.1.0", 32 | "1.9.0": "0.1.0", 33 | "1.9.1": "0.1.0", 34 | "1.9.2": "0.1.0", 35 | "1.9.3": "0.1.0", 36 | "1.9.4": "0.1.0", 37 | "1.9.5": "0.1.0", 38 | "1.9.6": "0.1.0", 39 | "1.10.0": "0.1.0", 40 | "1.10.1": "0.1.0", 41 | "1.10.2": "0.1.0", 42 | "1.10.3": "0.1.0", 43 | "1.10.4": "0.1.0", 44 | "1.10.5": "0.1.0", 45 | "1.10.6": "0.1.0", 46 | "1.11.0": "0.1.0", 47 | "1.11.1": "0.1.0", 48 | "1.12.0": "0.1.0", 49 | "1.12.1": "0.1.0", 50 | "1.12.2": "0.1.0", 51 | "1.12.3": "0.1.0", 52 | "1.12.4": "0.1.0", 53 | "1.12.5": "0.1.0", 54 | "1.12.6": "0.1.0", 55 | "1.12.7": "0.1.0", 56 | "1.13.0": "0.1.0", 57 | "1.13.1": "0.1.0", 58 | "1.13.2": "0.1.0", 59 | "1.13.3": "0.1.0", 60 | "1.13.4": "0.1.0", 61 | "1.13.5": "0.1.0", 62 | "1.13.6": "0.1.0", 63 | "1.13.7": "0.1.0", 64 | "1.13.8": "0.1.0", 65 | "1.13.9": "0.1.0", 66 | "1.13.10": "0.1.0", 67 | "1.13.11": "0.1.0", 68 | "1.13.12": "0.1.0", 69 | "1.14.0": "0.1.0", 70 | "1.14.1": "0.1.0", 71 | "1.14.2": "0.1.0", 72 | "1.14.3": "0.1.0", 73 | "1.14.4": "0.1.0", 74 | "1.15.0": "0.1.0", 75 | "1.15.1": "0.1.0", 76 | "1.15.2": "0.1.0", 77 | "1.15.3": "0.1.0", 78 | "1.15.4": "0.1.0", 79 | "1.15.5": "0.1.0", 80 | "1.15.6": "0.1.0", 81 | "1.15.7": "0.1.0", 82 | "1.16.0": "0.1.0", 83 | "1.17.0": "0.1.0", 84 | "1.18.0": "0.1.0", 85 | "1.18.1": "0.1.0", 86 | "1.18.2": "0.1.0", 87 | "1.19.0": "0.1.0", 88 | "1.19.1": "0.1.0", 89 | "1.19.2": "0.1.0", 90 | "1.19.3": "0.1.0", 91 | "1.20.0": "0.1.0", 92 | "2.0.0": "0.1.0" 93 | } -------------------------------------------------------------------------------- /esbuild.config.mjs: -------------------------------------------------------------------------------- 1 | import esbuild from "esbuild"; 2 | import process from "process"; 3 | import builtins from "builtin-modules"; 4 | import { sassPlugin } from "esbuild-sass-plugin"; 5 | 6 | // Determine if we're in production mode 7 | const isProd = process.argv.includes("--production"); 8 | const isDebug = process.argv.includes("--debug"); 9 | 10 | // Banner to include at the top of the generated file 11 | const banner = `/* 12 | THIS IS A GENERATED/BUNDLED FILE BY ESBUILD 13 | If you want to view the source, please visit the GitHub repository of this plugin. 14 | */ 15 | `; 16 | 17 | // Define external dependencies that should not be bundled 18 | const externalDependencies = [ 19 | "obsidian", 20 | "electron", 21 | "@codemirror/*", 22 | "@lezer/*", 23 | ...builtins, 24 | ]; 25 | 26 | // Build options common to both development and production 27 | const buildOptions = { 28 | entryPoints: ["./src/main.tsx", "./src/styles.scss"], 29 | plugins: [ 30 | sassPlugin({ 31 | type: "css", 32 | precompile: (source) => { 33 | const devFlag = isDebug ? "$isDev: true;" : "$isDev: false;"; 34 | return `${devFlag}\n${source}`; 35 | }, 36 | }), 37 | ], 38 | bundle: true, 39 | platform: "browser", 40 | target: "es2017", 41 | external: externalDependencies, 42 | format: "cjs", 43 | sourcemap: isProd ? false : "inline", 44 | minify: isProd, 45 | banner: { js: banner }, 46 | logLevel: "info", 47 | outdir: "build", 48 | resolveExtensions: [".js", ".jsx", ".ts", ".tsx"], // Ensure extensions are resolved 49 | alias: { 50 | src: "./src", // Add this to map the alias 51 | react: "preact/compat", 52 | "react-dom": "preact/compat", 53 | }, 54 | }; 55 | 56 | async function build() { 57 | if (!isProd) { 58 | // Development build with watch mode 59 | const context = await esbuild.context(buildOptions); 60 | 61 | // Start watching for file changes 62 | await context.watch(); 63 | 64 | console.log("Watching for changes..."); 65 | } else { 66 | // Production build (one-time build) 67 | await esbuild.build(buildOptions); 68 | } 69 | } 70 | 71 | // Execute the build function 72 | build().catch((error) => { 73 | console.error(error); 74 | process.exit(1); 75 | }); 76 | -------------------------------------------------------------------------------- /src/components/HeatmapBox/HeatmapBox.tsx: -------------------------------------------------------------------------------- 1 | import { ReactNode, useMemo } from "react"; 2 | import { Box } from "src/types"; 3 | 4 | import { useHeatmapContext } from "src/context/heatmap/heatmap.context"; 5 | import { useAppContext } from "src/context/app/app.context"; 6 | import { handleBoxClick } from "src/utils/heatmapBox"; 7 | 8 | interface HeatmapBoxProps { 9 | box: Box; 10 | } 11 | 12 | export function HeatmapBox({ box }: HeatmapBoxProps) { 13 | const { trackerData } = useHeatmapContext(); 14 | const app = useAppContext(); 15 | 16 | const boxClassNames = [ 17 | "heatmap-tracker-box", 18 | box.name, 19 | box.isToday ? "today" : "", 20 | box.showBorder ? "with-border" : "", 21 | box.hasData 22 | ? "hasData" 23 | : box.isSpaceBetweenBox 24 | ? "space-between-box" 25 | : "isEmpty", 26 | ]; 27 | 28 | // Prepare Obsidian internal-link or custom href; prefer customHref, then filePath, then date 29 | const linkTarget = useMemo(() => { 30 | if (box.customHref) { 31 | return box.customHref; 32 | } 33 | 34 | if (box.filePath) { 35 | return box.filePath; 36 | } 37 | 38 | return undefined; 39 | }, [box.customHref, box.filePath]); 40 | 41 | const content = 42 | box.content instanceof HTMLElement ? ( 43 | 44 | ) : ( 45 | (box.content as ReactNode) 46 | ); 47 | 48 | const isExternal = 49 | typeof linkTarget === "string" && /^https?:\/\//i.test(linkTarget); 50 | 51 | const linkAttrs = linkTarget 52 | ? { "data-href": linkTarget, href: linkTarget } 53 | : {}; 54 | 55 | function onBoxClick() { 56 | if (linkTarget) { 57 | return; 58 | } 59 | 60 | handleBoxClick(box, app, trackerData); 61 | } 62 | 63 | return ( 64 | 80 | ); 81 | } 82 | -------------------------------------------------------------------------------- /src/components/HeatmapHeader/HeatmapHeader.tsx: -------------------------------------------------------------------------------- 1 | import { useTranslation } from "react-i18next"; 2 | import { useHeatmapContext } from "src/context/heatmap/heatmap.context"; 3 | import { HeatmapTabs } from "../HeatmapTabs/HeatmapTabs"; 4 | 5 | export function HeatmapHeader() { 6 | const { t } = useTranslation(); 7 | const { currentYear, setCurrentYear, trackerData } = useHeatmapContext(); 8 | 9 | function onArrowBackClick() { 10 | setCurrentYear(currentYear - 1); 11 | } 12 | 13 | function onArrowForwardClick() { 14 | setCurrentYear(currentYear + 1); 15 | } 16 | 17 | return ( 18 |
19 |
20 |
21 | {trackerData?.ui?.hideYear ? null : ( 22 | <> 23 | 30 |
{currentYear}
31 | 38 | 39 | )} 40 |
41 | 42 | {trackerData?.ui?.hideTitle ? null : ( 43 |
49 | )} 50 | {trackerData?.ui?.hideTabs ? null : } 51 |
52 | {trackerData?.ui?.hideSubtitle ? null : trackerData?.heatmapSubtitle ? ( 53 |
54 |
60 |
61 | ) : null} 62 |
63 | ); 64 | } 65 | -------------------------------------------------------------------------------- /src/schemas/__tests__/palettes.schema.test.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | import { PalettesSchema } from "../palettes.schema"; 3 | 4 | 5 | describe("PalettesSchema", () => { 6 | it("accepts a valid palettes object", () => { 7 | const input = { 8 | primary: ["#ff0000", "#00ff00"], 9 | secondary: ["#0000ff"], 10 | }; 11 | 12 | const result = PalettesSchema.safeParse(input); 13 | 14 | expect(result.success).toBe(true); 15 | 16 | if (result.success) { 17 | expect(result.data).toEqual(input); 18 | } 19 | }); 20 | 21 | it("allows an empty object", () => { 22 | const input = {}; 23 | 24 | const result = PalettesSchema.safeParse(input); 25 | 26 | expect(result.success).toBe(true); 27 | if (result.success) { 28 | expect(result.data).toEqual({}); 29 | } 30 | }); 31 | 32 | it("rejects non-object values", () => { 33 | const result = PalettesSchema.safeParse("not-an-object" as any); 34 | 35 | expect(result.success).toBe(false); 36 | if (!result.success) { 37 | expect(result.error).toBeInstanceOf(z.ZodError); 38 | } 39 | }); 40 | 41 | it("rejects when a palette value is not an array", () => { 42 | const input = { 43 | primary: ["#ff0000"], 44 | secondary: "#00ff00" as any, // ❌ не массив 45 | }; 46 | 47 | const result = PalettesSchema.safeParse(input); 48 | 49 | expect(result.success).toBe(false); 50 | if (!result.success) { 51 | // ошибка должна быть на ключе "secondary" 52 | expect(result.error.issues[0].path).toEqual(["secondary"]); 53 | } 54 | }); 55 | 56 | it("rejects when a palette contains non-string elements", () => { 57 | const input = { 58 | primary: ["#ff0000", 123 as any], // ❌ число внутри массива 59 | }; 60 | 61 | const result = PalettesSchema.safeParse(input); 62 | 63 | expect(result.success).toBe(false); 64 | if (!result.success) { 65 | // путь до ошибки: ["primary", 1] 66 | expect(result.error.issues[0].path).toEqual(["primary", 1]); 67 | expect(result.error.issues[0].code).toBe("invalid_type"); 68 | } 69 | }); 70 | 71 | it("throws ZodError when using parse with invalid data", () => { 72 | const input = { 73 | primary: "not-an-array" as any, 74 | }; 75 | 76 | expect(() => PalettesSchema.parse(input)).toThrow(z.ZodError); 77 | }); 78 | }); 79 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "heatmap-tracker", 3 | "version": "2.0.0", 4 | "description": "A heatmap tracker plugin for Obsidian", 5 | "main": "/build/main.js", 6 | "scripts": { 7 | "dev": "concurrently \"node esbuild.config.mjs\" \"npm run copyAndWatchMainJS\" \"npm run copyAndWatchStylesJS\" \"npm run copyAndWatchManifestJS\"", 8 | "dev:debug": "concurrently \"node esbuild.config.mjs --debug\" \"npm run copyAndWatchMainJS\" \"npm run copyAndWatchStylesJS\" \"npm run copyAndWatchManifestJS\"", 9 | "copyAndWatchMainJS": "cpx \"./build/main.js\" \"EXAMPLE_VAULT\\.obsidian\\plugins\\heatmap-tracker\" -w", 10 | "copyAndWatchStylesJS": "cpx \"./build/styles.css\" \"EXAMPLE_VAULT\\.obsidian\\plugins\\heatmap-tracker\" -w", 11 | "copyAndWatchManifestJS": "cpx \"manifest.json\" \"EXAMPLE_VAULT\\.obsidian\\plugins\\heatmap-tracker\" -w", 12 | "build": "rimraf build && tsc -noEmit -skipLibCheck && node esbuild.config.mjs --production && npm run copyToBuild", 13 | "copyToBuild": "cpx \"./build/styles.css\" \"build\" && cpx \"manifest.json\" \"build\"", 14 | "version": "node version-bump.mjs && git add manifest.json versions.json", 15 | "test": "jest", 16 | "test:utc": "TZ=utc jest", 17 | "test:usa": "TZ=America/New_York jest" 18 | }, 19 | "keywords": [ 20 | "obsidian" 21 | ], 22 | "author": "Maksim Rubanau (mokkiebear)", 23 | "license": "Apache-2.0", 24 | "devDependencies": { 25 | "@testing-library/dom": "^10.4.1", 26 | "@testing-library/react": "^16.3.0", 27 | "@types/jest": "^30.0.0", 28 | "@types/node": "24.1.0", 29 | "@types/react": "^19.1.9", 30 | "@types/react-dom": "^19.1.7", 31 | "@types/react-window": "^1.8.8", 32 | "@typescript-eslint/eslint-plugin": "^8.38.0", 33 | "@typescript-eslint/parser": "^8.38.0", 34 | "builtin-modules": "5.0.0", 35 | "concurrently": "9.2.0", 36 | "cpx": "^1.5.0", 37 | "esbuild": "0.25.8", 38 | "esbuild-sass-plugin": "^3.3.1", 39 | "jest": "^30.0.5", 40 | "jest-environment-jsdom": "^30.0.5", 41 | "obsidian": "latest", 42 | "obsidian-daily-notes-interface": "^0.9.4", 43 | "obsidian-dataview": "^0.5.68", 44 | "rimraf": "^6.0.1", 45 | "ts-jest": "^29.4.0", 46 | "tslib": "2.8.1", 47 | "typedoc": "^0.28.9", 48 | "typescript": "5.8.3" 49 | }, 50 | "dependencies": { 51 | "i18next": "^25.3.2", 52 | "preact": "^10.27.0", 53 | "react": "^19.1.1", 54 | "react-dom": "^19.1.1", 55 | "react-i18next": "^15.6.1", 56 | "zod": "^4.1.12" 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /EXAMPLE_VAULT/.obsidian/workspace-mobile.json: -------------------------------------------------------------------------------- 1 | { 2 | "main": { 3 | "id": "0a72f5b884aa6b3f", 4 | "type": "split", 5 | "children": [ 6 | { 7 | "id": "0208ee7419c7dd74", 8 | "type": "tabs", 9 | "children": [ 10 | { 11 | "id": "231eeed7e54a5ede", 12 | "type": "leaf", 13 | "state": { 14 | "type": "markdown", 15 | "state": { 16 | "file": "1. Heatmap Tracker Overview.md", 17 | "mode": "source", 18 | "backlinks": false, 19 | "source": false 20 | }, 21 | "icon": "lucide-file", 22 | "title": "1. Heatmap Tracker Overview" 23 | } 24 | } 25 | ] 26 | } 27 | ], 28 | "direction": "vertical" 29 | }, 30 | "left": { 31 | "id": "7cab98df28ef7700", 32 | "type": "mobile-drawer", 33 | "children": [ 34 | { 35 | "id": "7b2e310612a714a4", 36 | "type": "leaf", 37 | "state": { 38 | "type": "file-explorer", 39 | "state": { 40 | "sortOrder": "alphabetical" 41 | }, 42 | "icon": "lucide-folder-closed", 43 | "title": "Files" 44 | } 45 | }, 46 | { 47 | "id": "40180728c8944f52", 48 | "type": "leaf", 49 | "state": { 50 | "type": "search", 51 | "state": { 52 | "query": "", 53 | "matchingCase": false, 54 | "explainSearch": false, 55 | "collapseAll": false, 56 | "extraContext": false, 57 | "sortOrder": "alphabetical" 58 | }, 59 | "icon": "lucide-search", 60 | "title": "Search" 61 | } 62 | }, 63 | { 64 | "id": "ce5e79033dd167d5", 65 | "type": "leaf", 66 | "state": { 67 | "type": "bookmarks", 68 | "state": {}, 69 | "icon": "lucide-bookmark", 70 | "title": "Bookmarks" 71 | } 72 | } 73 | ], 74 | "currentTab": 0 75 | }, 76 | "right": { 77 | "id": "02cc380ad3b23148", 78 | "type": "mobile-drawer", 79 | "children": [], 80 | "currentTab": 0 81 | }, 82 | "left-ribbon": { 83 | "hiddenItems": { 84 | "canvas:Create new canvas": false, 85 | "daily-notes:Open today's daily note": false, 86 | "command-palette:Open command palette": false, 87 | "workspaces:Manage workspace layouts": false 88 | } 89 | }, 90 | "active": "231eeed7e54a5ede", 91 | "lastOpenFiles": [] 92 | } -------------------------------------------------------------------------------- /EXAMPLE_VAULT/Examples/Task Tracking Example from Reddit/Task Tracking example from Reddit.md: -------------------------------------------------------------------------------- 1 | ```dataviewjs 2 | const trackerData = { 3 | entries: [], // the array that the values get assigned to 4 | colorScheme: { paletteName: "longerDefault" }, 5 | heatmapTitle: "Habit Tracker", 6 | heatmapSubtitle: "Tracking my days of completed Habits", 7 | intensityScaleStart: 0, 8 | intensityScaleEnd: 1, 9 | separateMonths: true 10 | } 11 | 12 | /* The following is just javascript logic to that turns the completed and uncompleted tasks into a number to assign a colour */ 13 | 14 | function countHabits(markdown) { 15 | // Split the markdown into lines for processing 16 | const lines = markdown.split('\n'); 17 | 18 | // Variables to track the habit section and counts 19 | let inHabitSection = false; 20 | let checkedCount = 0; 21 | let uncheckedCount = 0; 22 | 23 | // Loop through the lines 24 | for (const line of lines) { 25 | // Check for the start of the "## Habit" section 26 | if (line.trim() === '## Habit') { 27 | inHabitSection = true; 28 | continue; 29 | } 30 | 31 | // Exit the habit section if another "##" heading is encountered 32 | if (inHabitSection && line.startsWith('## ')) { 33 | break; 34 | } 35 | 36 | // Count checkboxes only within the "## Habit" section 37 | if (inHabitSection) { 38 | if (line.includes('- [x]')) { 39 | checkedCount++; 40 | } else if (line.includes('- [ ]')) { 41 | uncheckedCount++; 42 | } 43 | } 44 | } 45 | 46 | // Return the counts 47 | return 0 + checkedCount/(checkedCount+uncheckedCount); 48 | } 49 | 50 | /* this is a loop that finds all the files with the YYYY/MM/DD format in the 0. PeriodicNotes folder and executes the previous logic to assign the tasks as a number and puts them in teh array */ 51 | 52 | for (let page of dv.pages('"Examples/Task Tracking Example from Reddit/notes"') 53 | .where(p => /^\d{4}-\d{2}-\d{2}$/.test(p.file.name))) { 54 | const markdown = await dv.io.load(page.file.path); 55 | trackerData.entries.push({ 56 | date: page.file.name, 57 | filePath: page.file.path, 58 | intensity: countHabits(markdown) }); 59 | } 60 | 61 | /* this just executes the heatmap plugin to render the image */ 62 | 63 | trackerData.basePath = 'Examples/Task Tracking Example from Reddit/notes'; 64 | 65 | renderHeatmapTracker(this.container, trackerData); 66 | ``` 67 | 68 | https://www.reddit.com/r/ObsidianMD/comments/1hrmeil/i_need_help_in_creating_a_dataview_based_habit/ 69 | -------------------------------------------------------------------------------- /EXAMPLE_VAULT/Github Issues/56. Ability to disable elements in header.md: -------------------------------------------------------------------------------- 1 | --- 2 | status: 3 | - done 4 | --- 5 | Link: https://github.com/mokkiebear/heatmap-tracker/issues/56 6 | 7 | To disable `title` and `subtitle` - just don't add these properties in `trackerData` object. 8 | 9 | In release 1.19.2 added new `ui` property in `trackerData` schema to disable elements in header: 10 | - `hideTabs` 11 | - `hideYear` 12 | - `defaultView` 13 | - `hideTitle` 14 | - `hideSubtitle` 15 | 16 | ```dataviewjs 17 | const trackerData = { 18 | intensityScaleStart: -2, 19 | intensityScaleEnd: 2, 20 | separateMonths: true, 21 | heatmapTitle: 'This title will be hidden', 22 | heatmapSubtitle: 'This subtitle will be hidden', 23 | ui: { 24 | hideTabs: true, 25 | hideYear: true, 26 | hideTitle: true, 27 | hideSubtitle: true, 28 | defaultView: 'heatmap-tracker', // heatmap-tracker | heatmap-tracker-statistics | documentation | legend 29 | }, 30 | entries: [ 31 | { 32 | "date": "2025-04-14", 33 | "intensity": -2 34 | }, 35 | { 36 | "date": "2025-04-15", 37 | "intensity": -1 38 | }, 39 | { 40 | "date": "2025-04-16", 41 | "intensity": 0 42 | }, 43 | { 44 | "date": "2025-04-17", 45 | "intensity": 1 46 | }, 47 | { 48 | "date": "2025-04-18", 49 | "intensity": 2 50 | }, 51 | ] 52 | } 53 | 54 | renderHeatmapTracker(this.container, trackerData) 55 | 56 | ``` 57 | 58 | Using `defaultView` you can change the view which should be display, by default. For example `Statistics` view: 59 | ```dataviewjs 60 | const trackerData = { 61 | intensityScaleStart: -2, 62 | intensityScaleEnd: 2, 63 | separateMonths: true, 64 | heatmapTitle: 'This title will be hidden', 65 | heatmapSubtitle: 'This subtitle will be hidden', 66 | ui: { 67 | hideTabs: true, 68 | hideYear: true, 69 | hideTitle: true, 70 | hideSubtitle: true, 71 | defaultView: 'heatmap-tracker-statistics', // heatmap-tracker | heatmap-tracker-statistics | documentation | legend 72 | }, 73 | entries: [ 74 | { 75 | "date": "2025-04-14", 76 | "intensity": -2 77 | }, 78 | { 79 | "date": "2025-04-15", 80 | "intensity": -1 81 | }, 82 | { 83 | "date": "2025-04-16", 84 | "intensity": 0 85 | }, 86 | { 87 | "date": "2025-04-17", 88 | "intensity": 1 89 | }, 90 | { 91 | "date": "2025-04-18", 92 | "intensity": 2 93 | }, 94 | ] 95 | } 96 | 97 | renderHeatmapTracker(this.container, trackerData) 98 | 99 | ``` -------------------------------------------------------------------------------- /src/localization/locales/zh.json: -------------------------------------------------------------------------------- 1 | { 2 | "settings": { 3 | "language": "语言", 4 | "chooseYourPreferredLanguage": "选择您偏好的语言", 5 | "palettes": "调色板", 6 | "paletteName": "调色板名称", 7 | "addNewColorToPalette": "向 {{paletteName}} 调色板添加新颜色:", 8 | "addColor": "添加颜色", 9 | "removeColor": "移除颜色", 10 | "saveColor": "保存颜色", 11 | "addNewPalette": "添加新调色板", 12 | "enterPaletteName": "输入调色板名称以创建新调色板", 13 | "addPaletteNote": "添加将在您的热力图渲染设置中可用的调色板。", 14 | "colorsUsageNote": "您可以在热力图渲染设置中通过引用其名称来使用这些调色板。", 15 | "weekStartDay": "周起始日", 16 | "weekStartDayDescription": "选择您的一周从哪一天开始。", 17 | "separateMonths": "分隔月份", 18 | "separateMonthsDescription": "在您的追踪器视图中全局分隔月份。", 19 | "weekDisplayMode": { 20 | "label": "周显示模式", 21 | "description": "选择要显示一周中的哪些天:偶数天(例如:星期二、星期四、星期六)、奇数天(例如:星期一、星期三、星期五、星期日)、无(不显示任何天数)或全部(显示一周中的所有七天)。" 22 | }, 23 | "tabsVisibility": "标签页可见性", 24 | "tabsVisibilityDescription": "显示/隐藏 {{viewKey}} 视图的标签页", 25 | "showWeekNums": "显示周数", 26 | "showWeekNumsDescription": "在热力图下方显示周数(1-53)。" 27 | }, 28 | "statistics": { 29 | "title": "统计", 30 | "totalTrackingDaysThisYear": "本年总追踪天数", 31 | "totalTrackingDays": "总追踪天数", 32 | "currentStreak": "当前连续记录", 33 | "longestStreak": "最长连续记录" 34 | }, 35 | "monthsShort": { 36 | "January": "1月", 37 | "February": "2月", 38 | "March": "3月", 39 | "April": "4月", 40 | "May": "5月", 41 | "June": "6月", 42 | "July": "7月", 43 | "August": "8月", 44 | "September": "9月", 45 | "October": "10月", 46 | "November": "11月", 47 | "December": "12月" 48 | }, 49 | "weekdaysShort": { 50 | "Sunday": "周日", 51 | "Monday": "周一", 52 | "Tuesday": "周二", 53 | "Wednesday": "周三", 54 | "Thursday": "周四", 55 | "Friday": "周五", 56 | "Saturday": "周六" 57 | }, 58 | "weekdaysLong": { 59 | "Sunday": "星期日", 60 | "Monday": "星期一", 61 | "Tuesday": "星期二", 62 | "Wednesday": "星期三", 63 | "Thursday": "星期四", 64 | "Friday": "星期五", 65 | "Saturday": "星期六" 66 | }, 67 | "header": { 68 | "previousYear": "上一年", 69 | "nextYear": "下一年" 70 | }, 71 | "weekDisplayMode": { 72 | "even": "偶数", 73 | "odd": "奇数", 74 | "all": "全部", 75 | "none": "无" 76 | }, 77 | "view": { 78 | "heatmap-tracker": "热力图", 79 | "heatmap-tracker-statistics": "统计", 80 | "documentation": "文档", 81 | "donation": "捐赠", 82 | "legend": "图例" 83 | }, 84 | "tab": "标签页", 85 | "support.header": "☕️ 你好!", 86 | "support.text1": "如果这个插件能让你的日子顺畅一点,或者帮你更好地保持条理,那真的让我很开心 ❤️", 87 | "support.text2": "我已经投入了数十个小时来构建和改进它 —— 你的支持能帮助我持续添加新的想法和更新 ✨", 88 | "support.cta": "过程不到2分钟,无需注册,即使一杯5美元咖啡的支持也能带来实实在在的改变 ☕️💛" 89 | } -------------------------------------------------------------------------------- /src/utils/date.ts: -------------------------------------------------------------------------------- 1 | export function isValidDate(dateString: string): boolean { 2 | const date = new Date(dateString); 3 | return !isNaN(date.getTime()); 4 | } 5 | 6 | export function getDayOfYear(date: Date): number { 7 | const startOfYear = Date.UTC(date.getUTCFullYear(), 0, 1); 8 | 9 | const current = Date.UTC( 10 | date.getUTCFullYear(), 11 | date.getUTCMonth(), 12 | date.getUTCDate() 13 | ); 14 | 15 | const diff = current - startOfYear; 16 | return Math.floor(diff / (1000 * 60 * 60 * 24)) + 1; 17 | } 18 | 19 | export function getISOWeekNumber(date: Date): number { 20 | const d = new Date(Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate())); 21 | const dayNum = d.getUTCDay() || 7; 22 | d.setUTCDate(d.getUTCDate() + 4 - dayNum); 23 | const yearStart = new Date(Date.UTC(d.getUTCFullYear(), 0, 1)); 24 | return Math.ceil((((d.getTime() - yearStart.getTime()) / 86400000) + 1) / 7); 25 | } 26 | 27 | export function getShiftedWeekdays(weekdays: string[], weekStartDay: number): string[] { 28 | if (weekStartDay < 0 || weekStartDay > 6) { 29 | throw new Error('weekStartDay must be between 0 and 6'); 30 | } 31 | 32 | return weekdays.slice(weekStartDay).concat(weekdays.slice(0, weekStartDay)); 33 | } 34 | 35 | export function getFirstDayOfYear(year: number): Date { 36 | return new Date(Date.UTC(year, 0, 1)); 37 | } 38 | 39 | export function getNumberOfEmptyDaysBeforeYearStarts(year: number, weekStartDay: number): number { 40 | if (isNaN(weekStartDay) || weekStartDay < 0 || weekStartDay > 6) { 41 | throw new Error('weekStartDay must be a number between 0 and 6'); 42 | } 43 | 44 | if (isNaN(year)) { 45 | throw new Error('year must be a number'); 46 | } 47 | 48 | const firstDayOfYear = getFirstDayOfYear(year); 49 | const firstWeekday = firstDayOfYear.getUTCDay(); 50 | return (firstWeekday - weekStartDay + 7) % 7; 51 | } 52 | 53 | export function getLastDayOfYear(year: number): Date { 54 | return new Date(Date.UTC(year, 11, 31)); 55 | } 56 | 57 | export function getToday() { 58 | const todayUTC = new Date(); 59 | 60 | return todayUTC; 61 | } 62 | 63 | export function formatDateToISO8601(date: Date | null): string | null { 64 | if (!date) { 65 | return null; 66 | } 67 | 68 | const formattedDate = date?.toISOString?.()?.split('T')?.[0]; 69 | 70 | return formattedDate; 71 | } 72 | 73 | export function getFullYear(date: string) { 74 | return new Date(date).getUTCFullYear(); 75 | } 76 | 77 | export function getCurrentFullYear() { 78 | return new Date().getUTCFullYear(); 79 | } 80 | 81 | export function isSameDate(d1: Date, d2: Date): boolean { 82 | return ( 83 | d1.getUTCFullYear() === d2.getUTCFullYear() && 84 | d1.getUTCMonth() === d2.getUTCMonth() && 85 | d1.getUTCDate() === d2.getUTCDate() 86 | ); 87 | } -------------------------------------------------------------------------------- /EXAMPLE_VAULT/Activities Tracking/Reading.md: -------------------------------------------------------------------------------- 1 | --- 2 | class: reading 3 | tracking: true 4 | color: '#e254ff' 5 | --- 6 | - [[2024-06-07]] Started "To Kill a Mockingbird" 7 | - [[2024-06-08]] Read 15 pages of "To Kill a Mockingbird" 8 | - [[2024-06-09]] Read 20 pages of "To Kill a Mockingbird" 9 | - [[2024-06-10]] Finished "To Kill a Mockingbird" 10 | - [[2024-06-11]] Started "Pride and Prejudice" 11 | - [[2024-06-12]] Read 25 pages of "Pride and Prejudice" 12 | - [[2024-06-13]] Read 30 pages of "Pride and Prejudice" 13 | - [[2024-06-14]] Finished "Pride and Prejudice" 14 | - [[2024-06-15]] Started "Moby Dick" 15 | - [[2024-06-16]] Read 20 pages of "Moby Dick" 16 | - [[2024-06-17]] Read 25 pages of "Moby Dick" 17 | - [[2024-06-18]] Finished "Moby Dick" 18 | - [[2024-06-19]] Started "War and Peace" 19 | - [[2024-06-20]] Read 30 pages of "War and Peace" 20 | - [[2024-06-21]] Read 35 pages of "War and Peace" 21 | - [[2024-06-22]] Read 40 pages of "War and Peace" 22 | - [[2024-06-23]] Finished "War and Peace" 23 | - [[2024-06-24]] Started "The Catcher in the Rye" 24 | - [[2024-06-25]] Read 20 pages of "The Catcher in the Rye" 25 | - [[2024-06-26]] Read 25 pages of "The Catcher in the Rye" 26 | - [[2024-06-27]] Finished "The Catcher in the Rye" 27 | - [[2024-06-28]] Started "Brave New World" 28 | - [[2024-06-29]] Read 30 pages of "Brave New World" 29 | - [[2024-06-30]] Finished "Brave New World" 30 | - [[2024-08-01]] Started "The Hobbit" 31 | - [[2024-08-02]] Read 25 pages of "The Hobbit" 32 | - [[2024-08-03]] Read 30 pages of "The Hobbit" 33 | - [[2024-08-04]] Finished "The Hobbit" 34 | - [[2024-08-05]] Started "The Lord of the Rings: The Fellowship of the Ring" 35 | - [[2024-08-06]] Read 40 pages of "The Fellowship of the Ring" 36 | - [[2024-08-07]] Read 35 pages of "The Fellowship of the Ring" 37 | - [[2024-08-08]] Finished "The Fellowship of the Ring" 38 | - [[2024-08-09]] Started "The Lord of the Rings: The Two Towers" 39 | - [[2024-08-10]] Read 30 pages of "The Two Towers" 40 | - [[2024-08-11]] Read 35 pages of "The Two Towers" 41 | - [[2024-08-12]] Finished "The Two Towers" 42 | - [[2024-08-13]] Started "The Lord of the Rings: The Return of the King" 43 | - [[2024-08-14]] Read 40 pages of "The Return of the King" 44 | - [[2024-08-15]] Read 45 pages of "The Return of the King" 45 | - [[2024-08-16]] Finished "The Return of the King" 46 | - [[2024-08-17]] Started "Harry Potter and the Sorcerer's Stone" 47 | - [[2024-08-18]] Read 50 pages of "Harry Potter and the Sorcerer's Stone" 48 | - [[2024-08-19]] Read 55 pages of "Harry Potter and the Sorcerer's Stone" 49 | - [[2024-08-20]] Finished "Harry Potter and the Sorcerer's Stone" 50 | - [[2024-08-21]] Started "Harry Potter and the Chamber of Secrets" 51 | - [[2024-08-22]] Read 60 pages of "Harry Potter and the Chamber of Secrets" 52 | - [[2024-08-23]] Read 65 pages of "Harry Potter and the Chamber of Secrets" 53 | - [[2024-08-24]] Finished "Harry Potter and the Chamber of Secrets" -------------------------------------------------------------------------------- /EXAMPLE_VAULT/Showcase/5. Task (Project) tracker.md: -------------------------------------------------------------------------------- 1 | In your daily notes you're tracking tasks or projects in format: 2 | - [x] Task 1 3 | - [ ] Task 2 4 | - [x] Task 3 5 | - [x] Task 4 6 | - [x] Task 5 7 | 8 | Now, imagine you want to visualize your progress in two ways: 9 | 1. **Project-Specific Heatmaps**: Create a separate heatmap for each project to monitor streaks (e.g., “don’t break the streak”). 10 | 2. **Intensity-Based Heatmaps**: Build a heatmap where the intensity of each day increases based on the number of completed tasks. For example: 11 | • **1 task** = low intensity 12 | • **10 tasks** = high intensity 13 | 14 | Let’s implement this! 15 | 16 | 17 | ### 1. **Project-Specific Heatmap**: Find in "daily notes" folder information about Task 1 completion and display in on the heatmap 18 | 19 | ```dataviewjs 20 | // Update this object 21 | const trackerData = { 22 | entries: [], 23 | separateMonths: true, 24 | heatmapTitle: "Task 4 streak", 25 | colorScheme: { 26 | customColors: ["rgb(232, 131, 74)", "rgb(103, 214, 66)"] 27 | }, 28 | // heatmapSubtitle: "This is the subtitle for your heatmap. You can use it as a description.", 29 | } 30 | 31 | // Path to the folder with notes 32 | const PATH_TO_YOUR_FOLDER = "daily notes"; 33 | const TASK_NAME = "Task 4"; 34 | 35 | // You need dataviewjs plugin to get information from your pages 36 | for(let page of dv.pages(`"${PATH_TO_YOUR_FOLDER}"`)){ 37 | const tasks = page.file.tasks; 38 | const task = tasks.find((t) => t.text === TASK_NAME); 39 | 40 | trackerData.entries.push({ 41 | date: page.file.name, 42 | filePath: page.file.path, 43 | intensity: task?.status === "x" ? 10 : 0 44 | }); 45 | } 46 | 47 | trackerData.basePath = PATH_TO_YOUR_FOLDER; 48 | 49 | renderHeatmapTracker(this.container, trackerData); 50 | ``` 51 | ### 2. Intensity-Based Heatmap 52 | Build a heatmap where the intensity of each day increases based on the number of completed tasks. For example: 53 | • **1 task** = low intensity 54 | • **5 tasks** = high intensity 55 | 56 | ```dataviewjs 57 | // Update this object 58 | const trackerData = { 59 | entries: [], 60 | separateMonths: true, 61 | heatmapTitle: "Task Intensity Heatmap", 62 | // heatmapSubtitle: "This is the subtitle for your heatmap. You can use it as a description.", 63 | } 64 | 65 | // Path to the folder with notes 66 | const PATH_TO_YOUR_FOLDER = "daily notes"; 67 | 68 | // You need dataviewjs plugin to get information from your pages 69 | for(let page of dv.pages(`"${PATH_TO_YOUR_FOLDER}"`)){ 70 | const tasks = page.file.tasks; 71 | 72 | let intensity = 0; 73 | for (const task of tasks) { 74 | intensity += task.status === "x" ? 2 : 0; 75 | } 76 | 77 | trackerData.entries.push({ 78 | date: page.file.name, 79 | filePath: page.file.path, 80 | intensity: intensity 81 | }); 82 | } 83 | 84 | trackerData.basePath = PATH_TO_YOUR_FOLDER; 85 | 86 | renderHeatmapTracker(this.container, trackerData); 87 | ``` 88 | -------------------------------------------------------------------------------- /EXAMPLE_VAULT/Showcase/4. Activities Tracking in one file v2.md: -------------------------------------------------------------------------------- 1 | ```dataviewjs 2 | // Define data records 3 | let tracks = dv.pages().where(f => f.tracking == true).file.name; 4 | 5 | let legendData = []; 6 | let combinedData = []; 7 | let combinedList = []; 8 | 9 | // Loop through each data record 10 | for (let track of tracks) { 11 | // Process data for the current track 12 | let data = dv.pages().where(f => f.file.name == track && f.tracking == true); 13 | let t, s, r = data.file.lists.values.filter(x => x.text.match(/^\[\[([\d-]+)\]\](.+)/)); 14 | let color = data.color.values.toString(); 15 | 16 | // Get the count of records for the current track 17 | let recordCount = r.length; 18 | 19 | // Create legend data for the track with superscript count 20 | legendData.push([`
`, `[[${track}]]${recordCount}`]); 21 | 22 | // Process and combine data for combinedData and combinedList 23 | combinedData = combinedData.concat(r.map(x => { 24 | let [_, a, b] = x.text.match(/^\[\[([\d-]+)\]\](.+)/); 25 | let date = moment(a); 26 | let duration = date.fromNow(); 27 | [t, s] = s ? [duration, s] : [duration, duration]; 28 | return [t, b, track, color]; 29 | })); 30 | 31 | combinedList = combinedList.concat(r.map(x => { 32 | let [_, a, b] = x.text.match(/^\[\[([\d-]+)\]\](.+)/); 33 | let date = moment(a); 34 | let duration = date.fromNow(); 35 | [t, s] = s ? [duration, s] : [duration, duration]; 36 | return { Time: t, Event: b, Date: date.format('YYYY-MM-DD'), Track: track, Color: data.color.values.toString() }; 37 | })); 38 | } 39 | 40 | // Sort combinedData by date in descending order 41 | combinedData.sort((a, b) => b[2] - a[2]); 42 | 43 | // Replace "Track" values with HTML squares in combinedData 44 | combinedData = combinedData.map(item => { 45 | let track = item[2]; 46 | let trackSquare = item[3] ? `
` : ''; 47 | return [item[0], trackSquare, item[1]]; 48 | }); 49 | 50 | // Create the legend table 51 | let legendtable = legendData.map(item => `${item[0]} ${item[1]}`); 52 | const markdown = `> [!info]- Data 53 | >${legendtable.join('\n')} 54 | `; 55 | 56 | dv.paragraph(markdown); 57 | 58 | // Create Heatmap 59 | const calendarData = { 60 | showCurrentDayBorder: true, 61 | entries: [], 62 | year: 2024, 63 | } 64 | 65 | // DataviewJS loop to populate calendarData.entries 66 | for (let page of combinedList) { 67 | let dateStr = page.Date; 68 | calendarData.entries.push({ 69 | date: dateStr, 70 | customColor: page.Color, 71 | }) 72 | } 73 | 74 | // Render the Heatmap Calendar 75 | renderHeatmapTracker(this.container, calendarData); 76 | 77 | // Create the combined data table 78 | dv.header(1, "Activities") 79 | dv.table(['Time', 'Track', 'Event'], combinedData); 80 | 81 | ``` 82 | 83 | Added by https://github.com/dxcore35 84 | -------------------------------------------------------------------------------- /src/schemas/validation.ts: -------------------------------------------------------------------------------- 1 | import { ZodError } from "zod"; 2 | import { TrackerDataSchema } from "./trackerData.schema"; 3 | import { TrackerData } from "src/types"; 4 | 5 | // простой "левенштейн" для подсказок по опечаткам 6 | function levenshtein(a: string, b: string): number { 7 | const dp: number[][] = Array.from({ length: a.length + 1 }, () => 8 | Array(b.length + 1).fill(0) 9 | ); 10 | 11 | for (let i = 0; i <= a.length; i++) dp[i][0] = i; 12 | for (let j = 0; j <= b.length; j++) dp[0][j] = j; 13 | 14 | for (let i = 1; i <= a.length; i++) { 15 | for (let j = 1; j <= b.length; j++) { 16 | const cost = a[i - 1] === b[j - 1] ? 0 : 1; 17 | dp[i][j] = Math.min( 18 | dp[i - 1][j] + 1, // delete 19 | dp[i][j - 1] + 1, // insert 20 | dp[i - 1][j - 1] + cost // substitute 21 | ); 22 | } 23 | } 24 | return dp[a.length][b.length]; 25 | } 26 | 27 | const trackerAllowedKeys = [ 28 | "year", 29 | "colorScheme", 30 | "entries", 31 | "showCurrentDayBorder", 32 | "basePath", 33 | "defaultEntryIntensity", 34 | "intensityScaleStart", 35 | "intensityScaleEnd", 36 | "intensityConfig", 37 | "separateMonths", 38 | "heatmapTitle", 39 | "heatmapSubtitle", 40 | "insights", 41 | ]; 42 | 43 | function suggestKeyName(badKey: string): string | null { 44 | let best: { key: string; dist: number } | null = null; 45 | 46 | for (const key of trackerAllowedKeys) { 47 | const dist = levenshtein(badKey, key); 48 | if (!best || dist < best.dist) { 49 | best = { key, dist }; 50 | } 51 | } 52 | 53 | if (!best) return null; 54 | // эмпирически: если расстояние <= 3, считаем это «похоже» 55 | if (best.dist <= 3) return best.key; 56 | return null; 57 | } 58 | 59 | export function validateTrackerData(input: unknown): TrackerData { 60 | const result = TrackerDataSchema.safeParse(input); 61 | 62 | if (result.success) { 63 | return result.data; 64 | } 65 | 66 | // тут можно адаптировать формат под Notice / console и т.д. 67 | const error = result.error; 68 | 69 | const messages = formatZodError(error); 70 | throw new Error("Incorrect format for TrackerData:\n" + messages.join("\n")); 71 | } 72 | 73 | function formatZodError(error: ZodError): string[] { 74 | return error.issues.map((issue) => { 75 | const path = issue.path.join(".") || "root"; 76 | 77 | // extra: подсказки по неизвестным ключам 78 | if (issue.code === "unrecognized_keys") { 79 | const parts: string[] = []; 80 | for (const key of issue.keys) { 81 | const suggestion = suggestKeyName(key); 82 | if (suggestion) { 83 | parts.push( 84 | `Unknown property "${key}" in "${path}". Did you mean "${suggestion}"?` 85 | ); 86 | } else { 87 | parts.push(`Unknown property "${key}" in "${path}".`); 88 | } 89 | } 90 | return parts.join(" "); 91 | } 92 | 93 | // стандартные сообщения 94 | // Примеры: 95 | // entries.0.date: Required 96 | // year: Expected number, received string 97 | return `${path}: ${issue.message}`; 98 | }); 99 | } 100 | -------------------------------------------------------------------------------- /src/components/HeatmapWeekNums/__tests__/HeatmapWeekNums.test.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { render } from "@testing-library/react"; 3 | import { HeatmapWeekNums } from "../HeatmapWeekNums"; 4 | import { HeatmapContext } from "src/context/heatmap/heatmap.context"; 5 | import { TrackerData, TrackerSettings, Box } from "src/types"; 6 | 7 | // Mock dependencies 8 | jest.mock("src/utils/date", () => ({ 9 | getISOWeekNumber: jest.fn(() => 1), 10 | })); 11 | 12 | const mockSettings: TrackerSettings = { 13 | separateMonths: true, 14 | showWeekNums: false, 15 | language: "en", 16 | palettes: {}, 17 | viewTabsVisibility: {}, 18 | weekStartDay: 1, 19 | weekDisplayMode: "even", 20 | }; 21 | 22 | const mockTrackerData: TrackerData = { 23 | year: 2024, 24 | entries: [], 25 | separateMonths: true, 26 | intensityConfig: {} as any, 27 | colorScheme: {} as any, 28 | insights: [], 29 | ui: { 30 | showWeekNums: undefined 31 | }, 32 | showCurrentDayBorder: false, 33 | defaultEntryIntensity: 0 34 | }; 35 | 36 | const mockBoxes: Box[] = [ 37 | { date: "2024-01-01" }, // Week 1 38 | { date: "2024-01-02" }, 39 | { date: "2024-01-03" }, 40 | { date: "2024-01-04" }, 41 | { date: "2024-01-05" }, 42 | { date: "2024-01-06" }, 43 | { date: "2024-01-07" }, 44 | // Spacer column (7 items) 45 | { isSpaceBetweenBox: true }, { isSpaceBetweenBox: true }, { isSpaceBetweenBox: true }, { isSpaceBetweenBox: true }, { isSpaceBetweenBox: true }, { isSpaceBetweenBox: true }, { isSpaceBetweenBox: true }, 46 | ]; 47 | 48 | function renderComponent(settingsOverrides = {}, trackerDataOverrides = {}) { 49 | return render( 50 | 58 | 59 | 60 | ); 61 | } 62 | 63 | describe("HeatmapWeekNums", () => { 64 | it("should render nothing when showWeekNums is false (default)", () => { 65 | const { container } = renderComponent({ showWeekNums: false }); 66 | expect(container.firstChild).toBeNull(); 67 | }); 68 | 69 | it("should render week numbers when settings.showWeekNums is true", () => { 70 | const { container } = renderComponent({ showWeekNums: true }); 71 | // Assuming implementation renders divs with class heatmap-tracker-week-nums 72 | expect(container.querySelector(".heatmap-tracker-week-nums")).toBeTruthy(); 73 | }); 74 | 75 | it("should render week numbers when trackerData.ui.showWeekNums is true, overriding settings", () => { 76 | const { container } = renderComponent( 77 | { showWeekNums: false }, 78 | { ui: { showWeekNums: true } } 79 | ); 80 | expect(container.querySelector(".heatmap-tracker-week-nums")).toBeTruthy(); 81 | }); 82 | 83 | it("should hide week numbers when trackerData.ui.showWeekNums is false, overriding settings", () => { 84 | const { container } = renderComponent( 85 | { showWeekNums: true }, 86 | { ui: { showWeekNums: false } } 87 | ); 88 | expect(container.firstChild).toBeNull(); 89 | }); 90 | }); 91 | -------------------------------------------------------------------------------- /EXAMPLE_VAULT/Documentation with Examples/1. How to start?/1. How to add a heatmap to your obsidian page?.md: -------------------------------------------------------------------------------- 1 | Hi! 2 | 3 | Let's see how you can add a simple heatmap chart on your page. 4 | 5 | **Option 1 (Dataview JS):** 6 | If you want to use this plugin for tracking information from your diary or other pages you can use dataview js plugin to get pages and filter them by specific criteria and after that display this information on the chart. 7 | 8 | In my case I want to get information about `steps` from `Daily Notes` folder. 9 | 10 | ```dataviewjs 11 | 12 | const trackerData = { 13 | year: 2024, 14 | entries: [], 15 | heatmapTitle: "Steps Example" 16 | } 17 | 18 | const PATH_TO_FOLDER = "daily notes"; 19 | const PARAMETER_NAME = "steps"; 20 | 21 | for(let page of dv.pages(`"${PATH_TO_FOLDER}"`).where(p=>p[PARAMETER_NAME])){ 22 | trackerData.entries.push({ 23 | date: page.file.name, 24 | filePath: page.file.path, 25 | intensity: page[PARAMETER_NAME] 26 | }) 27 | } 28 | 29 | trackerData.basePath = PATH_TO_FOLDER; 30 | 31 | renderHeatmapTracker(this.container, trackerData) 32 | 33 | ``` 34 | And it works! 35 | 36 | **Option 2 (Data from table)**: 37 | What if you're writing all your activity history in one file? (Check [[Activities Tracking/Gym]] as an example). 38 | 39 | It's also possible. For more information check [[2. Activities Tracking in one file]] 40 | 41 | ```dataviewjs 42 | const trackingFiles = dv.pages().where(file => file.file.name === "Reading").file.name; 43 | 44 | let activityDetails = []; 45 | 46 | // Process each tracking file to extract data 47 | for (const fileName of trackingFiles) { 48 | // Fetch data for the current file where "tracking" is true 49 | const fileData = dv.pages().where(file => file.file.name === fileName); 50 | const trackColor = fileData.color.values.toString(); // Get the color associated with the track 51 | const trackedEvents = fileData.file.lists.values.filter(listItem => 52 | listItem.text.match(/^\[\[([\d-]+)\]\](.+)/) // Filter events that match the pattern [[YYYY-MM-DD]] Description 53 | ); 54 | 55 | // Transform tracked events into a structured format for further processing 56 | const activityRecords = trackedEvents.map(event => { 57 | const [_, eventDate, eventDescription] = event.text.match(/^\[\[([\d-]+)\]\](.+)/); // Extract date and description 58 | const parsedDate = moment(eventDate); 59 | return { 60 | Date: parsedDate.format('YYYY-MM-DD'), 61 | Color: trackColor 62 | }; 63 | }); 64 | 65 | // Append detailed activity records for use in the heatmap 66 | activityDetails = activityDetails.concat(activityRecords); 67 | } 68 | 69 | // Create the heatmap data object with color mapping and activity entries 70 | const heatmapData = { 71 | year: 2024, 72 | showCurrentDayBorder: true, // Highlight the current day 73 | entries: activityDetails.map(({ Date, Track, Color }) => ({ 74 | date: Date, // Event date 75 | customColor: Color // Track name serves as the "color" key 76 | })) 77 | }; 78 | 79 | // Render the heatmap using the provided tracker rendering function 80 | renderHeatmapTracker(this.container, heatmapData); 81 | ``` 82 | 83 | **Option 3 (When you have heatmap in the same page with the data)** 84 | Check [[3. Headache Tracker in one file]]. 85 | -------------------------------------------------------------------------------- /EXAMPLE_VAULT/Documentation with Examples/2. Features/Display Legend under Heatmap.md: -------------------------------------------------------------------------------- 1 | 2 | Request: 3 | https://github.com/mokkiebear/heatmap-tracker/issues/21 4 | https://github.com/Richardsl/heatmap-calendar-obsidian/discussions/103 5 | 6 | ```dataviewjs 7 | 8 | const trackerData = { 9 | heatmapTitle: "Another way to display Legend", 10 | intensityScaleStart: 0, 11 | intensityScaleEnd: 10, 12 | separateMonths: true, 13 | colorScheme: { 14 | customColors: [ 15 | "rgb(246, 250, 199)", 16 | "rgb(228, 242, 156)", 17 | "rgb(198, 228, 139)", 18 | "rgb(161, 213, 123)", 19 | "rgb(123, 200, 111)", 20 | "rgb(95, 191, 103)", 21 | "rgb(74, 176, 94)", 22 | "rgb(60, 159, 80)", 23 | "rgb(47, 137, 65)", 24 | "rgb(34, 114, 50)", 25 | "rgb(25, 97, 40)" 26 | ], 27 | }, 28 | entries: [ 29 | { 30 | "date": "2025-04-04T00:00:00+01:00", 31 | "intensity": '0' 32 | }, 33 | { 34 | "date": "2025-04-15", 35 | "intensity": '5' 36 | }, 37 | { 38 | "date": "2025-04-16", 39 | "intensity": 8 40 | }, 41 | { 42 | "date": "2025-04-17", 43 | "intensity": 10 44 | }, 45 | ] 46 | } 47 | 48 | const heatmapTrackerEl = renderHeatmapTracker(this.container, trackerData) 49 | 50 | // Adding container for legends and colored boxes on the same line 51 | const legendsContainer = document.createElement('div'); 52 | legendsContainer.style.display = 'flex'; 53 | legendsContainer.style.alignItems = 'center'; // Align vertically 54 | 55 | // Adding first legend with reduced size and custom font 56 | const firstLegend = document.createElement('div'); 57 | firstLegend.innerText = 'LESS'; // Updated to "LESS" with all caps 58 | firstLegend.style.fontSize = '65%'; // Reduce font size by 50% 59 | firstLegend.style.fontFamily = 'IBM plex mono, sans-serif'; // Set custom font 60 | firstLegend.style.textTransform = 'uppercase'; // Convert text to uppercase 61 | firstLegend.style.marginRight = '5px'; // Add margin to separate from the boxes 62 | legendsContainer.appendChild(firstLegend); 63 | 64 | // Adding colored boxes with adjusted size 65 | const legendContainer = document.createElement('div'); 66 | legendContainer.style.display = 'flex'; 67 | legendContainer.style.alignItems = 'center'; // Align vertically 68 | 69 | for (let color of trackerData.colorScheme.customColors) { 70 | const colorBox = document.createElement('div'); 71 | colorBox.style.width = '9px'; // Reduced box width by 50% 72 | colorBox.style.height = '9px'; // Reduced box height by 50% 73 | colorBox.style.backgroundColor = color; 74 | colorBox.style.marginRight = '5px'; // Adjusted margin 75 | legendContainer.appendChild(colorBox); 76 | } 77 | 78 | // Adding second legend after the colored boxes with reduced size and custom font 79 | const secondLegend = document.createElement('div'); 80 | secondLegend.innerText = 'MORE'; // Updated to "MORE" with all caps 81 | secondLegend.style.fontSize = '65%'; // Reduce font size by 50% 82 | secondLegend.style.fontFamily = 'IBM plex mono, sans-serif'; // Set custom font 83 | secondLegend.style.textTransform = 'uppercase'; // Convert text to uppercase 84 | legendContainer.appendChild(secondLegend); 85 | 86 | legendsContainer.appendChild(legendContainer); 87 | legendsContainer.style.marginBottom = "12px"; 88 | 89 | heatmapTrackerEl.appendChild(legendsContainer); 90 | 91 | ``` 92 | -------------------------------------------------------------------------------- /src/views/DocumentationView/DocumentationView.tsx: -------------------------------------------------------------------------------- 1 | function DocumentationView() { 2 | const codeString = ` 3 | const trackerData = { 4 | entries: [{ 5 | date: "2021-01-01", 6 | filePath: page.file.path, 7 | intensity: 1, 8 | // customColor: "#ff0000", 9 | }], 10 | separateMonths: true, 11 | heatmapTitle: "This is the title for your heatmap", 12 | heatmapSubtitle: "This is the subtitle for your heatmap. You can use it as a description.", 13 | showCurrentDayBorder: true, 14 | disableFileCreation: true, // OPTIONAL: If you want to disable new file creation on click 15 | 16 | // OPTIONAL: If you want to define your own color scheme 17 | colorScheme: { 18 | paletteName: "default", // or customColors 19 | customColors: ["#c6e48b", "#7bc96f", "#49af5d", "#2e8840", "#196127"] 20 | }, 21 | 22 | // OPTIONAL: If you want to define your own intensity start/end values. 23 | // Use this if you want to have a custom intensity scale. 24 | // E.g. if you want to track book reading progress only from 30 minutes to 2 hours. 25 | defaultEntryIntensity: 4, 26 | intensityScaleStart: 1, 27 | intensityScaleEnd: 5 28 | } 29 | `; 30 | 31 | return ( 32 |
33 |

34 | Actual Heatmap Tracker API 35 |

36 |
37 |
38 | Since version 1.9 colors property is 39 | removed. Please, remove colors and use{" "} 40 | colorScheme instead (check example below). 41 |
42 |
43 | 44 |
 45 |         {codeString}
 46 |       
47 | 48 |

Color Scheme

49 |

You have 2 (to be honest 3) options how you can define colors

50 |

1. Palette name

51 |

52 | In the Heatmap Tracker plugin settings you can create your own palette 53 | and use the name of this palette for you heatmap. 54 |

55 |
 56 |         {`
 57 |         {
 58 |           colorScheme: {
 59 |             paletteName: "the_name_of_your_palette", // "default" is used by default
 60 |           }
 61 |         }
 62 |         `}
 63 |       
64 |

2. Custom colors

65 |

66 | You can define your own colors for the heatmap. Just provide an array of 67 | colors. In case you're lazy to create a palette. 68 |

69 |
 70 |         {`
 71 |         {
 72 |           colorScheme: {
 73 |             customColors: ["#c6e48b", "#7bc96f", "#49af5d", "#2e8840", "#196127"]
 74 |           }
 75 |         }
 76 |         `}
 77 |       
78 |

3. customColor for entry

79 |

80 | You can define custom color for each entry. Just provide a color in the 81 | entry object. It can be useful if you want to take color from page 82 | itself or other cases. 83 |

84 |
 85 |         {`
 86 |         {
 87 |           entries: [{
 88 |             date: "2021-01-01",
 89 |             intensity: 1,
 90 |             customColor: "#ff0000",
 91 |           }]
 92 |         }
 93 |         `}
 94 |       
95 |
96 | ); 97 | } 98 | 99 | export default DocumentationView; 100 | -------------------------------------------------------------------------------- /src/styles.scss: -------------------------------------------------------------------------------- 1 | @use './styles/heatmap-statistics.scss'; 2 | @use './styles/heatmap-legend.scss'; 3 | @use './styles/heatmap-settings.scss'; 4 | @use './styles/heatmap-tracker-header.scss'; 5 | @use './styles/heatmap-documentation.scss'; 6 | @use './styles/heatmap-breaking-changes.scss'; 7 | @use './styles/heatmap-tracker-footer.scss'; 8 | @use './styles/heatmap-tracker-months.scss'; 9 | @use './styles/heatmap-tracker-days.scss'; 10 | @use './styles/heatmap-tracker-box.scss'; 11 | @use './styles/heatmap-tracker-boxes.scss'; 12 | @use './styles/heatmap-tracker-week-nums.scss'; 13 | 14 | .heatmap-tracker__container, 15 | .heatmap-tracker-legend, 16 | .heatmap-tracker-statistics { 17 | --heatmap-tracker-box-size: 12px; 18 | --heatmap-tracker-box-gap: 2px; 19 | --heatmap-tracker-months-height: 1.5em; 20 | 21 | position: relative; 22 | padding: 0.5em; 23 | overflow: hidden; 24 | 25 | @if $isDev { 26 | div:not(.heatmap-tracker-box) { 27 | outline: 1px dashed red !important; /* Use outline instead of border */ 28 | position: relative; 29 | 30 | &:hover::after { 31 | content: attr(class); 32 | position: absolute; 33 | top: 0; 34 | left: 0; 35 | background-color: rgba(255, 0, 0, 0.8); 36 | color: white; 37 | font-size: 10px; 38 | padding: 2px 4px; 39 | border-radius: 2px; 40 | z-index: 9999; 41 | white-space: nowrap; 42 | } 43 | } 44 | } 45 | } 46 | 47 | .heatmap-tracker-container, 48 | .heatmap-tracker-legend, 49 | .heatmap-tracker-statistics { 50 | border: var(--border-width) solid transparent; 51 | border-radius: var(--radius-s); 52 | transition: border 0.2s; 53 | 54 | &:hover { 55 | border: var(--border-width) solid var(--background-modifier-border-hover); 56 | } 57 | } 58 | 59 | .heatmap-tracker-legend { 60 | overflow-x: hidden; 61 | } 62 | 63 | .heatmap-tracker { 64 | display: flex; 65 | flex-direction: row; 66 | align-items: flex-start; 67 | justify-content: center; 68 | padding: 12px; 69 | } 70 | 71 | .heatmap-tracker-loading::after { 72 | content: ""; 73 | position: absolute; 74 | top: 0; 75 | left: 0; 76 | width: 100%; 77 | height: 100%; 78 | background-color: var(--background-primary-alt); 79 | } 80 | 81 | .heatmap-tracker-graph { 82 | overflow-x: auto; 83 | padding-bottom: 12px; 84 | 85 | font-size: 0.65em; 86 | display: grid; 87 | grid-template-columns: auto; 88 | grid-template-rows: auto 1fr auto; 89 | width: max-content; 90 | } 91 | 92 | .heatmap-tracker-arrow { 93 | cursor: pointer; 94 | } 95 | 96 | .heatmap-tracker-year-display { 97 | margin: 0 12px; 98 | } 99 | 100 | // .heatmap-tracker-boxes .month-feb.isEmpty, 101 | // .heatmap-tracker-boxes .month-apr.isEmpty, 102 | // .heatmap-tracker-boxes .month-jun.isEmpty, 103 | // .heatmap-tracker-boxes .month-aug.isEmpty, 104 | // .heatmap-tracker-boxes .month-oct.isEmpty, 105 | // .heatmap-tracker-boxes .month-dec.isEmpty { 106 | // background: #e2e2e2 !important; 107 | // } 108 | 109 | // .theme-dark .heatmap-tracker-boxes .month-feb.isEmpty, 110 | // .theme-dark .heatmap-tracker-boxes .month-apr.isEmpty, 111 | // .theme-dark .heatmap-tracker-boxes .month-jun.isEmpty, 112 | // .theme-dark .heatmap-tracker-boxes .month-aug.isEmpty, 113 | // .theme-dark .heatmap-tracker-boxes .month-oct.isEmpty, 114 | // .theme-dark .heatmap-tracker-boxes .month-dec.isEmpty { 115 | // background: #424242 !important; 116 | // } 117 | -------------------------------------------------------------------------------- /src/localization/locales/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "settings": { 3 | "language": "Language", 4 | "chooseYourPreferredLanguage": "Choose your preferred language", 5 | "palettes": "Palettes", 6 | "paletteName": "Palette name", 7 | "addNewColorToPalette": "Add a new color to {{paletteName}} palette:", 8 | "addColor": "Add color", 9 | "removeColor": "Remove color", 10 | "saveColor": "Save color", 11 | "addNewPalette": "Add a new palette", 12 | "enterPaletteName": "Enter a palette name to create a new palette", 13 | "addPaletteNote": "Add color palettes that will be available in your heatmap render settings.", 14 | "colorsUsageNote": "You can use these palettes by referencing their name in your heatmap render settings.", 15 | "weekStartDay": "Week start day", 16 | "weekStartDayDescription": "Select the day on which your week starts.", 17 | "separateMonths": "Separate months", 18 | "separateMonthsDescription": "Separate months in your tracker views, globally.", 19 | "weekDisplayMode": { 20 | "label": "Week display mode", 21 | "description": "Choose which days of the week to display: Even days (e.g., Tuesday, Thursday, Saturday), Odd days (e.g., Monday, Wednesday, Friday, Sunday), None (Do not display any days), or All (Display all seven days of the week)." 22 | }, 23 | "tabsVisibility": "Tabs visibility", 24 | "tabsVisibilityDescription": "Show/Hide a tab for {{viewKey}} view", 25 | "showWeekNums": "Show week numbers", 26 | "showWeekNumsDescription": "Display week numbers (1-53) below the heatmap." 27 | }, 28 | "statistics": { 29 | "title": "Statistics", 30 | "totalTrackingDaysThisYear": "Total tracking days this year", 31 | "totalTrackingDays": "Total tracking days", 32 | "currentStreak": "The current streak", 33 | "longestStreak": "The longest streak" 34 | }, 35 | "monthsShort": { 36 | "January": "Jan", 37 | "February": "Feb", 38 | "March": "Mar", 39 | "April": "Apr", 40 | "May": "May", 41 | "June": "Jun", 42 | "July": "Jul", 43 | "August": "Aug", 44 | "September": "Sep", 45 | "October": "Oct", 46 | "November": "Nov", 47 | "December": "Dec" 48 | }, 49 | "weekdaysShort": { 50 | "Sunday": "Sun", 51 | "Monday": "Mon", 52 | "Tuesday": "Tue", 53 | "Wednesday": "Wed", 54 | "Thursday": "Thu", 55 | "Friday": "Fri", 56 | "Saturday": "Sat" 57 | }, 58 | "weekdaysLong": { 59 | "Sunday": "Sunday", 60 | "Monday": "Monday", 61 | "Tuesday": "Tuesday", 62 | "Wednesday": "Wednesday", 63 | "Thursday": "Thursday", 64 | "Friday": "Friday", 65 | "Saturday": "Saturday" 66 | }, 67 | "header": { 68 | "previousYear": "Previous Year", 69 | "nextYear": "Next Year" 70 | }, 71 | "weekDisplayMode": { 72 | "even": "Even", 73 | "odd": "Odd", 74 | "all": "All", 75 | "none": "None" 76 | }, 77 | "view": { 78 | "heatmap-tracker": "Heatmap", 79 | "heatmap-tracker-statistics": "Statistics", 80 | "documentation": "Documentation", 81 | "donation": "Donation", 82 | "legend": "Legend" 83 | }, 84 | "tab": "Tab", 85 | "support.header": "☕️ Hey there!", 86 | "support.text1": "If this plugin makes your day a bit smoother or helps you stay organized, that honestly makes me so happy ❤️", 87 | "support.text2": "I’ve poured dozens of hours into building and improving it — your support helps me keep adding new ideas and updates ✨", 88 | "support.cta": "It takes under 2 minutes, no sign-up needed, and even a $5 coffee makes a real difference ☕️💛" 89 | } -------------------------------------------------------------------------------- /src/localization/locales/hi.json: -------------------------------------------------------------------------------- 1 | { 2 | "settings": { 3 | "language": "भाषा", 4 | "chooseYourPreferredLanguage": "अपनी पसंदीदा भाषा चुनें", 5 | "weekStartDay": "सप्ताह की शुरुआत का दिन", 6 | "weekStartDayDescription": "सप्ताह की शुरुआत के दिन का चयन करें।", 7 | "separateMonths": "महीनों को अलग करें", 8 | "separateMonthsDescription": "अपने ट्रैकर दृश्य में महीनों को अलग-अलग दिखाएं।", 9 | "palettes": "पैलेट्स", 10 | "paletteName": "पैलेट का नाम", 11 | "addNewColorToPalette": "{{paletteName}} पैलेट में नया रंग जोड़ें:", 12 | "addColor": "रंग जोड़ें", 13 | "removeColor": "रंग हटाएं", 14 | "addNewPalette": "नया पैलेट जोड़ें", 15 | "enterPaletteName": "नया पैलेट बनाने के लिए एक पैलेट नाम दर्ज करें", 16 | "addPaletteNote": "उन रंग पैलेट्स को जोड़ें जो आपके हीटमैप रेंडर सेटिंग्स में उपलब्ध होंगे।", 17 | "colorsUsageNote": "आप इन पैलेट्स का उपयोग अपने हीटमैप रेंडर सेटिंग्स में उनके नाम का संदर्भ देकर कर सकते हैं।", 18 | "weekDisplayMode": { 19 | "description": "चुनें कि सप्ताह के कौन से दिन प्रदर्शित करने हैं: सम दिन (जैसे, मंगलवार, गुरुवार, शनिवार), विषम दिन (जैसे, सोमवार, बुधवार, शुक्रवार, रविवार), कोई नहीं (कोई भी दिन प्रदर्शित न करें), या सभी (सभी सात दिन प्रदर्शित करें) \nसप्ताह के दिन)।", 20 | "label": "सप्ताह प्रदर्शन मोड" 21 | }, 22 | "tabsVisibility": "टैब दृश्यता", 23 | "tabsVisibilityDescription": "{{viewKey}} दृश्य के लिए एक टैब दिखाएँ/छिपाएँ", 24 | "saveColor": "रंग बचाओ", 25 | "showWeekNums": "सप्ताह संख्या दिखाएं", 26 | "showWeekNumsDescription": "हीटमैप के नीचे सप्ताह संख्या (1-53) प्रदर्शित करें।" 27 | }, 28 | "statistics": { 29 | "title": "सांख्यिकी", 30 | "totalTrackingDaysThisYear": "इस वर्ष के कुल ट्रैकिंग दिन", 31 | "totalTrackingDays": "कुल ट्रैकिंग दिन", 32 | "currentStreak": "वर्तमान क्रम", 33 | "longestStreak": "सबसे लंबा क्रम" 34 | }, 35 | "monthsShort": { 36 | "January": "जनवरी", 37 | "February": "फरवरी", 38 | "March": "मार्च", 39 | "April": "अप्रैल", 40 | "May": "मई", 41 | "June": "जून", 42 | "July": "जुलाई", 43 | "August": "अगस्त", 44 | "September": "सितंबर", 45 | "October": "अक्टूबर", 46 | "November": "नवंबर", 47 | "December": "दिसंबर" 48 | }, 49 | "weekdaysShort": { 50 | "Sunday": "रवि", 51 | "Monday": "सोम", 52 | "Tuesday": "मंगल", 53 | "Wednesday": "बुध", 54 | "Thursday": "गुरु", 55 | "Friday": "शुक्र", 56 | "Saturday": "शनि" 57 | }, 58 | "weekdaysLong": { 59 | "Sunday": "रविवार", 60 | "Monday": "सोमवार", 61 | "Tuesday": "मंगलवार", 62 | "Wednesday": "बुधवार", 63 | "Thursday": "गुरुवार", 64 | "Friday": "शुक्रवार", 65 | "Saturday": "शनिवार" 66 | }, 67 | "header": { 68 | "previousYear": "पिछला वर्ष", 69 | "nextYear": "अगला वर्ष" 70 | }, 71 | "weekDisplayMode": { 72 | "all": "सभी", 73 | "even": "यहां तक ​​की", 74 | "none": "कोई नहीं", 75 | "odd": "विषम" 76 | }, 77 | "tab": "टैब", 78 | "view": { 79 | "documentation": "प्रलेखन", 80 | "donation": "दान", 81 | "heatmap-tracker": "हीटमैप", 82 | "heatmap-tracker-statistics": "आंकड़े", 83 | "legend": "दंतकथा" 84 | }, 85 | "support.header": "☕️ नमस्ते!", 86 | "support.text1": "अगर यह प्लगइन आपका दिन थोड़ा आसान बनाता है या आपको संगठित रहने में मदद करता है, तो यह मुझे सच-मुच खुश करता है ❤️", 87 | "support.text2": "इसे बनाने और बेहतर करने में मैंने दर्जनों घंटे लगाए हैं — आपका सपोर्ट मुझे नए आइडिया और अपडेट लाने में मदद करता है ✨", 88 | "support.cta": "2 मिनट से भी कम लगता है, कोई साइन-अप नहीं, और $5 का एक कॉफ़ी भी बड़ा फर्क लाता है ☕️💛" 89 | } -------------------------------------------------------------------------------- /src/utils/statistics.ts: -------------------------------------------------------------------------------- 1 | import { Entry, Insight } from "src/types"; 2 | 3 | export function processCustomMetrics(insights: Insight[], yearEntries: Entry[]): Record { 4 | const results: Record = {}; 5 | 6 | insights.forEach((insight) => { 7 | // Calculate the result for the current metric 8 | const result = insight.calculate({ yearEntries }); 9 | // Store the result with the metric name as the key 10 | results[insight.name] = result?.toString() || ""; 11 | }); 12 | 13 | return results; 14 | } 15 | 16 | const mostActiveDayMetric: Insight = { 17 | name: "The most active day of the week", 18 | calculate: ({ yearEntries }: { yearEntries: Entry[] }): string => { 19 | const dayCounts: Record = {}; 20 | 21 | // Map each box to the day of the week 22 | yearEntries.forEach((entry) => { 23 | const date = new Date(entry.date); 24 | const day = date.toLocaleDateString("en-US", { weekday: "long" }); 25 | 26 | if (!dayCounts[day]) { 27 | dayCounts[day] = 0; 28 | } 29 | 30 | dayCounts[day]++; 31 | }); 32 | 33 | // Find the day with the highest count 34 | const mostActiveDay = Object.entries(dayCounts).reduce( 35 | (maxDay, [day, count]) => (count > maxDay.count ? { day, count } : maxDay), 36 | { day: "", count: 0 } 37 | ); 38 | 39 | return mostActiveDay.day; 40 | }, 41 | }; 42 | 43 | const totalValueMetric: Insight = { 44 | name: "Total Value", 45 | calculate: ({ yearEntries }: { yearEntries: Entry[] }) => { 46 | const total = yearEntries.reduce((sum, entry) => sum + (entry.value || 0), 0); 47 | return total.toString(); 48 | }, 49 | }; 50 | 51 | const averageValueMetric: Insight = { 52 | name: "Average Value", 53 | calculate: ({ yearEntries }: { yearEntries: Entry[] }) => { 54 | const total = yearEntries.reduce((sum, entry) => sum + (entry.value || 0), 0); 55 | return (total / yearEntries.length).toFixed(2); // Two decimal places 56 | }, 57 | }; 58 | 59 | const mostFrequentIntensityMetric: Insight = { 60 | name: "Most Frequent Intensity", 61 | calculate: ({ yearEntries }: { yearEntries: Entry[] }) => { 62 | const intensityCounts: Record = {}; 63 | 64 | yearEntries.forEach((entry) => { 65 | const intensity = entry.intensity || 0; 66 | intensityCounts[intensity] = (intensityCounts[intensity] || 0) + 1; 67 | }); 68 | 69 | const mostFrequent = Object.entries(intensityCounts).reduce((a, b) => 70 | b[1] > a[1] ? b : a 71 | ); 72 | 73 | return mostFrequent[0]; // Return the intensity level 74 | }, 75 | }; 76 | 77 | const highestValueDayMetric: Insight = { 78 | name: "Day with the Highest Value", 79 | calculate: ({ yearEntries }: { yearEntries: Entry[] }) => { 80 | const maxEntry = yearEntries.reduce((max, entry) => 81 | (entry.value || 0) > (max.value || 0) ? entry : max 82 | ); 83 | return maxEntry.date || "No data"; 84 | }, 85 | }; 86 | 87 | const intensityDistributionMetric: Insight = { 88 | name: "Intensity Distribution", 89 | calculate: ({ yearEntries }: { yearEntries: Entry[] }) => { 90 | const distribution: Record = {}; 91 | 92 | yearEntries.forEach((entry) => { 93 | const intensity = entry.intensity || 0; 94 | distribution[intensity] = (distribution[intensity] || 0) + 1; 95 | }); 96 | 97 | return Object.entries(distribution) 98 | .map(([intensity, count]) => `Intensity ${intensity}: ${count}`) 99 | .join(", "); 100 | }, 101 | }; --------------------------------------------------------------------------------