From ecd33e0d1a0ff549a3005753921ddb8b7e6c034c Mon Sep 17 00:00:00 2001 From: Matteo Mattiuzzi Date: Mon, 3 Aug 2026 13:06:05 +0000 Subject: [PATCH 1/3] corrected type SFW --- DOCS/filenaming/clms-filenaming_filename-tree_v1.qmd | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/DOCS/filenaming/clms-filenaming_filename-tree_v1.qmd b/DOCS/filenaming/clms-filenaming_filename-tree_v1.qmd index 98789f29..3e031fb2 100644 --- a/DOCS/filenaming/clms-filenaming_filename-tree_v1.qmd +++ b/DOCS/filenaming/clms-filenaming_filename-tree_v1.qmd @@ -39,7 +39,7 @@ CLMS_ │ └── IMCCS_S{YYYY}_R10m_E{XXX}N{YYY}_3035_V01-R00.tif │ ├── SLF- Small Landscape Features -│ └── SFW_S{YYYY}_R5m_E{XXX}N{YYY}_3035_V01-R01.tif +│ └── SWF_S{YYYY}_R5m_E{XXX}N{YYY}_3035_V01-R01.tif │ ├── WSI- Water & Snow/Ice │ ├── SP-SCD_A{YYYYMMDD}P1Y_R20m_T{ZZ}{GZD}{AA}_V01-R00.tif @@ -156,7 +156,7 @@ CLMS_ {CODE}-{VARIABLE}_ ├── {PARAM} Single token (VLCC, NVLCC, SLF, HRL) │ ├── GRA, DLT, CTY (VLCC) -│ ├── IMCCS, SBCC, SFW, CM (NVLCC, SLF) +│ ├── IMCCS, SBCC, SWF, CM (NVLCC, SLF) │ ├── TCD, FTY, IMD, IBU, WAW (HRL - 10m) │ └── SWF (HRL - 5m) │ From 3375c8df8de73d04be90d92dba14a45fd93f0dfc Mon Sep 17 00:00:00 2001 From: Matteo Mattiuzzi Date: Wed, 5 Aug 2026 14:50:38 +0000 Subject: [PATCH 2/3] fix: remove bar labels and default to weekly view in CDSE migration dashboard Remove the +N number labels from the onboarding history bar chart (values are still available via tooltip on hover). Switch the default aggregation mode from daily to weekly for a cleaner overview. --- .../CLMS_CDSE_Migration_Dashboard.qmd | 145 +++++++++++++++--- 1 file changed, 120 insertions(+), 25 deletions(-) diff --git a/DOCS/CDSE_Migration/CLMS_CDSE_Migration_Dashboard.qmd b/DOCS/CDSE_Migration/CLMS_CDSE_Migration_Dashboard.qmd index b38b687e..8afc393a 100644 --- a/DOCS/CDSE_Migration/CLMS_CDSE_Migration_Dashboard.qmd +++ b/DOCS/CDSE_Migration/CLMS_CDSE_Migration_Dashboard.qmd @@ -173,8 +173,8 @@ toggleScript = { // ── Onboarding history plot ── onboardingPlot = { - const barHeight = 28 - let mode = "daily" + const plotHeight = 170 + let mode = "weekly" function getWeekNumber(dateStr) { const d = new Date(dateStr) @@ -186,38 +186,133 @@ onboardingPlot = { function buildChart(container) { container.innerHTML = "" + // Weekly mode sums the daily deltas per ISO week so each week gets its own + // bar; reading odataWeeklyDelta directly yielded one bar for the whole + // trailing period. const data = mode === "daily" ? odataHist.map((h, i) => { const d = i > 0 ? h.total - odataHist[i - 1].total : 0 return {label: h.date, value: d, color: d >= 0 ? '#e67e22' : '#e74c3c'} }).slice(1).reverse() - : [{label: "WoY " + getWeekNumber(odataDaily.scraped_at || ""), value: odataWeeklyDelta, color: '#e67e22'}] + : (() => { + const deltas = odataHist.map((h, i) => { + const d = i > 0 ? h.total - odataHist[i - 1].total : 0 + return {date: h.date, value: d} + }).slice(1) + const weekly = {} + for (const d of deltas) { + // Year-prefixed so the sort stays correct across a year boundary + const key = d.date.slice(0, 4) + "-W" + String(getWeekNumber(d.date)).padStart(2, "0") + if (!weekly[key]) weekly[key] = {label: key, value: 0} + weekly[key].value += d.value + } + return Object.values(weekly) + .sort((a, b) => a.label.localeCompare(b.label)) + .reverse() + .map(d => ({...d, color: d.value >= 0 ? '#e67e22' : '#e74c3c'})) + })() const maxVal = Math.max(...data.map(d => Math.abs(d.value)), 1) - data.forEach(d => { - const pct = Math.abs(d.value) / maxVal - const row = document.createElement("div") - row.style.cssText = "display:flex;align-items:center;margin:2px 0;font-size:0.85em" - - const label = document.createElement("span") - label.textContent = d.label - label.style.cssText = "width:100px;text-align:right;padding-right:8px;color:#555;flex-shrink:0" - row.appendChild(label) - - const track = document.createElement("div") - track.style.cssText = `height:${barHeight}px;flex:1;display:flex;align-items:center` - - const fill = document.createElement("div") - fill.style.cssText = `height:${barHeight - 4}px;width:${Math.max(pct * 100, 2)}%;background:${d.color};border-radius:3px;display:flex;align-items:center;padding-left:6px;min-width:fit-content` - fill.textContent = (d.value >= 0 ? "+" : "") + Math.abs(d.value).toLocaleString() - fill.style.color = "#fff" - fill.style.fontSize = "0.85em" - fill.style.fontWeight = "bold" - track.appendChild(fill) - row.appendChild(track) - container.appendChild(row) + // `data` is newest-first; vertical bars read left-to-right in time, so plot + // a chronological copy rather than reordering the builders above. + const chrono = data.slice().reverse() + + // Tooltip carries the exact value — no need for labels on the bars themselves. + const yAxisW = 46 + // Daily labels are day-first: "2026-07-21" -> "21-07". Weekly keys already + // carry their own form: "2026-W31" -> "W31". + const shortLabel = (l) => l.includes("-W") + ? l.slice(5) + : l.slice(8, 10) + "-" + l.slice(5, 7) + const signed = (v) => (v >= 0 ? "+" : "\u2212") + Math.abs(v).toLocaleString() + + // Round the scale up to a whole number of readable steps, so ticks land on + // values worth reading. Bars are scaled to this same top, which is what keeps + // their heights consistent with the gridlines. + const rawStep = maxVal / 4 + const mag = Math.pow(10, Math.floor(Math.log10(rawStep))) + const norm = rawStep / mag + const step = Math.max(1, Math.round((norm <= 1 ? 1 : norm <= 2 ? 2 : norm <= 5 ? 5 : 10) * mag)) + const top = Math.max(Math.ceil(maxVal / step) * step, step) + const ticks = [] + for (let v = 0; v <= top; v += step) ticks.push(v) + + // Height of a value above the baseline. plotHeight is the bar area; the value + // band sits above it, so bars keep the same height whichever way the numbers + // are drawn. + const yOf = (v) => v / top * plotHeight + + const chartRow = document.createElement("div") + chartRow.style.cssText = "display:flex" + + // ── y axis ── + const yAxis = document.createElement("div") + yAxis.style.cssText = `width:${yAxisW}px;flex-shrink:0;position:relative;height:${plotHeight}px` + ticks.forEach(v => { + const t = document.createElement("div") + t.textContent = v.toLocaleString() + // translateY(50%) centres the text on its gridline + t.style.cssText = `position:absolute;right:6px;bottom:${yOf(v)}px;transform:translateY(50%);font-size:0.62em;color:#555;white-space:nowrap` + yAxis.appendChild(t) }) + chartRow.appendChild(yAxis) + + // ── plot area: gridlines behind, bars in front ── + const plotWrap = document.createElement("div") + plotWrap.style.cssText = `flex:1;position:relative;height:${plotHeight}px;border-bottom:1px solid #ccc` + + const grid = document.createElement("div") + grid.style.cssText = `position:absolute;left:0;right:0;bottom:0;height:${plotHeight}px;pointer-events:none` + ticks.forEach(v => { + // Zero needs no line; the plot's own bottom border is the baseline. + if (v === 0) return + const line = document.createElement("div") + line.style.cssText = `position:absolute;left:0;right:0;bottom:${yOf(v)}px;border-top:1px solid #eee` + grid.appendChild(line) + }) + plotWrap.appendChild(grid) + + // position:relative lifts the bars above the absolutely-placed gridlines + const plot = document.createElement("div") + plot.style.cssText = "position:relative;display:flex;align-items:flex-end;gap:3px;height:100%" + + chrono.forEach(d => { + const col = document.createElement("div") + col.style.cssText = "flex:1 1 0;min-width:8px;height:100%;display:flex;flex-direction:column" + col.title = d.label + ": " + signed(d.value) + + const barArea = document.createElement("div") + barArea.style.cssText = "flex:1;display:flex;align-items:flex-end" + const bar = document.createElement("div") + // Small floor keeps a zero-delta day visible as a baseline tick + bar.style.cssText = `width:100%;height:${Math.max(Math.abs(d.value) / top * 100, 0.5)}%;background:${d.color};border-radius:2px 2px 0 0` + barArea.appendChild(bar) + col.appendChild(barArea) + + plot.appendChild(col) + }) + plotWrap.appendChild(plot) + chartRow.appendChild(plotWrap) + container.appendChild(chartRow) + + // Tick labels are anchored at each cell's centre with the transform origin at + // the text's top-right corner, so rotating -60deg sweeps the text down and to + // the left, keeping it below the axis. Rotating about `top center` (as before) + // swept it upwards into the bars. The left margin matches the y axis so the + // labels stay aligned with their columns. + const axis = document.createElement("div") + axis.style.cssText = `display:flex;gap:3px;height:44px;margin-left:${yAxisW}px` + chrono.forEach(d => { + const cell = document.createElement("div") + cell.style.cssText = "flex:1 1 0;min-width:8px;position:relative" + const t = document.createElement("span") + t.textContent = shortLabel(d.label) + t.style.cssText = "position:absolute;top:3px;right:50%;text-align:right;font-size:0.62em;color:#555;white-space:nowrap;transform:rotate(-60deg);transform-origin:top right" + cell.appendChild(t) + axis.appendChild(cell) + }) + container.appendChild(axis) // Radio buttons below the chart const controls = document.createElement("div") From 5898ddc1a31852254e8cfb6bce041d4529890891 Mon Sep 17 00:00:00 2001 From: mckeea Date: Thu, 6 Aug 2026 09:37:48 +0000 Subject: [PATCH 3/3] fix: plot negative onboarding deltas below zero and key weeks by ISO week-year --- .../CLMS_CDSE_Migration_Dashboard.qmd | 71 +++++++++++-------- .../clms-filenaming_filename-tree_v1.qmd | 4 +- 2 files changed, 44 insertions(+), 31 deletions(-) diff --git a/DOCS/CDSE_Migration/CLMS_CDSE_Migration_Dashboard.qmd b/DOCS/CDSE_Migration/CLMS_CDSE_Migration_Dashboard.qmd index 8afc393a..0601f33f 100644 --- a/DOCS/CDSE_Migration/CLMS_CDSE_Migration_Dashboard.qmd +++ b/DOCS/CDSE_Migration/CLMS_CDSE_Migration_Dashboard.qmd @@ -176,11 +176,17 @@ onboardingPlot = { const plotHeight = 170 let mode = "weekly" - function getWeekNumber(dateStr) { - const d = new Date(dateStr) - const start = new Date(d.getFullYear(), 0, 1) - const days = Math.floor((d - start) / 86400000) - return Math.ceil((days + start.getDay() + 1) / 7) + // ISO-8601 week key, e.g. "2026-W31". The Thursday of a week decides its + // week-year, which is why the year comes from the shifted date and not from + // the input: a week straddling New Year then keys to one year, not two, and + // plain string sort stays chronological across the boundary. + function isoWeekKey(dateStr) { + const d = new Date(dateStr + "T00:00:00Z") + d.setUTCDate(d.getUTCDate() + 4 - (d.getUTCDay() || 7)) // Mon=1..Sun=7 + const year = d.getUTCFullYear() + const jan1 = Date.UTC(year, 0, 1) + const week = Math.ceil(((d - jan1) / 86400000 + 1) / 7) + return year + "-W" + String(week).padStart(2, "0") } function buildChart(container) { @@ -201,8 +207,7 @@ onboardingPlot = { }).slice(1) const weekly = {} for (const d of deltas) { - // Year-prefixed so the sort stays correct across a year boundary - const key = d.date.slice(0, 4) + "-W" + String(getWeekNumber(d.date)).padStart(2, "0") + const key = isoWeekKey(d.date) if (!weekly[key]) weekly[key] = {label: key, value: 0} weekly[key].value += d.value } @@ -212,7 +217,10 @@ onboardingPlot = { .map(d => ({...d, color: d.value >= 0 ? '#e67e22' : '#e74c3c'})) })() - const maxVal = Math.max(...data.map(d => Math.abs(d.value)), 1) + // Signed domain: the scale has to cover the losses as well as the gains. + const hiVal = Math.max(...data.map(d => d.value), 0) + const loVal = Math.min(...data.map(d => d.value), 0) + const range = Math.max(hiVal - loVal, 1) // `data` is newest-first; vertical bars read left-to-right in time, so plot // a chronological copy rather than reordering the builders above. @@ -227,21 +235,24 @@ onboardingPlot = { : l.slice(8, 10) + "-" + l.slice(5, 7) const signed = (v) => (v >= 0 ? "+" : "\u2212") + Math.abs(v).toLocaleString() - // Round the scale up to a whole number of readable steps, so ticks land on - // values worth reading. Bars are scaled to this same top, which is what keeps - // their heights consistent with the gridlines. - const rawStep = maxVal / 4 + // Round the scale out to whole steps, so ticks land on values worth reading. + // Bars are scaled to this same domain, which is what keeps their heights + // consistent with the gridlines. + const rawStep = range / 4 const mag = Math.pow(10, Math.floor(Math.log10(rawStep))) const norm = rawStep / mag const step = Math.max(1, Math.round((norm <= 1 ? 1 : norm <= 2 ? 2 : norm <= 5 ? 5 : 10) * mag)) - const top = Math.max(Math.ceil(maxVal / step) * step, step) + // With no losses in the window `bot` is 0 and this is an ordinary 0..top + // scale; a loss pushes the floor down instead of being folded into the top. + const bot = Math.floor(loVal / step) * step + const top = Math.max(Math.ceil(hiVal / step) * step, bot + step) + const span = top - bot const ticks = [] - for (let v = 0; v <= top; v += step) ticks.push(v) + for (let v = bot; v <= top; v += step) ticks.push(v) - // Height of a value above the baseline. plotHeight is the bar area; the value - // band sits above it, so bars keep the same height whichever way the numbers - // are drawn. - const yOf = (v) => v / top * plotHeight + // Pixels above the bottom of the plot band for a given value. + const yOf = (v) => (v - bot) / span * plotHeight + const zeroY = yOf(0) const chartRow = document.createElement("div") chartRow.style.cssText = "display:flex" @@ -265,30 +276,32 @@ onboardingPlot = { const grid = document.createElement("div") grid.style.cssText = `position:absolute;left:0;right:0;bottom:0;height:${plotHeight}px;pointer-events:none` ticks.forEach(v => { - // Zero needs no line; the plot's own bottom border is the baseline. - if (v === 0) return + // The lowest tick needs no line; the plot's own bottom border draws it. + if (v === bot) return const line = document.createElement("div") - line.style.cssText = `position:absolute;left:0;right:0;bottom:${yOf(v)}px;border-top:1px solid #eee` + // Zero is what the bars grow from, so it reads darker than the rest. + line.style.cssText = `position:absolute;left:0;right:0;bottom:${yOf(v)}px;border-top:1px solid ${v === 0 ? "#ccc" : "#eee"}` grid.appendChild(line) }) plotWrap.appendChild(grid) // position:relative lifts the bars above the absolutely-placed gridlines const plot = document.createElement("div") - plot.style.cssText = "position:relative;display:flex;align-items:flex-end;gap:3px;height:100%" + plot.style.cssText = "position:relative;display:flex;gap:3px;height:100%" chrono.forEach(d => { const col = document.createElement("div") - col.style.cssText = "flex:1 1 0;min-width:8px;height:100%;display:flex;flex-direction:column" + col.style.cssText = "flex:1 1 0;min-width:8px;height:100%;position:relative" col.title = d.label + ": " + signed(d.value) - const barArea = document.createElement("div") - barArea.style.cssText = "flex:1;display:flex;align-items:flex-end" + // Gains grow up from the zero line, losses hang below it, so a drop can + // never be read as a rise. Small floor keeps a zero delta visible as a tick. + const barH = Math.max(Math.abs(d.value) / span * plotHeight, 1) const bar = document.createElement("div") - // Small floor keeps a zero-delta day visible as a baseline tick - bar.style.cssText = `width:100%;height:${Math.max(Math.abs(d.value) / top * 100, 0.5)}%;background:${d.color};border-radius:2px 2px 0 0` - barArea.appendChild(bar) - col.appendChild(barArea) + bar.style.cssText = `position:absolute;left:0;right:0;height:${barH}px;background:${d.color};` + (d.value >= 0 + ? `bottom:${zeroY}px;border-radius:2px 2px 0 0` + : `top:${plotHeight - zeroY}px;border-radius:0 0 2px 2px`) + col.appendChild(bar) plot.appendChild(col) }) diff --git a/DOCS/filenaming/clms-filenaming_filename-tree_v1.qmd b/DOCS/filenaming/clms-filenaming_filename-tree_v1.qmd index 3e031fb2..98789f29 100644 --- a/DOCS/filenaming/clms-filenaming_filename-tree_v1.qmd +++ b/DOCS/filenaming/clms-filenaming_filename-tree_v1.qmd @@ -39,7 +39,7 @@ CLMS_ │ └── IMCCS_S{YYYY}_R10m_E{XXX}N{YYY}_3035_V01-R00.tif │ ├── SLF- Small Landscape Features -│ └── SWF_S{YYYY}_R5m_E{XXX}N{YYY}_3035_V01-R01.tif +│ └── SFW_S{YYYY}_R5m_E{XXX}N{YYY}_3035_V01-R01.tif │ ├── WSI- Water & Snow/Ice │ ├── SP-SCD_A{YYYYMMDD}P1Y_R20m_T{ZZ}{GZD}{AA}_V01-R00.tif @@ -156,7 +156,7 @@ CLMS_ {CODE}-{VARIABLE}_ ├── {PARAM} Single token (VLCC, NVLCC, SLF, HRL) │ ├── GRA, DLT, CTY (VLCC) -│ ├── IMCCS, SBCC, SWF, CM (NVLCC, SLF) +│ ├── IMCCS, SBCC, SFW, CM (NVLCC, SLF) │ ├── TCD, FTY, IMD, IBU, WAW (HRL - 10m) │ └── SWF (HRL - 5m) │