Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
278 changes: 278 additions & 0 deletions .gestalt/plans/advanced-data-visualization.org

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ dev-resources
.nrepl-port
.gorilla-port
.cpcache
.clj-config/
.gestalt
test/assets/Budget_uno.xlsx
test/assets/Budget_due.xlsx
test/assets/Budget_tre.xlsx
Expand Down
9 changes: 9 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,15 @@
- Project and task identifiers are normalized to uppercase in several paths. Preserve that behavior when changing import or matching logic.
- The codebase is old and not aggressively refactored. Prefer targeted fixes over stylistic rewrites.

## Visualization Ownership
- `agiladmin.tabular` remains the domain and application table format.
- `src/agiladmin/visualization.clj` is the adapter to Tablecloth and owns all production Tableplot/Plotly specifications.
- Clay is limited to local exploration and reviewer reports under the `:viz` alias; never invoke it during HTTP request handling.
- Plotly.js is a local frontend asset initialized by `resources/public/static/js/app.js` on page load and `htmx:load`.
- Chart models must follow the same role and configuration capabilities as their surrounding views. Manager payloads are hours-only.
- Keep existing detail tables authoritative beneath charts and leave the DHTMLX Gantt island unchanged.
- Available activity facts are monthly assignment totals. Do not infer daily, weekly, or within-month activity without a separate parser change.

## High-Risk Areas
- `src/agiladmin/core.clj`
- spreadsheet parsing is position-based and depends on hard-coded row/column coordinates.
Expand Down
27 changes: 27 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -306,6 +306,33 @@ Notes:
- [pb_migrations/](/home/jrml/devel/agiladmin/pb_migrations): PocketBase schema migrations kept for future schema changes
- [test/agiladmin/](/home/jrml/devel/planb-agiladmin/test/agiladmin): Midje test suite

## Data Visualizations

Project pages add hours-only monthly, annual, task-budget, and cumulative
plan-versus-actual charts where the available project data supports them.
Personnel pages add a twelve-month project mix, quarter totals, a project
heatmap when enough activity exists, and compact yearly summary facts. Existing
tables remain the detailed and authoritative view.

The production visualization boundary is `agiladmin.visualization`. It converts
the existing tabular rows to Tablecloth and builds Tableplot/Plotly
specifications. Plotly.js is served locally and synchronized with the other
frontend assets:

```sh
npm install
npm run build:frontend
```

Clay is development-only. Regenerate the fixture-based review report with:

```sh
clj -M:viz -m agiladmin.visualization-notebook
```

The report is written to `target/visualization/`. Charts use monthly assignment
totals retained by the application; they do not imply daily or weekly precision.

## Operational Notes

- Timesheet upload and commit logic writes temporary files under `/tmp/...`
Expand Down
6 changes: 5 additions & 1 deletion deps.edn
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,12 @@
clj-jgit/clj-jgit {:mvn/version "0.9.1-SNAPSHOT"}
me.raynes/fs {:mvn/version "1.4.6"}
org.clojars.dyne/clj-openssh-keygen {:mvn/version "0.1.0"}
clj-time/clj-time {:mvn/version "0.15.1"}}
clj-time/clj-time {:mvn/version "0.15.1"}
scicloj/tablecloth {:mvn/version "8.021"}
org.scicloj/tableplot {:mvn/version "1-beta17"}}
:aliases {:dev {:extra-deps {nrepl/nrepl {:mvn/version "1.3.1"}}}
:viz {:extra-paths ["dev"]
:extra-deps {org.scicloj/clay {:mvn/version "2.0.16"}}}
:build {:deps {io.github.clojure/tools.build {:mvn/version "0.10.10"}}
:ns-default build}
:run {:main-opts ["-m" "agiladmin.main"]}
Expand Down
100 changes: 100 additions & 0 deletions dev/agiladmin/visualization_notebook.clj
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
(ns agiladmin.visualization-notebook
(:require
[agiladmin.tabular :as tab]
[agiladmin.visualization :as viz]
[scicloj.clay.v2.api :as clay]
[scicloj.kindly.v4.kind :as kind]))

(kind/md
"# Agiladmin visualization review

This fixture-only report reviews the production chart specifications. Agiladmin
currently retains monthly assignment totals, so these charts must not be read as
daily or weekly activity."
)

(def sample-hours
"Representative fixture data including voluntary work and inactive months."
(tab/dataset
[:month :name :project :task :tag :hours]
[{:month "2026-1" :name "Ada" :project "CORE" :task "T1" :tag "" :hours 10}
{:month "2026-2" :name "Ada" :project "CORE" :task "T1" :tag "" :hours 5}
{:month "2026-2" :name "Ada" :project "ALPHA" :task "T2" :tag "" :hours 3}
{:month "2026-4" :name "Ada" :project "ALPHA" :task "T2" :tag "VOL" :hours 4}]))

(def sample-tasks
"Representative planned task data, including an overrun."
(tab/dataset
[:task :description :start :duration :pm :hours]
[{:task "T1" :description "Coordination" :start "01-01-2026" :duration 3 :pm 0.1 :hours 20}
{:task "T2" :description "Delivery" :start "01-02-2026" :duration 3 :pm 0.2 :hours 7}]))

(kind/md
"## Source and normalization

The legacy tabular dataset remains authoritative. The visualization boundary
converts its row maps to Tablecloth without changing spreadsheet ingestion."
)

sample-hours
(viz/to-tablecloth sample-hours)

(kind/md
"## Personnel questions

- Monthly project mix: how did activity vary through the year?
- Heatmap: which projects dominated each period?
- Quarter totals: where was work concentrated?

The heatmap keeps the eight most active projects and combines the rest as
`Other`. It is omitted unless two projects and two months are active."
)

(viz/person-monthly-project-chart-spec sample-hours 2026)
(viz/person-activity-heatmap-spec sample-hours 2026)
(viz/person-period-chart-spec sample-hours 2026)

(kind/md
"## Project manager questions

- Monthly composition: which tasks drove effort?
- Cumulative comparison: is actual effort ahead of the configured linear plan?
- Task budget: which tasks are near or beyond their PM-derived budget?

Planned curves omit tasks with incomplete start, duration, or PM data. Costs are
not part of any chart model."
)

(viz/project-monthly-task-chart-spec sample-hours 2026)
(viz/project-cumulative-chart-spec sample-hours sample-tasks)
(viz/project-task-budget-chart-spec sample-tasks)

(defn payload-measurements
"Return stable payload size observations for the review report."
[]
(let [specs [(viz/person-monthly-project-chart-spec sample-hours 2026)
(viz/person-activity-heatmap-spec sample-hours 2026)
(viz/person-period-chart-spec sample-hours 2026)]]
{:charts (count specs)
:serialized-bytes (mapv #(count (.getBytes (viz/safe-json %) "UTF-8")) specs)
:total-hours (tab/sum-col sample-hours :hours)}))

(kind/md
"## Checks and operational decision

Totals are conserved across the source and aggregations. Specifications are
created once per request and category reduction happens before JSON
serialization. These fixture payloads are small, so no chart cache is justified."
)

(payload-measurements)

(defn -main
"Generate the static Clay visualization review under target/visualization."
[& _]
(clay/make! {:source-path "dev/agiladmin/visualization_notebook.clj"
:base-target-path "target/visualization"
:format [:html]
:show false
:browse false
:live-reload false}))
8 changes: 8 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
"sync:htmx": "node ./scripts/sync-frontend-assets.mjs",
"build:frontend": "npm run sync:htmx && npm run build:css",
"test:e2e": "node ./scripts/e2e/run-playwright.mjs",
"test:e2e:base-path": "E2E_BASE_PATH=/agiladmin node ./scripts/e2e/run-playwright.mjs test/e2e/base-path.spec.js",
"test:e2e:base-path": "node ./scripts/e2e/run-base-path.mjs",
"test:e2e:caddy": "E2E_PROXY=caddy node ./scripts/e2e/run-playwright.mjs test/e2e/personnel-visibility.spec.js test/e2e/timesheet-upload.spec.js",
"test:e2e:caddy:base-path": "E2E_PROXY=caddy E2E_BASE_PATH=/agiladmin node ./scripts/e2e/run-playwright.mjs test/e2e/base-path.spec.js test/e2e/timesheet-upload.spec.js",
"test:e2e:headed": "node ./scripts/e2e/run-playwright.mjs --headed",
Expand All @@ -21,6 +21,7 @@
"conventional-changelog-conventionalcommits": "^8.0.0",
"daisyui": "^4.12.24",
"htmx.org": "^2.0.4",
"plotly.js-dist-min": "^3.1.0",
"semantic-release": "^24.2.9",
"tailwindcss": "^3.4.17"
}
Expand Down
1 change: 1 addition & 0 deletions playwright.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { defineConfig } from "@playwright/test";

export default defineConfig({
testDir: "./test/e2e",
workers: 1,
retries: process.env.CI ? 1 : 0,
use: {
baseURL: process.env.PLAYWRIGHT_BASE_URL || "http://127.0.0.1:18080",
Expand Down
4 changes: 2 additions & 2 deletions resources/public/static/css/app.css

Large diffs are not rendered by default.

87 changes: 86 additions & 1 deletion resources/public/static/js/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,11 @@
panels.forEach(function (panel) {
panel.hidden = panel.getAttribute("data-tab-panel") !== id;
});

window.requestAnimationFrame(function () {
initPlotlyCharts(group);
resizePlotlyCharts(group);
});
}

triggers.forEach(function (trigger) {
Expand Down Expand Up @@ -101,6 +106,83 @@
});
}

function plotlyConfig() {
return {
responsive: true,
displaylogo: false,
modeBarButtonsToRemove: ["select2d", "lasso2d", "autoScale2d"],
};
}

function canRenderChart(chart) {
return Boolean(chart && chart.getClientRects && chart.getClientRects().length);
}

function plotlyFallback(chart, message) {
if (!chart) {
return;
}

chart.innerHTML =
'<p class="rounded-box border border-dashed border-base-300 px-4 py-6 text-sm text-base-content/70">' +
message +
"</p>";
}

function renderPlotlyChart(chart) {
if (!chart || chart.dataset.plotlyInitialized === "true") {
return;
}

if (!window.Plotly) {
plotlyFallback(chart, "Plotly is not available in this browser session.");
return;
}

if (!canRenderChart(chart)) {
return;
}

var rawSpec = chart.getAttribute("data-plotly-spec") || "";
if (rawSpec === "") {
plotlyFallback(chart, "No chart data was provided.");
return;
}

var spec;
try {
spec = JSON.parse(rawSpec);
} catch (error) {
plotlyFallback(chart, "This chart could not be parsed.");
return;
}

chart.dataset.plotlyInitialized = "true";
window.Plotly.newPlot(chart, spec.data || [], spec.layout || {}, plotlyConfig());
}

function resizePlotlyCharts(root) {
if (!window.Plotly || !window.Plotly.Plots) {
return;
}

Array.prototype.slice
.call(root.querySelectorAll("[data-plotly-chart][data-plotly-initialized='true']"))
.forEach(function (chart) {
if (canRenderChart(chart)) {
window.Plotly.Plots.resize(chart);
}
});
}

function initPlotlyCharts(root) {
Array.prototype.slice
.call(root.querySelectorAll("[data-plotly-chart]"))
.forEach(function (chart) {
renderPlotlyChart(chart);
});
}

function applyTheme(theme) {
var body = document.body;
if (!body) {
Expand Down Expand Up @@ -344,6 +426,7 @@
initTabGroups(root);
initNavToggles(root);
initTextFilters(root);
initPlotlyCharts(root);
initThemeToggle(root);
initPageLoading(root);
initUploadProgress(root);
Expand All @@ -353,7 +436,7 @@
boot(document);
});

document.addEventListener("htmx:afterSwap", function (event) {
document.addEventListener("htmx:load", function (event) {
boot(event.target);
});

Expand All @@ -370,5 +453,7 @@
.forEach(function (indicator) {
indicator.style.display = "none";
});

resizePlotlyCharts(document);
});
})();
3,882 changes: 3,882 additions & 0 deletions resources/public/static/js/plotly.min.js

Large diffs are not rendered by default.

16 changes: 16 additions & 0 deletions scripts/e2e/run-base-path.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { spawn } from "node:child_process";

const child = spawn(
process.execPath,
["./scripts/e2e/run-playwright.mjs", "test/e2e/base-path.spec.js"],
{
stdio: "inherit",
env: {...process.env, E2E_BASE_PATH: "/agiladmin"},
},
);

child.on("exit", (code) => process.exit(code ?? 1));
child.on("error", (error) => {
console.error(error);
process.exit(1);
});
Loading