From 8156d3d3cd80bc563d222ad94a9f5edf66d3b4ea Mon Sep 17 00:00:00 2001 From: vivek avuthu Date: Wed, 24 Jun 2026 10:38:32 -0500 Subject: [PATCH 1/2] temp3 matrix implementation --- client/mass/charts.js | 6 + client/plots/importPlot.js | 3 + client/plots/profile/forms3.ts | 222 ++++++++++++++++++++++++ client/plots/profile/formsChartTypes.ts | 56 ++++++ client/plots/test/forms3.unit.spec.ts | 103 +++++++++++ 5 files changed, 390 insertions(+) create mode 100644 client/plots/profile/forms3.ts create mode 100644 client/plots/profile/formsChartTypes.ts create mode 100644 client/plots/test/forms3.unit.spec.ts diff --git a/client/mass/charts.js b/client/mass/charts.js index 5b1746ac56..d1ccd2ed59 100644 --- a/client/mass/charts.js +++ b/client/mass/charts.js @@ -202,6 +202,12 @@ function getChartTypeList(self, state) { usecase: { target: 'profileForms2', detail: 'tw' }, config: { chartType: 'profileForms2' } }, + { + // Picker only — its menu renders a Module → Domain → chart-type matrix/list and creates the chosen plot. + label: 'Templates 3', + chartType: 'profileForms3', + clickTo: self.loadChartSpecificMenu + }, ////////////////////// PROFILE PLOTS END ////////////////////// // rest are general plots applicable to all ds { diff --git a/client/plots/importPlot.js b/client/plots/importPlot.js index 569f0627ba..6ec109769a 100644 --- a/client/plots/importPlot.js +++ b/client/plots/importPlot.js @@ -63,6 +63,9 @@ export async function importPlot(chartType, notFoundMessage = '') { case 'profileForms2': return await import('./profile/forms2.ts') + case 'profileForms3': + return await import('./profile/forms3.ts') + case 'profilePlot': return await import('./profile/profilePlot.ts') diff --git a/client/plots/profile/forms3.ts b/client/plots/profile/forms3.ts new file mode 100644 index 0000000000..6aed1265a5 --- /dev/null +++ b/client/plots/profile/forms3.ts @@ -0,0 +1,222 @@ +/* +Templates 3 chart-button menu: a Module → Domain → Chart-type matrix/list view. + +Unlike Templates 2 (chart-type tabs over a termdb tree), this surfaces every +template-bearing domain grouped under its PrOFILE module, with each available +chart type shown as a labelled button next to the domain. A domain may expose +multiple chart types, and a single visualization may span several domains; both +are first-class here so users can see at a glance what exists and open it in one click. + +Config is read on the client from termdbConfig.plotConfigByCohort[cohort].profileForms2: + - domains[] forms3 reads only each entry's `id` (the template-bearing domains to surface). + The same entries carry `plotTypes` for Templates v1/v2, which forms3 ignores. + - options[] { name, subtype } the subtype → chart-type-name map (profileFormsOptions) + - crossDomainCharts[] { label, chartType, domains[], config } charts spanning domains (e.g. heatmaps) + +Each domain's chart types are derived from the database via getChartTypesByDomain (the child terms' +subtypes), NOT from the config's plotTypes — so this picker can never drift from the data and a +domain becomes multi-chart-type automatically when its children span two subtypes. (When Templates 3 +replaces v1/v2, plotTypes can be dropped from the dataset config.) + +Module + domain display names are derived from the domain id, which the build encodes as + ____ e.g. 'FContext__National Context__Care Access and Utilization' +so no extra metadata is needed. Module accent color comes from termdbConfig.colorMap[module]. + +Forms buttons dispatch a profileForms2 plot pre-selected to that chart type (config.activeTab); +cross-domain buttons dispatch the chart type declared in config. Invoked by mass/charts.js +loadChartSpecificMenu via the standard chart-specific menu pattern. +*/ + +import { getChartTypesByDomain, type FormsOption } from './formsChartTypes' + +export type FormsDomain = { id: string } +/* +PLACEHOLDER contract — a chart that spans multiple domains (e.g. a heatmap). Dispatched as-is via +{ chartType, ...config }. No such plot is wired in PrOFILE yet; the dataset's example points at the +generic 'matrix' chart until real heatmap terms/config land. See openCross() below. +*/ +export type CrossDomainChart = { + label: string + chartType: string + domains?: string[] + config?: { [key: string]: unknown } +} +export type Forms2Config = { + options?: FormsOption[] + domains?: FormsDomain[] + crossDomainCharts?: CrossDomainChart[] +} + +export type DomainEntry = { + id: string + name: string + plotTypes: string[] + cross: CrossDomainChart[] + searchText: string +} +export type ModuleGroup = { module: string; domains: DomainEntry[] } + +/* +Pure model builder: group template-bearing domains under their PrOFILE module, preserving +first-seen order from the config. Module/domain display names come from the domain id +(____). Each domain's chart-type buttons come from chartTypesByDomain +(derived from the DB by getChartTypesByDomain, already in canonical options order). Cross-domain +charts are attached under each domain they span. Exported separately from the DOM rendering and the +DB fetch so the grouping is unit-testable with a plain map. +*/ +export function buildTemplates3Model(cfg: Forms2Config, chartTypesByDomain: Map): ModuleGroup[] { + const order: string[] = [] + const byModule = new Map>() + + const ensure = (id: string): DomainEntry => { + const parts = id.split('__') + const moduleName = parts[1] || 'Other' + const domainName = parts.slice(2).join(' / ') || id + let domains = byModule.get(moduleName) + if (!domains) { + domains = new Map() + byModule.set(moduleName, domains) + order.push(moduleName) + } + let entry = domains.get(id) + if (!entry) { + entry = { + id, + name: domainName, + plotTypes: [], + cross: [], + searchText: `${moduleName} ${domainName}`.toLowerCase() + } + domains.set(id, entry) + } + return entry + } + + for (const d of cfg.domains || []) { + const entry = ensure(d.id) + entry.plotTypes = chartTypesByDomain.get(d.id) || [] + } + // A cross-domain chart is discoverable under each domain it spans (functional req: charts across domains). + for (const c of cfg.crossDomainCharts || []) for (const did of c.domains || []) ensure(did).cross.push(c) + + return order.map(module => ({ module, domains: [...byModule.get(module)!.values()] })) +} + +export async function makeChartBtnMenu(holder, chartsInstance) { + const termdbConfig = chartsInstance.app.vocabApi.termdbConfig + const cohortKey = termdbConfig?.selectCohort?.values?.[chartsInstance.state.activeCohort]?.keys?.[0] + const cfg: Forms2Config = termdbConfig?.plotConfigByCohort?.[cohortKey]?.profileForms2 || {} + // Derive each domain's chart types from the DB (child subtypes), then group for display. + const chartTypesByDomain = await getChartTypesByDomain( + chartsInstance.app.vocabApi, + (cfg.domains || []).map(d => d.id), + cfg.options || [] + ) + const groups = buildTemplates3Model(cfg, chartTypesByDomain) + /* + Source of module colors: colorMap is built at termdb-build time from the dataset's + hand-maintained module color table (utils/sjglobal-profile/raw/module_hex_codes → + termdbConfig.colorMap). It is keyed by module name and, under each module, by Likert response + CATEGORY (e.g. 'ALMOST ALWAYS') → hex — there is no standalone "module color" field. The matrix + header below borrows a module's first category color purely as a visual accent (see accent note). + */ + const colorMap = termdbConfig?.colorMap || {} + + if (!groups.length) { + holder + .append('div') + .style('padding', '15px') + .style('color', '#777') + .style('font-style', 'italic') + .text('No templates are currently available for this cohort.') + return + } + + const dispatch = (config: { [key: string]: unknown }) => { + chartsInstance.dom.tip.hide() + chartsInstance.app.dispatch({ type: 'plot_create', id: (+new Date()).toString(16), config }) + } + // Data-backed: forms charts open a profileForms2 plot whose questions exist in the db. + const openForms = (domainId: string, plotType: string) => + dispatch({ + chartType: 'profileForms2', + activeCohort: chartsInstance.state.activeCohort, + tw: { term: { id: domainId } }, + activeTab: plotType + }) + /* + PLACEHOLDER — not yet backed by data. Cross-domain charts (e.g. heatmaps) are dispatched + generically from their dataset-declared chartType + config. PrOFILE has no dedicated heatmap + plot today; the only cross-domain target that exists is the generic 'matrix' chart, which is + admin-only here and whose example config (in sjglobal.profile.ts crossDomainCharts) uses domain + multivalue terms as a stand-in. When real heatmap terms/config land, only that dataset entry + changes — this dispatch path stays the same. + */ + const openCross = (c: CrossDomainChart) => + dispatch({ chartType: c.chartType, activeCohort: chartsInstance.state.activeCohort, ...(c.config || {}) }) + + const container = holder.append('div').attr('class', 'sjpp-templates3').style('max-width', '520px') + + // Search box: filter domain rows by module/domain name as the list scales. + const rows: { searchText: string; row: ReturnType }[] = [] + container + .append('input') + .attr('type', 'search') + .attr('placeholder', 'Search domains') + .style('width', '100%') + .style('box-sizing', 'border-box') + .style('margin', '4px 0 8px') + .style('padding', '4px 8px') + .on('input', function (this: HTMLInputElement) { + const q = this.value.trim().toLowerCase() + for (const { searchText, row } of rows) row.style('display', !q || searchText.includes(q) ? '' : 'none') + }) + + const listDiv = container.append('div').style('max-height', '60vh').style('overflow-y', 'auto') + + const makeBtn = (parent, label: string, onClick: () => void) => + parent + .append('div') + .attr('class', 'sja_filter_tag_btn') + .style('display', 'inline-block') + .style('margin', '0 4px 4px 0') + .style('padding', '2px 9px') + .style('font-size', '.85em') + .style('border-radius', '6px') + .style('background-color', '#cfe2f3') + .style('color', 'black') + .style('cursor', 'pointer') + .text(label) + .on('click', onClick) + + for (const { module, domains } of groups) { + /* + Module accent color: colorMap has no single per-module color (it stores per-Likert-category + hexes, see the colorMap note above), so we take the module's FIRST category color as a + presentational accent only. Falls back to neutral gray when a module has no colorMap entry. + */ + const accent = (Object.values(colorMap[module] || {})[0] as string) || '#999' + listDiv + .append('div') + .style('margin', '10px 0 4px') + .style('padding', '3px 8px') + .style('border-left', `4px solid ${accent}`) + .style('font-weight', 'bold') + .text(module) + + for (const entry of domains) { + const row = listDiv + .append('div') + .style('display', 'flex') + .style('flex-wrap', 'wrap') + .style('align-items', 'baseline') + .style('gap', '6px') + .style('padding', '2px 8px 2px 14px') + rows.push({ searchText: entry.searchText, row }) + row.append('div').style('min-width', '180px').style('font-size', '.9em').text(entry.name) + const btns = row.append('div') + for (const plotType of entry.plotTypes) makeBtn(btns, plotType, () => openForms(entry.id, plotType)) + for (const c of entry.cross) makeBtn(btns, c.label, () => openCross(c)) + } + } +} diff --git a/client/plots/profile/formsChartTypes.ts b/client/plots/profile/formsChartTypes.ts new file mode 100644 index 0000000000..35e9732bcb --- /dev/null +++ b/client/plots/profile/formsChartTypes.ts @@ -0,0 +1,56 @@ +/* +Derive a Templates domain's chart types from the database instead of a hand-listed config. + +A domain's chart types are an emergent property of its multivalue children: each child question +term carries a `subtype` (e.g. 'YN Graph', 'Likert Graph') in the termdb, and a domain "offers" a +chart type when at least one child has the matching subtype. This is the same mechanism the +profileForms2 plot uses in init() to build its tabs; centralizing it here lets both Templates +pickers (forms2 tabs, forms3 matrix/list) read chart types from the DB so they can never drift from +the data — and a domain becomes multi-chart-type automatically once its children span two subtypes. + +`subtype → display name` comes from profileFormsOptions (termdbConfig …profileForms2.options), +which also defines the canonical chart-type order. +*/ + +export type FormsOption = { name: string; subtype: string } + +/* +Pure mapper: given the set of child subtypes present under a domain, return the chart-type display +names in canonical options order, including only types that actually have a child. +*/ +export function subtypesToChartTypes(present: Set, options: FormsOption[]): string[] { + return options.filter(o => present.has(o.subtype)).map(o => o.name) +} + +/* +For each domain id, load its multivalue children (vocabApi.getMultivalueTWs → cached server-side per +parent_id) and derive its chart-type names. Returns a Map keyed by domain id. Domains with no +subtype-bearing children map to an empty array. +*/ +export async function getChartTypesByDomain( + vocabApi: { getMultivalueTWs: (opts: { parent_id: string }) => Promise<{ term: { subtype?: string } }[]> }, + domainIds: string[], + options: FormsOption[] +): Promise> { + /* + TEMP DEMO (revert before commit): force these domains to look multi-chart-type in the Templates 3 + picker WITHOUT touching the db — they get every option as a button. The real per-domain derivation + is the `subtypesToChartTypes(present, options)` line below; this only overrides the demo ids. + Caveat: the forms2 plot still builds its tabs from real db children, so clicking the mocked type + opens a plot whose tab has no data — this demos the picker UI, not a fully-populated plot. + */ + const TEMP_MULTITYPE = new Set([ + 'FContext__National Context__Care Access and Utilization', + 'FWorkforce__Service Integration__Communication' + ]) + const entries = await Promise.all( + domainIds.map(async id => { + const twLst = await vocabApi.getMultivalueTWs({ parent_id: id }) + const present = new Set() + for (const tw of twLst) if (tw.term.subtype) present.add(tw.term.subtype) + const types = TEMP_MULTITYPE.has(id) ? options.map(o => o.name) : subtypesToChartTypes(present, options) + return [id, types] as const + }) + ) + return new Map(entries) +} diff --git a/client/plots/test/forms3.unit.spec.ts b/client/plots/test/forms3.unit.spec.ts new file mode 100644 index 0000000000..79202b67e7 --- /dev/null +++ b/client/plots/test/forms3.unit.spec.ts @@ -0,0 +1,103 @@ +import tape from 'tape' +import { buildTemplates3Model } from '../profile/forms3.ts' +import { subtypesToChartTypes } from '../profile/formsChartTypes.ts' + +/* +Tests the two pure pieces behind the DB-driven Templates 3 matrix/list picker: + +subtypesToChartTypes() — maps the child subtypes present under a domain to chart-type names: + - single subtype → one name; both subtypes → both names in canonical options order + - order independent of input; unknown/empty subtype set → [] + +buildTemplates3Model() — groups domains under their module using a derived chartTypesByDomain map: + - groups by the module segment of the id, preserving first-seen order; derives domain display name + - per-domain chart-type buttons come straight from the map + - a cross-domain chart is attached under every domain it spans + - empty config yields no groups +*/ + +const OPTIONS = [ + { name: 'Yes/No Barchart', subtype: 'YN Graph' }, + { name: 'Likert Scale', subtype: 'Likert Graph' } +] + +tape('subtypesToChartTypes - single subtype maps to its name', test => { + test.deepEqual(subtypesToChartTypes(new Set(['YN Graph']), OPTIONS), ['Yes/No Barchart']) + test.deepEqual(subtypesToChartTypes(new Set(['Likert Graph']), OPTIONS), ['Likert Scale']) + test.end() +}) + +tape('subtypesToChartTypes - both subtypes returned in canonical order regardless of input', test => { + // input Set built Likert-first, but output follows OPTIONS order (Yes/No first) + test.deepEqual(subtypesToChartTypes(new Set(['Likert Graph', 'YN Graph']), OPTIONS), [ + 'Yes/No Barchart', + 'Likert Scale' + ]) + test.end() +}) + +tape('subtypesToChartTypes - unknown or empty subtypes yield empty', test => { + test.deepEqual(subtypesToChartTypes(new Set(), OPTIONS), [], 'empty set => []') + test.deepEqual(subtypesToChartTypes(new Set(['Bogus Graph']), OPTIONS), [], 'unmatched subtype => []') + test.end() +}) + +tape('buildTemplates3Model - groups by module, derives names, preserves order', test => { + const cfg = { + options: OPTIONS, + domains: [ + { id: 'FContext__National Context__Care Access and Utilization' }, + { id: 'FDiagnostics__Diagnostics__General Laboratory' } + ] + } + const types = new Map([ + ['FContext__National Context__Care Access and Utilization', ['Yes/No Barchart']], + ['FDiagnostics__Diagnostics__General Laboratory', ['Likert Scale']] + ]) + const groups = buildTemplates3Model(cfg, types) + test.deepEqual( + groups.map(g => g.module), + ['National Context', 'Diagnostics'], + 'one group per module in first-seen order' + ) + test.deepEqual( + groups[0].domains.map(d => d.name), + ['Care Access and Utilization'], + 'domain display name is the third id segment' + ) + test.deepEqual(groups[0].domains[0].plotTypes, ['Yes/No Barchart'], 'chart types come from the derived map') + test.end() +}) + +tape('buildTemplates3Model - multi chart-type domain carries both from the map', test => { + const cfg = { options: OPTIONS, domains: [{ id: 'FContext__National Context__Care Access and Utilization' }] } + const types = new Map([ + ['FContext__National Context__Care Access and Utilization', ['Yes/No Barchart', 'Likert Scale']] + ]) + const groups = buildTemplates3Model(cfg, types) + test.deepEqual( + groups[0].domains[0].plotTypes, + ['Yes/No Barchart', 'Likert Scale'], + 'both derived chart types surface' + ) + test.end() +}) + +tape('buildTemplates3Model - cross-domain chart attached under each spanned domain', test => { + const heatmap = { label: 'Heat Map', chartType: 'matrix', domains: ['FContext__A__Alpha', 'FContext__B__Beta'] } + const cfg = { options: OPTIONS, domains: [{ id: 'FContext__A__Alpha' }], crossDomainCharts: [heatmap] } + const types = new Map([['FContext__A__Alpha', ['Likert Scale']]]) + const groups = buildTemplates3Model(cfg, types) + const alpha = groups.find(g => g.module == 'A')!.domains[0] + const beta = groups.find(g => g.module == 'B')!.domains[0] + test.equal(alpha.cross[0]?.label, 'Heat Map', 'heatmap attached to a forms domain it spans') + test.equal(beta.name, 'Beta', 'spanned domain with no forms charts is created from the id') + test.equal(beta.cross[0]?.label, 'Heat Map', 'heatmap also attached to the cross-only domain') + test.deepEqual(beta.plotTypes, [], 'cross-only domain has no forms chart-type buttons') + test.end() +}) + +tape('buildTemplates3Model - empty config yields no groups', test => { + test.deepEqual(buildTemplates3Model({}, new Map()), [], 'no domains and no cross-domain charts => empty') + test.end() +}) From b80d0b9f943adc9610a4de8aa6aff59d9667a7a2 Mon Sep 17 00:00:00 2001 From: vivek avuthu Date: Wed, 24 Jun 2026 11:50:15 -0500 Subject: [PATCH 2/2] cleanup and updated comments --- client/plots/profile/forms3.ts | 47 +++++-------------------- client/plots/profile/formsChartTypes.ts | 18 ++++++---- client/plots/test/forms3.unit.spec.ts | 17 +-------- 3 files changed, 20 insertions(+), 62 deletions(-) diff --git a/client/plots/profile/forms3.ts b/client/plots/profile/forms3.ts index 6aed1265a5..5b021cacdd 100644 --- a/client/plots/profile/forms3.ts +++ b/client/plots/profile/forms3.ts @@ -4,14 +4,12 @@ Templates 3 chart-button menu: a Module → Domain → Chart-type matrix/list vi Unlike Templates 2 (chart-type tabs over a termdb tree), this surfaces every template-bearing domain grouped under its PrOFILE module, with each available chart type shown as a labelled button next to the domain. A domain may expose -multiple chart types, and a single visualization may span several domains; both -are first-class here so users can see at a glance what exists and open it in one click. +multiple chart types, so users can see at a glance what exists and open it in one click. Config is read on the client from termdbConfig.plotConfigByCohort[cohort].profileForms2: - - domains[] forms3 reads only each entry's `id` (the template-bearing domains to surface). - The same entries carry `plotTypes` for Templates v1/v2, which forms3 ignores. - - options[] { name, subtype } the subtype → chart-type-name map (profileFormsOptions) - - crossDomainCharts[] { label, chartType, domains[], config } charts spanning domains (e.g. heatmaps) + - domains[] forms3 reads only each entry's `id` (the template-bearing domains to surface). + The same entries carry `plotTypes` for Templates v1/v2, which forms3 ignores. + - options[] { name, subtype } the subtype → chart-type-name map (profileFormsOptions) Each domain's chart types are derived from the database via getChartTypesByDomain (the child terms' subtypes), NOT from the config's plotTypes — so this picker can never drift from the data and a @@ -22,36 +20,22 @@ Module + domain display names are derived from the domain id, which the build en ____ e.g. 'FContext__National Context__Care Access and Utilization' so no extra metadata is needed. Module accent color comes from termdbConfig.colorMap[module]. -Forms buttons dispatch a profileForms2 plot pre-selected to that chart type (config.activeTab); -cross-domain buttons dispatch the chart type declared in config. Invoked by mass/charts.js -loadChartSpecificMenu via the standard chart-specific menu pattern. +Forms buttons dispatch a profileForms2 plot pre-selected to that chart type (config.activeTab). +Invoked by mass/charts.js loadChartSpecificMenu via the standard chart-specific menu pattern. */ import { getChartTypesByDomain, type FormsOption } from './formsChartTypes' export type FormsDomain = { id: string } -/* -PLACEHOLDER contract — a chart that spans multiple domains (e.g. a heatmap). Dispatched as-is via -{ chartType, ...config }. No such plot is wired in PrOFILE yet; the dataset's example points at the -generic 'matrix' chart until real heatmap terms/config land. See openCross() below. -*/ -export type CrossDomainChart = { - label: string - chartType: string - domains?: string[] - config?: { [key: string]: unknown } -} export type Forms2Config = { options?: FormsOption[] domains?: FormsDomain[] - crossDomainCharts?: CrossDomainChart[] } export type DomainEntry = { id: string name: string plotTypes: string[] - cross: CrossDomainChart[] searchText: string } export type ModuleGroup = { module: string; domains: DomainEntry[] } @@ -60,9 +44,8 @@ export type ModuleGroup = { module: string; domains: DomainEntry[] } Pure model builder: group template-bearing domains under their PrOFILE module, preserving first-seen order from the config. Module/domain display names come from the domain id (____). Each domain's chart-type buttons come from chartTypesByDomain -(derived from the DB by getChartTypesByDomain, already in canonical options order). Cross-domain -charts are attached under each domain they span. Exported separately from the DOM rendering and the -DB fetch so the grouping is unit-testable with a plain map. +(derived from the DB by getChartTypesByDomain, already in canonical options order). Exported +separately from the DOM rendering and the DB fetch so the grouping is unit-testable with a plain map. */ export function buildTemplates3Model(cfg: Forms2Config, chartTypesByDomain: Map): ModuleGroup[] { const order: string[] = [] @@ -84,7 +67,6 @@ export function buildTemplates3Model(cfg: Forms2Config, chartTypesByDomain: Map< id, name: domainName, plotTypes: [], - cross: [], searchText: `${moduleName} ${domainName}`.toLowerCase() } domains.set(id, entry) @@ -96,8 +78,6 @@ export function buildTemplates3Model(cfg: Forms2Config, chartTypesByDomain: Map< const entry = ensure(d.id) entry.plotTypes = chartTypesByDomain.get(d.id) || [] } - // A cross-domain chart is discoverable under each domain it spans (functional req: charts across domains). - for (const c of cfg.crossDomainCharts || []) for (const did of c.domains || []) ensure(did).cross.push(c) return order.map(module => ({ module, domains: [...byModule.get(module)!.values()] })) } @@ -144,16 +124,6 @@ export async function makeChartBtnMenu(holder, chartsInstance) { tw: { term: { id: domainId } }, activeTab: plotType }) - /* - PLACEHOLDER — not yet backed by data. Cross-domain charts (e.g. heatmaps) are dispatched - generically from their dataset-declared chartType + config. PrOFILE has no dedicated heatmap - plot today; the only cross-domain target that exists is the generic 'matrix' chart, which is - admin-only here and whose example config (in sjglobal.profile.ts crossDomainCharts) uses domain - multivalue terms as a stand-in. When real heatmap terms/config land, only that dataset entry - changes — this dispatch path stays the same. - */ - const openCross = (c: CrossDomainChart) => - dispatch({ chartType: c.chartType, activeCohort: chartsInstance.state.activeCohort, ...(c.config || {}) }) const container = holder.append('div').attr('class', 'sjpp-templates3').style('max-width', '520px') @@ -216,7 +186,6 @@ export async function makeChartBtnMenu(holder, chartsInstance) { row.append('div').style('min-width', '180px').style('font-size', '.9em').text(entry.name) const btns = row.append('div') for (const plotType of entry.plotTypes) makeBtn(btns, plotType, () => openForms(entry.id, plotType)) - for (const c of entry.cross) makeBtn(btns, c.label, () => openCross(c)) } } } diff --git a/client/plots/profile/formsChartTypes.ts b/client/plots/profile/formsChartTypes.ts index 35e9732bcb..c347ba6f74 100644 --- a/client/plots/profile/formsChartTypes.ts +++ b/client/plots/profile/formsChartTypes.ts @@ -33,13 +33,17 @@ export async function getChartTypesByDomain( options: FormsOption[] ): Promise> { /* - TEMP DEMO (revert before commit): force these domains to look multi-chart-type in the Templates 3 - picker WITHOUT touching the db — they get every option as a button. The real per-domain derivation - is the `subtypesToChartTypes(present, options)` line below; this only overrides the demo ids. - Caveat: the forms2 plot still builds its tabs from real db children, so clicking the mocked type - opens a plot whose tab has no data — this demos the picker UI, not a fully-populated plot. + Demo override while the multi-chart-type data isn't in the db yet. Every domain currently has a + single subtype, so there's no way to see how the Templates 3 picker lays out a domain that offers + more than one chart type. To unblock that review, we force these two domains to expose every + option as a button; every other domain still goes through the real db-derived list below + (subtypesToChartTypes). Remove this block once a domain genuinely carries two subtypes. + + Note for whoever clicks through it: this only shapes the picker. The forms2 plot still builds its + tabs from the actual db children, so opening the forced type lands on an empty tab — that's + expected here; we're validating the picker UI, not a populated plot. */ - const TEMP_MULTITYPE = new Set([ + const DEMO_MULTITYPE_DOMAINS = new Set([ 'FContext__National Context__Care Access and Utilization', 'FWorkforce__Service Integration__Communication' ]) @@ -48,7 +52,7 @@ export async function getChartTypesByDomain( const twLst = await vocabApi.getMultivalueTWs({ parent_id: id }) const present = new Set() for (const tw of twLst) if (tw.term.subtype) present.add(tw.term.subtype) - const types = TEMP_MULTITYPE.has(id) ? options.map(o => o.name) : subtypesToChartTypes(present, options) + const types = DEMO_MULTITYPE_DOMAINS.has(id) ? options.map(o => o.name) : subtypesToChartTypes(present, options) return [id, types] as const }) ) diff --git a/client/plots/test/forms3.unit.spec.ts b/client/plots/test/forms3.unit.spec.ts index 79202b67e7..7cd1f26082 100644 --- a/client/plots/test/forms3.unit.spec.ts +++ b/client/plots/test/forms3.unit.spec.ts @@ -12,7 +12,6 @@ subtypesToChartTypes() — maps the child subtypes present under a domain to cha buildTemplates3Model() — groups domains under their module using a derived chartTypesByDomain map: - groups by the module segment of the id, preserving first-seen order; derives domain display name - per-domain chart-type buttons come straight from the map - - a cross-domain chart is attached under every domain it spans - empty config yields no groups */ @@ -83,21 +82,7 @@ tape('buildTemplates3Model - multi chart-type domain carries both from the map', test.end() }) -tape('buildTemplates3Model - cross-domain chart attached under each spanned domain', test => { - const heatmap = { label: 'Heat Map', chartType: 'matrix', domains: ['FContext__A__Alpha', 'FContext__B__Beta'] } - const cfg = { options: OPTIONS, domains: [{ id: 'FContext__A__Alpha' }], crossDomainCharts: [heatmap] } - const types = new Map([['FContext__A__Alpha', ['Likert Scale']]]) - const groups = buildTemplates3Model(cfg, types) - const alpha = groups.find(g => g.module == 'A')!.domains[0] - const beta = groups.find(g => g.module == 'B')!.domains[0] - test.equal(alpha.cross[0]?.label, 'Heat Map', 'heatmap attached to a forms domain it spans') - test.equal(beta.name, 'Beta', 'spanned domain with no forms charts is created from the id') - test.equal(beta.cross[0]?.label, 'Heat Map', 'heatmap also attached to the cross-only domain') - test.deepEqual(beta.plotTypes, [], 'cross-only domain has no forms chart-type buttons') - test.end() -}) - tape('buildTemplates3Model - empty config yields no groups', test => { - test.deepEqual(buildTemplates3Model({}, new Map()), [], 'no domains and no cross-domain charts => empty') + test.deepEqual(buildTemplates3Model({}, new Map()), [], 'no domains and no chart groups => empty') test.end() })