From b0195b04def9822b35697ae7cfd325ae11ac5741 Mon Sep 17 00:00:00 2001 From: Samuel Monroe Date: Mon, 15 Dec 2025 11:02:42 +0100 Subject: [PATCH 01/23] Gantt headers with week and days --- vue/package-lock.json | 61 +++++++------ vue/package.json | 2 + .../GanttChartComponent.vue | 85 +++++++++++++++++++ .../components/GanttChartComponent/index.ts | 1 + vue/src/demo/router.ts | 6 ++ vue/src/demo/views/PlaygroundView.vue | 8 ++ 6 files changed, 131 insertions(+), 32 deletions(-) create mode 100644 vue/src/components/GanttChartComponent/GanttChartComponent.vue create mode 100644 vue/src/components/GanttChartComponent/index.ts create mode 100644 vue/src/demo/views/PlaygroundView.vue diff --git a/vue/package-lock.json b/vue/package-lock.json index 2a563175..e10a88eb 100644 --- a/vue/package-lock.json +++ b/vue/package-lock.json @@ -16,6 +16,7 @@ "@tanstack/vue-query": "^5.92.1", "@vee-validate/zod": "^4.15.1", "axios": "^1.13.2", + "luxon": "^3.7.2", "marked": "^17.0.1", "pinia": "^3.0.4", "plotly.js-dist-min": "^3.3.1", @@ -42,6 +43,7 @@ "@tsconfig/node22": "^22.0.5", "@types/eslint-plugin-security": "^3.0.0", "@types/jsdom": "^27.0.0", + "@types/luxon": "^3.7.1", "@types/node": "^24.10.2", "@types/plotly.js-dist-min": "^2.3.4", "@vitejs/plugin-vue": "^6.0.3", @@ -194,7 +196,6 @@ "integrity": "sha512-UlLAnTPrFdNGoFtbSXwcGFQBtQZJCNjaN6hQNP3UPvuNXT1i82N26KL3dZeIpNalWywr9IuQuncaAfUaS1g6sQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.27.1", @@ -608,6 +609,7 @@ "integrity": "sha512-KHp2IflsnGywDjBWDkR9iEqiWSpc8GIi0lgTT3mOElT0PP1tG26P4tmFI2YvAdzgq9RGyoHZQEIEdZy6Ec5xCA==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=6.9.0" } @@ -768,7 +770,6 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": ">=18" }, @@ -815,7 +816,6 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": ">=18" } @@ -3025,7 +3025,6 @@ "integrity": "sha512-PRA911Blj99jR5RMeTunVbNXMF6Lp4vZXnk5GQjcnUWUTsrXtekg/pnmFFI2u/I36Y/2bITGS30GZCXei6uNkA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "json-schema-traverse": "^1.0.0", @@ -3933,7 +3932,8 @@ "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/@types/chai": { "version": "5.2.2", @@ -3998,6 +3998,13 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/luxon": { + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/@types/luxon/-/luxon-3.7.1.tgz", + "integrity": "sha512-H3iskjFIAn5SlJU7OuxUmTEpebK6TKB8rxZShDslBMZJ5u9S//KM1sbdAisiSrqwLQncVjnpi2OK2J51h+4lsg==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/mdx": { "version": "2.0.13", "resolved": "https://registry.npmjs.org/@types/mdx/-/mdx-2.0.13.tgz", @@ -4011,7 +4018,6 @@ "integrity": "sha512-WOhQTZ4G8xZ1tjJTvKOpyEVSGgOTvJAfDK3FNFgELyaTpzhdgHVHeqW8V+UJvzF5BT+/B54T/1S2K6gd9c7bbA==", "devOptional": true, "license": "MIT", - "peer": true, "dependencies": { "undici-types": "~7.16.0" } @@ -4097,7 +4103,6 @@ "integrity": "sha512-VGMpFQGUQWYT9LfnPcX8ouFojyrZ/2w3K5BucvxL/spdNehccKhB4jUyB1yBCXpr2XFm0jkECxgrpXBW2ipoAw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.44.0", "@typescript-eslint/types": "8.44.0", @@ -4345,7 +4350,6 @@ "integrity": "sha512-zedtczX688KehaIaAv7m25CeDLb0gBtAOa2Oi1G1cqvSO5aLSVfH6lpZMJLW8BKYuWMxLQc9/5GYoM+jgvGIrw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@vitest/mocker": "4.0.15", "@vitest/utils": "4.0.15", @@ -4369,7 +4373,6 @@ "integrity": "sha512-94yVpDbb+ykiT7mK6ToonGnq2GIHEQGBTZTAzGxBGQXcVNCh54YKC2/WkfaDzxy0m6Kgw05kq3FYHKHu+wRdIA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@vitest/browser": "4.0.15", "@vitest/mocker": "4.0.15", @@ -4775,7 +4778,6 @@ "integrity": "sha512-+A+yMY8dGixUhHmNdPUxOh0la6uVzun86vAbuMT3hIDxMrAOmn5ILBHm8ajrqHE0t8R9T1dGnde1A5DTnmi3qw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@vitest/utils": "4.0.15", "pathe": "^2.0.3" @@ -5326,7 +5328,6 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -5588,7 +5589,6 @@ "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.2.tgz", "integrity": "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==", "license": "MIT", - "peer": true, "dependencies": { "follow-redirects": "^1.15.6", "form-data": "^4.0.4", @@ -5707,7 +5707,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -6339,7 +6338,8 @@ "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/dotenv": { "version": "17.2.3", @@ -6518,7 +6518,6 @@ "integrity": "sha512-vVC0USHGtMi8+R4Kz8rt6JhEWLxsv9Rnu/lGYbPR8u47B+DCBksq9JarW0zOO7bs37hyOK1l2/oqtbciutL5+Q==", "hasInstallScript": true, "license": "MIT", - "peer": true, "bin": { "esbuild": "bin/esbuild" }, @@ -6583,7 +6582,6 @@ "integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -6644,7 +6642,6 @@ "integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==", "dev": true, "license": "MIT", - "peer": true, "bin": { "eslint-config-prettier": "bin/cli.js" }, @@ -6815,7 +6812,6 @@ "integrity": "sha512-SbR9ZBUFKgvWAbq3RrdCtWaW0IKm6wwUiApxf3BVTNfqUIo4IQQmreMg2iHFJJ6C/0wss3LXURBJ1OwS/MhFcQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", "natural-compare": "^1.4.0", @@ -8689,12 +8685,22 @@ "yallist": "^3.0.2" } }, + "node_modules/luxon": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.7.2.tgz", + "integrity": "sha512-vtEhXh/gNjI9Yg1u4jX/0YVPMvxzHuGgCm6tC5kZyb08yjGWGnqAjGJvcXbqQR2P3MyMEFnRbpcdFS6PBcLqew==", + "license": "MIT", + "engines": { + "node": ">=12" + } + }, "node_modules/lz-string": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", "dev": true, "license": "MIT", + "peer": true, "bin": { "lz-string": "bin/bin.js" } @@ -9459,7 +9465,6 @@ "integrity": "sha512-aFi5B0WovBHTEvpM3DzXTUaeN6eN0qWnTkKx4NQaH4Wvcmc153PdaY2UBdSYKaGYw+UyWXSVyxDUg5DoPEttjw==", "dev": true, "license": "Apache-2.0", - "peer": true, "dependencies": { "playwright-core": "1.56.1" }, @@ -9521,7 +9526,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -9581,7 +9585,6 @@ "integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==", "dev": true, "license": "MIT", - "peer": true, "bin": { "prettier": "bin/prettier.cjs" }, @@ -9611,6 +9614,7 @@ "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "ansi-regex": "^5.0.1", "ansi-styles": "^5.0.0", @@ -9626,6 +9630,7 @@ "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=10" }, @@ -9893,7 +9898,6 @@ "integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -9904,7 +9908,6 @@ "integrity": "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "scheduler": "^0.26.0" }, @@ -9917,7 +9920,8 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/read-package-json-fast": { "version": "4.0.0", @@ -10087,7 +10091,6 @@ "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.46.1.tgz", "integrity": "sha512-33xGNBsDJAkzt0PvninskHlWnTIPgDtTwhg0U38CUoNP/7H6wI2Cz6dUeoNPbjdTdsYTGuiFFASuUOWovH0SyQ==", "license": "MIT", - "peer": true, "dependencies": { "@types/estree": "1.0.8" }, @@ -10395,7 +10398,6 @@ "integrity": "sha512-P33uUf76J1VmhxV8CyC+M0/zoop9oMYXRypNxuvgvXwmun/9yZtu5ThNgp6MkF9hEMA53X7Gf+P/P5Jn/TYPng==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@storybook/global": "^5.0.0", "@storybook/icons": "^2.0.0", @@ -10654,8 +10656,7 @@ "version": "4.1.18", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.18.tgz", "integrity": "sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/tailwindcss-primeui": { "version": "0.6.1", @@ -10900,7 +10901,6 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "devOptional": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -11078,7 +11078,6 @@ "resolved": "https://registry.npmjs.org/vite/-/vite-7.2.7.tgz", "integrity": "sha512-ITcnkFeR3+fI8P1wMgItjGrR10170d8auB4EpMLPqmx6uxElH3a/hHGQabSHKdqd4FXWO1nFIp9rRn7JQ34ACQ==", "license": "MIT", - "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", @@ -11358,7 +11357,6 @@ "integrity": "sha512-n1RxDp8UJm6N0IbJLQo+yzLZ2sQCDyl1o0LeugbPWf8+8Fttp29GghsQBjYJVmWq3gBFfe9Hs1spR44vovn2wA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@vitest/expect": "4.0.15", "@vitest/mocker": "4.0.15", @@ -11555,7 +11553,6 @@ "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.25.tgz", "integrity": "sha512-YLVdgv2K13WJ6n+kD5owehKtEXwdwXuj2TTyJMsO7pSeKw2bfRNZGjhB7YzrpbMYj5b5QsUebHpOqR3R3ziy/g==", "license": "MIT", - "peer": true, "dependencies": { "@vue/compiler-dom": "3.5.25", "@vue/compiler-sfc": "3.5.25", diff --git a/vue/package.json b/vue/package.json index dcc43f8c..725ca5fc 100644 --- a/vue/package.json +++ b/vue/package.json @@ -74,6 +74,7 @@ "@vee-validate/zod": "^4.15.1", "axios": "^1.13.2", "marked": "^17.0.1", + "luxon": "^3.7.2", "pinia": "^3.0.4", "plotly.js-dist-min": "^3.3.1", "primeicons": "^7.0.0", @@ -96,6 +97,7 @@ "@tsconfig/node22": "^22.0.5", "@types/eslint-plugin-security": "^3.0.0", "@types/jsdom": "^27.0.0", + "@types/luxon": "^3.7.1", "@types/node": "^24.10.2", "@types/plotly.js-dist-min": "^2.3.4", "@vitejs/plugin-vue": "^6.0.3", diff --git a/vue/src/components/GanttChartComponent/GanttChartComponent.vue b/vue/src/components/GanttChartComponent/GanttChartComponent.vue new file mode 100644 index 00000000..fe9fc817 --- /dev/null +++ b/vue/src/components/GanttChartComponent/GanttChartComponent.vue @@ -0,0 +1,85 @@ + + + diff --git a/vue/src/components/GanttChartComponent/index.ts b/vue/src/components/GanttChartComponent/index.ts new file mode 100644 index 00000000..906804f8 --- /dev/null +++ b/vue/src/components/GanttChartComponent/index.ts @@ -0,0 +1 @@ +export { default as GanttChartComponent } from './GanttChartComponent.vue' diff --git a/vue/src/demo/router.ts b/vue/src/demo/router.ts index 29463571..a11a7b8d 100644 --- a/vue/src/demo/router.ts +++ b/vue/src/demo/router.ts @@ -1,6 +1,7 @@ import { createRouter, createWebHistory } from 'vue-router' import DemoView from './DemoView.vue' import ShowcaseView from './ShowcaseView.vue' +import PlaygroundView from '@/demo/views/PlaygroundView.vue' const router = createRouter({ history: createWebHistory(import.meta.env.BASE_URL), @@ -15,6 +16,11 @@ const router = createRouter({ name: 'showcase', component: ShowcaseView, }, + { + path: '/playground', + name: 'playground', + component: PlaygroundView + }, { path: '/:pathMatch(.*)*', name: '404', diff --git a/vue/src/demo/views/PlaygroundView.vue b/vue/src/demo/views/PlaygroundView.vue new file mode 100644 index 00000000..1b5a786c --- /dev/null +++ b/vue/src/demo/views/PlaygroundView.vue @@ -0,0 +1,8 @@ + + + From 4876e35fdf1fe5239dcdb83f7ee558282a69e547 Mon Sep 17 00:00:00 2001 From: Samuel Monroe Date: Mon, 15 Dec 2025 11:10:11 +0100 Subject: [PATCH 02/23] Add Month Header and Grouping --- .../GanttChartComponent.vue | 42 ++++++++++++++++++- 1 file changed, 40 insertions(+), 2 deletions(-) diff --git a/vue/src/components/GanttChartComponent/GanttChartComponent.vue b/vue/src/components/GanttChartComponent/GanttChartComponent.vue index fe9fc817..e64bd356 100644 --- a/vue/src/components/GanttChartComponent/GanttChartComponent.vue +++ b/vue/src/components/GanttChartComponent/GanttChartComponent.vue @@ -3,6 +3,19 @@
+ +
+
+ {{ month.label }} +
+
{ const dateRange: ComputedRef = computed(() => { const dates = [] - const currentDate = new Date(2025, 0, 2) - const endDate = new Date(2025, 11, 31) + const currentDate = new Date(2026, 0, 1) + const endDate = new Date(2026, 11, 31) while (currentDate <= endDate) { dates.push(new Date(currentDate)) @@ -82,4 +95,29 @@ const weeks = computed(() => { return groups }) + +const months = computed(() => { + type MonthGroup = { month: number; year: number; label: string; days: Date[] } + const groups: MonthGroup[] = [] + + dateRange.value.forEach((date) => { + const luxonDate = DateTime.fromJSDate(date) + const keyYear = luxonDate.year + const keyMonth = luxonDate.month + const existing = groups.find((m) => m.year === keyYear && m.month === keyMonth) + + if (existing) { + existing.days.push(date) + } else { + groups.push({ + month: keyMonth, + year: keyYear, + label: luxonDate.toFormat('MMM yy'), + days: [date], + }) + } + }) + + return groups +}) From b86595c4dde8a815e8080b5818ffcd703e53a154 Mon Sep 17 00:00:00 2001 From: Samuel Monroe Date: Mon, 15 Dec 2025 11:53:42 +0100 Subject: [PATCH 03/23] Sticky scrolling and placeholder values --- .../GanttChartComponent.vue | 51 +++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/vue/src/components/GanttChartComponent/GanttChartComponent.vue b/vue/src/components/GanttChartComponent/GanttChartComponent.vue index e64bd356..d6904a8f 100644 --- a/vue/src/components/GanttChartComponent/GanttChartComponent.vue +++ b/vue/src/components/GanttChartComponent/GanttChartComponent.vue @@ -5,6 +5,13 @@
+
+
+
+ Dates +
+ + +
+
+
+ {{ row.title }} +
+
+
+
@@ -52,6 +93,7 @@ import { computed, type ComputedRef, onMounted, useTemplateRef } from 'vue' import { DateTime } from 'luxon' const DAY_CELL_WIDTH_PX = 40 // match Tailwind w-10 (2.5rem) at base 16px +const ROW_HEADER_WIDTH_PX = 160 const ganntChart = useTemplateRef('ganntChart') @@ -72,6 +114,15 @@ const dateRange: ComputedRef = computed(() => { return dates }) +const timelineWidthPx = computed(() => dateRange.value.length * DAY_CELL_WIDTH_PX) + +const rows = computed(() => { + return Array.from({ length: 10 }, (_, index) => ({ + id: index + 1, + title: `Row ${index + 1}`, + })) +}) + const weeks = computed(() => { type WeekGroup = { weekYear: number; weekNumber: number; days: Date[] } const groups: WeekGroup[] = [] From 2a9d9de2c0385f502cacf933a619f8b02b8fd5a2 Mon Sep 17 00:00:00 2001 From: Samuel Monroe Date: Mon, 15 Dec 2025 13:30:17 +0100 Subject: [PATCH 04/23] Remove placeholder values --- .../GanttChartComponent.vue | 51 ------------------- 1 file changed, 51 deletions(-) diff --git a/vue/src/components/GanttChartComponent/GanttChartComponent.vue b/vue/src/components/GanttChartComponent/GanttChartComponent.vue index d6904a8f..e64bd356 100644 --- a/vue/src/components/GanttChartComponent/GanttChartComponent.vue +++ b/vue/src/components/GanttChartComponent/GanttChartComponent.vue @@ -5,13 +5,6 @@
-
-
-
- Dates -
- - -
-
-
- {{ row.title }} -
-
-
-
@@ -93,7 +52,6 @@ import { computed, type ComputedRef, onMounted, useTemplateRef } from 'vue' import { DateTime } from 'luxon' const DAY_CELL_WIDTH_PX = 40 // match Tailwind w-10 (2.5rem) at base 16px -const ROW_HEADER_WIDTH_PX = 160 const ganntChart = useTemplateRef('ganntChart') @@ -114,15 +72,6 @@ const dateRange: ComputedRef = computed(() => { return dates }) -const timelineWidthPx = computed(() => dateRange.value.length * DAY_CELL_WIDTH_PX) - -const rows = computed(() => { - return Array.from({ length: 10 }, (_, index) => ({ - id: index + 1, - title: `Row ${index + 1}`, - })) -}) - const weeks = computed(() => { type WeekGroup = { weekYear: number; weekNumber: number; days: Date[] } const groups: WeekGroup[] = [] From 5c3ca909330fcc6f2ba14712ec7eb2eb6e706fa0 Mon Sep 17 00:00:00 2001 From: Samuel Monroe Date: Mon, 15 Dec 2025 14:00:39 +0100 Subject: [PATCH 05/23] Dedicated component for Gantt chart header --- .../GanttChartComponent.vue | 120 ++---------------- .../GanttChartComponent/GanttChartHeader.vue | 120 ++++++++++++++++++ 2 files changed, 130 insertions(+), 110 deletions(-) create mode 100644 vue/src/components/GanttChartComponent/GanttChartHeader.vue diff --git a/vue/src/components/GanttChartComponent/GanttChartComponent.vue b/vue/src/components/GanttChartComponent/GanttChartComponent.vue index e64bd356..6eaabcfa 100644 --- a/vue/src/components/GanttChartComponent/GanttChartComponent.vue +++ b/vue/src/components/GanttChartComponent/GanttChartComponent.vue @@ -1,123 +1,23 @@ diff --git a/vue/src/components/GanttChartComponent/GanttChartHeader.vue b/vue/src/components/GanttChartComponent/GanttChartHeader.vue new file mode 100644 index 00000000..4eea415d --- /dev/null +++ b/vue/src/components/GanttChartComponent/GanttChartHeader.vue @@ -0,0 +1,120 @@ + + + From 2398d9c0908a99c7d9a1b0cc7e457189de22387c Mon Sep 17 00:00:00 2001 From: Samuel Monroe Date: Mon, 15 Dec 2025 14:53:08 +0100 Subject: [PATCH 06/23] Add rows with virtual rendering and horizontal scrolling --- vue/package-lock.json | 45 +++++++++++++++++++ vue/package.json | 3 +- .../GanttChartComponent.vue | 37 ++++++++++++--- .../GanttChartComponent/GanttChartHeader.vue | 21 +++------ .../GanttChartComponent/GanttChartRow.vue | 27 +++++++++++ 5 files changed, 111 insertions(+), 22 deletions(-) create mode 100644 vue/src/components/GanttChartComponent/GanttChartRow.vue diff --git a/vue/package-lock.json b/vue/package-lock.json index e10a88eb..43013eca 100644 --- a/vue/package-lock.json +++ b/vue/package-lock.json @@ -15,6 +15,7 @@ "@tailwindcss/vite": "^4.1.18", "@tanstack/vue-query": "^5.92.1", "@vee-validate/zod": "^4.15.1", + "@vueuse/core": "^14.1.0", "axios": "^1.13.2", "luxon": "^3.7.2", "marked": "^17.0.1", @@ -4057,6 +4058,12 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/web-bluetooth": { + "version": "0.0.21", + "resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.21.tgz", + "integrity": "sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA==", + "license": "MIT" + }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "8.44.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.44.0.tgz", @@ -5312,6 +5319,44 @@ } } }, + "node_modules/@vueuse/core": { + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/@vueuse/core/-/core-14.1.0.tgz", + "integrity": "sha512-rgBinKs07hAYyPF834mDTigH7BtPqvZ3Pryuzt1SD/lg5wEcWqvwzXXYGEDb2/cP0Sj5zSvHl3WkmMELr5kfWw==", + "license": "MIT", + "dependencies": { + "@types/web-bluetooth": "^0.0.21", + "@vueuse/metadata": "14.1.0", + "@vueuse/shared": "14.1.0" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "vue": "^3.5.0" + } + }, + "node_modules/@vueuse/metadata": { + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-14.1.0.tgz", + "integrity": "sha512-7hK4g015rWn2PhKcZ99NyT+ZD9sbwm7SGvp7k+k+rKGWnLjS/oQozoIZzWfCewSUeBmnJkIb+CNr7Zc/EyRnnA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@vueuse/shared": { + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-14.1.0.tgz", + "integrity": "sha512-EcKxtYvn6gx1F8z9J5/rsg3+lTQnvOruQd8fUecW99DCK04BkWD7z5KQ/wTAx+DazyoEE9dJt/zV8OIEQbM6kw==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "vue": "^3.5.0" + } + }, "node_modules/abbrev": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-2.0.0.tgz", diff --git a/vue/package.json b/vue/package.json index 725ca5fc..74a63504 100644 --- a/vue/package.json +++ b/vue/package.json @@ -72,9 +72,10 @@ "@tailwindcss/vite": "^4.1.18", "@tanstack/vue-query": "^5.92.1", "@vee-validate/zod": "^4.15.1", + "@vueuse/core": "^14.1.0", "axios": "^1.13.2", - "marked": "^17.0.1", "luxon": "^3.7.2", + "marked": "^17.0.1", "pinia": "^3.0.4", "plotly.js-dist-min": "^3.3.1", "primeicons": "^7.0.0", diff --git a/vue/src/components/GanttChartComponent/GanttChartComponent.vue b/vue/src/components/GanttChartComponent/GanttChartComponent.vue index 6eaabcfa..0a49e826 100644 --- a/vue/src/components/GanttChartComponent/GanttChartComponent.vue +++ b/vue/src/components/GanttChartComponent/GanttChartComponent.vue @@ -1,23 +1,46 @@ diff --git a/vue/src/components/GanttChartComponent/GanttChartHeader.vue b/vue/src/components/GanttChartComponent/GanttChartHeader.vue index 4eea415d..a230f85f 100644 --- a/vue/src/components/GanttChartComponent/GanttChartHeader.vue +++ b/vue/src/components/GanttChartComponent/GanttChartHeader.vue @@ -51,29 +51,22 @@ From 189926aeac64a2c9579fe2cf0aaa1bc383f257f7 Mon Sep 17 00:00:00 2001 From: Samuel Monroe Date: Mon, 15 Dec 2025 15:22:59 +0100 Subject: [PATCH 07/23] Make use of resizeObserver to dynamically adapt the virtual renderer for the rows --- .../GanttChartComponent.vue | 27 +++++++++++++------ .../GanttChartComponent/GanttChartHeader.vue | 2 +- 2 files changed, 20 insertions(+), 9 deletions(-) diff --git a/vue/src/components/GanttChartComponent/GanttChartComponent.vue b/vue/src/components/GanttChartComponent/GanttChartComponent.vue index 0a49e826..f7508ca3 100644 --- a/vue/src/components/GanttChartComponent/GanttChartComponent.vue +++ b/vue/src/components/GanttChartComponent/GanttChartComponent.vue @@ -1,9 +1,9 @@ From 896754314748603b1cbce608538c6f1735bc64bb Mon Sep 17 00:00:00 2001 From: Samuel Monroe Date: Tue, 16 Dec 2025 13:42:31 +0100 Subject: [PATCH 11/23] Include GanttChartComponent export in lib.ts --- vue/src/lib.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/vue/src/lib.ts b/vue/src/lib.ts index 77ad8fd2..9ae9f1d8 100644 --- a/vue/src/lib.ts +++ b/vue/src/lib.ts @@ -9,6 +9,7 @@ export { PrimeVue as PrimeVueLibConfig } // Export your library components, functions and props in this file export * from '@/components/AutoroutedBreadcrumb' export * from '@/components/ControlBarComponent' +export * from '@/components/GanttChartComponent' export * from '@/components/DashboardComponent' export * from '@/components/FormComponent' export * from '@/components/NetworkButton' From ac7ed94288298b0c19c3d74b2e42efd17c50184a Mon Sep 17 00:00:00 2001 From: Fabian Souris Date: Fri, 19 Dec 2025 14:20:05 +0100 Subject: [PATCH 12/23] shade on weekends --- .../GanttChartComponent/GanttChartRow.vue | 37 ++++++++++++++++++- 1 file changed, 35 insertions(+), 2 deletions(-) diff --git a/vue/src/components/GanttChartComponent/GanttChartRow.vue b/vue/src/components/GanttChartComponent/GanttChartRow.vue index 62ef9ec2..9b040802 100644 --- a/vue/src/components/GanttChartComponent/GanttChartRow.vue +++ b/vue/src/components/GanttChartComponent/GanttChartRow.vue @@ -6,8 +6,8 @@ Header
Content
@@ -22,11 +22,44 @@ export interface GanttChartRowProps { const { dateRange } = defineProps() import { computed } from 'vue' +import { DateTime } from 'luxon' const ROW_HEIGHT_PX = 30 const DAY_CELL_WIDTH_PX = 40 // match Tailwind w-10 (2.5rem) at base 16px +const WEEK_DAYS = 7 const lineWidth = computed(() => { return `${dateRange.length * DAY_CELL_WIDTH_PX}px` }) + +const weekPattern = computed(() => { + const base = dateRange[0] ? DateTime.fromJSDate(dateRange[0]).startOf('week') : DateTime.now().startOf('week') + const stops = Array.from({ length: WEEK_DAYS }, (_, index) => { + const day = base.plus({ days: index }) + const color = day.isWeekend ? 'rgb(243 244 246)' : 'transparent' + const start = index * DAY_CELL_WIDTH_PX + const end = (index + 1) * DAY_CELL_WIDTH_PX + return `${color} ${start}px ${end}px` + }) + + return `linear-gradient(90deg, ${stops.join(', ')})` +}) + +const gridStyle = computed(() => { + const firstDate = dateRange[0] + const weekStart = firstDate ? DateTime.fromJSDate(firstDate).startOf('week') : DateTime.now().startOf('week') + const offsetDays = firstDate ? Math.floor(DateTime.fromJSDate(firstDate).diff(weekStart, 'days').days) : 0 + const offsetPx = offsetDays * DAY_CELL_WIDTH_PX + const gridLines = `repeating-linear-gradient(90deg, transparent 0, transparent ${ + DAY_CELL_WIDTH_PX - 1 + }px, rgb(229 231 235) ${DAY_CELL_WIDTH_PX - 1}px, rgb(229 231 235) ${DAY_CELL_WIDTH_PX}px)` + + return { + width: lineWidth.value, + backgroundImage: `${gridLines}, ${weekPattern.value}`, + backgroundSize: `${DAY_CELL_WIDTH_PX}px 100%, ${DAY_CELL_WIDTH_PX * WEEK_DAYS}px 100%`, + backgroundPosition: `0 0, -${offsetPx}px 0`, + backgroundRepeat: 'repeat-x, repeat-x', + } +}) From a74521cc7863470941a0ac6197a07a22b572cb0f Mon Sep 17 00:00:00 2001 From: Fabian Souris Date: Mon, 22 Dec 2025 17:23:25 +0100 Subject: [PATCH 13/23] implement base featureset for Gantt Chart --- .../GanttChartComponent.spec.ts | 154 ++++++++++ .../GanttChartComponent.stories.ts | 113 ++++++- .../GanttChartComponent.vue | 97 ++++-- .../GanttChartComponent/GanttChartHeader.vue | 118 ++++++-- .../GanttChartComponent/GanttChartRow.vue | 285 ++++++++++++++++-- .../GanttChartComponent/ganttChartLayout.ts | 149 +++++++++ .../GanttChartComponent/ganttChartTypes.ts | 16 + vue/src/demo/views/PlaygroundView.vue | 73 ++++- 8 files changed, 925 insertions(+), 80 deletions(-) create mode 100644 vue/src/components/GanttChartComponent/GanttChartComponent.spec.ts create mode 100644 vue/src/components/GanttChartComponent/ganttChartLayout.ts create mode 100644 vue/src/components/GanttChartComponent/ganttChartTypes.ts diff --git a/vue/src/components/GanttChartComponent/GanttChartComponent.spec.ts b/vue/src/components/GanttChartComponent/GanttChartComponent.spec.ts new file mode 100644 index 00000000..4fb2dfd6 --- /dev/null +++ b/vue/src/components/GanttChartComponent/GanttChartComponent.spec.ts @@ -0,0 +1,154 @@ +import { describe, it, expect, vi } from 'vitest' +import { mount } from '@vue/test-utils' +import GanttChartComponent from './GanttChartComponent.vue' +import { getWeekColumns } from './ganttChartLayout' +import type { GanttChartRowData } from './ganttChartTypes' + +vi.mock('@vueuse/core', async () => { + const vue = await import('vue') + return { + useResizeObserver: () => { + /* no-op for tests */ + }, + useVirtualList: (source: unknown) => { + const list = vue.computed(() => { + const value = vue.unref(source) ?? [] + return (value as Array).map((data, index) => ({ data, index })) + }) + return { + list, + containerProps: { + ref: vue.ref(null), + onScroll: () => { + /* no-op */ + }, + style: {}, + }, + wrapperProps: vue.computed(() => ({ + style: { width: '100%', height: '100%', marginTop: '0px' }, + })), + } + }, + } +}) + +const buildDateRange = (start: Date, end: Date) => { + const dates = [] + const cursor = new Date(start) + while (cursor <= end) { + dates.push(new Date(cursor)) + cursor.setDate(cursor.getDate() + 1) + } + return dates +} + +const baseRows: GanttChartRowData[] = [ + { + id: 1, + label: 'Row 1', + header: 'Line A', + activities: [ + { + id: 'bar-1', + label: 'Optimized', + startDate: new Date(2026, 0, 3), + endDate: new Date(2026, 0, 5), + visualType: 'bar', + colorClass: 'bg-emerald-400/80', + }, + ], + }, +] + +describe('GanttChartComponent', () => { + it('renders top-left header label and row headers', () => { + const wrapper = mount(GanttChartComponent, { + props: { + dateRange: buildDateRange(new Date(2026, 0, 1), new Date(2026, 0, 7)), + rows: baseRows, + headerLabel: 'Line', + }, + global: { + directives: { + tooltip: () => { + /* no-op */ + }, + }, + }, + }) + + expect(wrapper.text()).toContain('Line') + expect(wrapper.text()).toContain('Line A') + }) + + it('emits activityClick with activity and row data', async () => { + const wrapper = mount(GanttChartComponent, { + props: { + dateRange: buildDateRange(new Date(2026, 0, 1), new Date(2026, 0, 7)), + rows: baseRows, + }, + global: { + directives: { + tooltip: () => { + /* no-op */ + }, + }, + }, + }) + + await wrapper.find('span').trigger('click') + + const emitted = wrapper.emitted('activityClick') as unknown[] | undefined + expect(emitted).toBeTruthy() + const payload = emitted?.[0] as Array<{ label?: string; id?: number }> + expect(payload?.[0]?.label).toBe('Optimized') + expect(payload?.[1]?.id).toBe(1) + }) + + it('renders weekly headers with month spans across overlapping weeks', () => { + const start = new Date(2026, 0, 25) + const end = new Date(2026, 1, 10) + const dateRange = buildDateRange(start, end) + const expectedWeeks = getWeekColumns(start, end).length + + const wrapper = mount(GanttChartComponent, { + props: { + dateRange, + rows: baseRows, + viewMode: 'week', + }, + global: { + directives: { + tooltip: () => { + /* no-op */ + }, + }, + }, + }) + + const weekLabels = wrapper.findAll('[aria-label^="Week "]') + expect(weekLabels.length).toBe(expectedWeeks) + expect(wrapper.text()).toContain('Jan 2026') + expect(wrapper.text()).toContain('Feb 2026') + }) + + it('hides day headers in weekly view', () => { + const dateRange = buildDateRange(new Date(2026, 0, 1), new Date(2026, 0, 7)) + const wrapper = mount(GanttChartComponent, { + props: { + dateRange, + rows: baseRows, + viewMode: 'week', + }, + global: { + directives: { + tooltip: () => { + /* no-op */ + }, + }, + }, + }) + + expect(wrapper.html()).not.toContain('Thu Jan 01 2026') + }) +}) diff --git a/vue/src/components/GanttChartComponent/GanttChartComponent.stories.ts b/vue/src/components/GanttChartComponent/GanttChartComponent.stories.ts index 1ab7fc9e..1b2c6341 100644 --- a/vue/src/components/GanttChartComponent/GanttChartComponent.stories.ts +++ b/vue/src/components/GanttChartComponent/GanttChartComponent.stories.ts @@ -2,10 +2,84 @@ import { type Meta, type StoryObj } from '@storybook/vue3-vite' import { expect, within } from 'storybook/test' import GanttChartComponent from './GanttChartComponent.vue' +import type { GanttChartActivityData, GanttChartRowData } from './ganttChartTypes' + +const buildDateRange = (start: Date, end: Date) => { + const dates = [] + const cursor = new Date(start) + + while (cursor <= end) { + dates.push(new Date(cursor)) + cursor.setDate(cursor.getDate() + 1) + } + + return dates +} + +const buildRows = (count: number): GanttChartRowData[] => { + return Array.from({ length: count }, (_, index) => { + const startDay = (index * 3) % 28 + const baseDate = new Date(2026, 0, 1 + startDay) + const baseActivities: GanttChartActivityData[] = [ + { + id: 'planned', + label: 'Planned', + startDate: new Date(baseDate), + endDate: new Date(2026, 0, 6 + startDay), + visualType: 'stripe', + color: 'rgba(59, 130, 246, 0.2)', + }, + { + id: 'optimized', + label: 'Optimized', + startDate: new Date(2026, 0, 3 + startDay), + endDate: new Date(2026, 0, 9 + startDay), + visualType: 'bar', + colorClass: 'bg-emerald-400/80', + }, + { + id: 'desired', + label: 'Desired', + startDate: new Date(2026, 0, 5 + startDay), + endDate: new Date(2026, 0, 12 + startDay), + visualType: 'mini', + colorClass: 'bg-amber-400/80', + }, + ] + const extraActivities: GanttChartActivityData[] = + index % 3 === 0 + ? [ + { + id: 'desired-2', + label: 'Alt', + startDate: new Date(2026, 0, 7 + startDay), + endDate: new Date(2026, 0, 10 + startDay), + visualType: 'mini', + colorClass: 'bg-amber-500/80', + }, + ] + : [] + + return { + id: index, + label: `Row ${index + 1}`, + header: `Line ${index + 1}`, + activities: [...baseActivities, ...extraActivities], + } + }) +} const meta: Meta = { title: 'Components/GanttChart', component: GanttChartComponent, + args: { + dateRange: buildDateRange(new Date(2026, 0, 1), new Date(2026, 1, 28)), + rows: buildRows(40), + headerLabel: 'Line', + viewMode: 'day', + showWeekendShading: true, + stackMiniActivities: true, + }, parameters: { layout: 'fullscreen', docs: { @@ -19,6 +93,33 @@ The story wraps the component in a fixed-height container to demonstrate scrolli }, }, }, + argTypes: { + viewMode: { + control: { type: 'inline-radio' }, + options: ['day', 'week'], + }, + showWeekendShading: { + control: { type: 'boolean' }, + }, + stackMiniActivities: { + control: { type: 'boolean' }, + }, + dateRange: { + control: { type: 'object' }, + }, + rows: { + control: { type: 'object' }, + }, + headerLabel: { + control: { type: 'text' }, + }, + activityTooltip: { + control: false, + }, + activityClick: { + control: false, + }, + }, } export default meta @@ -26,17 +127,19 @@ export default meta type Story = StoryObj export const Default: Story = { - render: () => ({ + render: (args) => ({ components: { GanttChartComponent }, + setup() { + return { args } + }, template: `
- +
`, }), - play: async ({ canvasElement }) => { + play: async ({ canvasElement, args }) => { const canvas = within(canvasElement) - const headers = canvas.getAllByText('Header') - await expect(headers.length).toBeGreaterThan(0) + await expect(canvas.getByText(args.headerLabel ?? 'Header')).toBeInTheDocument() }, } diff --git a/vue/src/components/GanttChartComponent/GanttChartComponent.vue b/vue/src/components/GanttChartComponent/GanttChartComponent.vue index e9d87206..87ef6f5b 100644 --- a/vue/src/components/GanttChartComponent/GanttChartComponent.vue +++ b/vue/src/components/GanttChartComponent/GanttChartComponent.vue @@ -1,14 +1,31 @@ diff --git a/vue/src/components/GanttChartComponent/GanttChartHeader.vue b/vue/src/components/GanttChartComponent/GanttChartHeader.vue index 8226fde5..28170f84 100644 --- a/vue/src/components/GanttChartComponent/GanttChartHeader.vue +++ b/vue/src/components/GanttChartComponent/GanttChartHeader.vue @@ -1,45 +1,78 @@
@@ -86,6 +86,7 @@ ``` @@ -89,6 +95,7 @@ Activities support a `visualType`: @@ -115,6 +122,14 @@ export type GanttChartRowData = { header?: string activities: GanttChartActivityData[] } + +export type GanttChartLinkData = { + id?: string | number + fromId: string | number + toId: string | number + type?: 'finish-start' | 'start-start' + color?: string +} ``` ## Translation Notes diff --git a/vue/src/components/GanttChartComponent/GanttChartComponent.spec.ts b/vue/src/components/GanttChartComponent/GanttChartComponent.spec.ts index c65ea008..d50f8ef1 100644 --- a/vue/src/components/GanttChartComponent/GanttChartComponent.spec.ts +++ b/vue/src/components/GanttChartComponent/GanttChartComponent.spec.ts @@ -62,6 +62,21 @@ const baseRows: GanttChartRowData[] = [ }, ], }, + { + id: 2, + label: 'Row 2', + header: 'Line B', + activities: [ + { + id: 'bar-2', + label: 'Optimized', + startDate: new Date(2026, 0, 5), + endDate: new Date(2026, 0, 7), + visualType: 'bar', + colorClass: 'bg-emerald-400/80', + }, + ], + }, ] describe('GanttChartComponent', () => { @@ -109,6 +124,25 @@ describe('GanttChartComponent', () => { expect(payload?.[1]?.id).toBe(1) }) + it('renders link paths when links are provided', () => { + const wrapper = mount(GanttChartComponent, { + props: { + dateRange: buildDateRange(new Date(2026, 0, 1), new Date(2026, 0, 7)), + rows: baseRows, + links: [{ fromId: 'bar-1', toId: 'bar-2' }], + }, + global: { + directives: { + tooltip: () => { + /* no-op */ + }, + }, + }, + }) + + expect(wrapper.find('[data-link-id="bar-1-bar-2"]').exists()).toBe(true) + }) + it('renders weekly headers with month spans across overlapping weeks', () => { const start = new Date(2026, 0, 25) const end = new Date(2026, 1, 10) diff --git a/vue/src/components/GanttChartComponent/GanttChartComponent.stories.ts b/vue/src/components/GanttChartComponent/GanttChartComponent.stories.ts index 3092fffc..7d303be7 100644 --- a/vue/src/components/GanttChartComponent/GanttChartComponent.stories.ts +++ b/vue/src/components/GanttChartComponent/GanttChartComponent.stories.ts @@ -21,9 +21,10 @@ const buildRows = (count: number): GanttChartRowData[] => { return Array.from({ length: count }, (_, index) => { const startDay = (index * 3) % 28 const baseDate = new Date(2026, 0, 1 + startDay) + const rowId = `row-${index}` const baseActivities: GanttChartActivityData[] = [ { - id: 'planned', + id: `planned-${index}`, label: 'Planned', startDate: new Date(baseDate), endDate: new Date(2026, 0, 6 + startDay), @@ -31,7 +32,7 @@ const buildRows = (count: number): GanttChartRowData[] => { color: 'rgba(59, 130, 246, 0.2)', }, { - id: 'optimized', + id: `optimized-${index}`, label: 'Optimized', startDate: new Date(2026, 0, 3 + startDay), endDate: new Date(2026, 0, 9 + startDay), @@ -39,7 +40,7 @@ const buildRows = (count: number): GanttChartRowData[] => { colorClass: 'bg-emerald-400/80', }, { - id: 'desired', + id: `desired-${index}`, label: 'Desired', startDate: new Date(2026, 0, 5 + startDay), endDate: new Date(2026, 0, 12 + startDay), @@ -51,7 +52,7 @@ const buildRows = (count: number): GanttChartRowData[] => { index % 3 === 0 ? [ { - id: 'desired-2', + id: `desired-2-${index}`, label: 'Alt', startDate: new Date(2026, 0, 7 + startDay), endDate: new Date(2026, 0, 10 + startDay), @@ -60,22 +61,67 @@ const buildRows = (count: number): GanttChartRowData[] => { }, ] : [] + const forwardMiniActivities: GanttChartActivityData[] = + index % 4 === 0 + ? [ + { + id: `desired-forward-${index}`, + label: 'Follow-up', + startDate: new Date(2026, 0, 14 + startDay), + endDate: new Date(2026, 0, 16 + startDay), + visualType: 'mini', + colorClass: 'bg-amber-300/80', + }, + ] + : [] return { - id: index, + id: rowId, label: `Row ${index + 1}`, header: `Line ${index + 1}`, - activities: [...baseActivities, ...extraActivities], + activities: [...baseActivities, ...extraActivities, ...forwardMiniActivities], } }) } +const buildLinks = (rows: GanttChartRowData[]) => { + const links = [] + for (let index = 0; index < rows.length - 1; index += 4) { + const from = rows[index]?.activities.find((activity) => activity.visualType === 'bar') + const to = rows[index + 1]?.activities.find((activity) => activity.visualType === 'bar') + if (from?.id && to?.id) { + links.push({ fromId: from.id, toId: to.id }) + } + } + for (let index = 0; index < rows.length - 2; index += 5) { + const from = rows[index]?.activities.find((activity) => activity.visualType === 'mini') + const to = rows[index + 2]?.activities.find((activity) => activity.visualType === 'mini') + if (from?.id && to?.id) { + links.push({ fromId: from.id, toId: to.id }) + } + } + rows.forEach((row) => { + const miniActivities = row.activities + .filter((activity) => activity.visualType === 'mini') + .sort((a, b) => a.startDate.getTime() - b.startDate.getTime()) + if (miniActivities.length < 2) { + return + } + + const [first, second] = miniActivities + if (first?.id && second?.id && first.endDate <= second.startDate) { + links.push({ fromId: first.id, toId: second.id }) + } + }) + return links +} const meta: Meta = { title: 'Components/GanttChart', component: GanttChartComponent, args: { dateRange: buildDateRange(new Date(2026, 0, 1), new Date(2026, 1, 28)), rows: buildRows(40), + links: buildLinks(buildRows(40)), headerLabel: 'gantt_chart.header', viewMode: 'day', showWeekendShading: true, @@ -111,6 +157,9 @@ The story wraps the component in a fixed-height container to demonstrate scrolli rows: { control: { type: 'object' }, }, + links: { + control: { type: 'object' }, + }, headerLabel: { control: { type: 'text' }, }, @@ -167,7 +216,11 @@ export const CustomTooltipAndClick: Story = { template: `
Last click: {{ lastClick }}
- +
`, }), diff --git a/vue/src/components/GanttChartComponent/GanttChartComponent.vue b/vue/src/components/GanttChartComponent/GanttChartComponent.vue index 8416b532..22680915 100644 --- a/vue/src/components/GanttChartComponent/GanttChartComponent.vue +++ b/vue/src/components/GanttChartComponent/GanttChartComponent.vue @@ -1,31 +1,131 @@