Skip to content

Comments

feat: add data source disclaimers to all dashboards#424

Merged
pethers merged 7 commits intomainfrom
copilot/add-comprehensive-dashboard-tests
Feb 22, 2026
Merged

feat: add data source disclaimers to all dashboards#424
pethers merged 7 commits intomainfrom
copilot/add-comprehensive-dashboard-tests

Conversation

Copy link
Contributor

Copilot AI commented Feb 22, 2026

  • Add shared showDataSourceDisclaimer() utility in dom-utils.ts
  • Add CSS styling using theme variables (--success-color, --warning-color, --primary-cyan)
  • Update all 9 dashboards with data source disclaimer banners
  • Make anomaly-detection fallback data deterministic (no Math.random())
  • Fix heading insertion to use insertAdjacentElement('afterend') for all cases
  • Reset DOM in test beforeEach to avoid cross-test coupling
  • Add "heading is last child" positioning test
  • All 1216 tests passing, build succeeds

💬 We'd love your input! Share your thoughts on Copilot coding agent in our 2 minute survey.

…ion chart time scale

- Anomaly Detection Dashboard: Add generateFallbackData() to provide synthetic
  data when CSV sources are unavailable (consistent with other dashboards).
  Also call hideLoading() in error handler so canvases aren't left hidden.
- Coalition Dashboard: Change votingAnomalyChart x-axis from type:'time' to
  type:'linear' with custom tick formatter, avoiding the missing date adapter
  error ("This method is not implemented").

Co-authored-by: pethers <[email protected]>
Use consistent z-score values: ballot_z_score and doc_z_score now drive
severity/anomaly_type/direction classification. max_z_score is derived
as max(abs(ballotZ), abs(docZ)). Remove unused constant arrays.

Co-authored-by: pethers <[email protected]>
Copilot AI changed the title [WIP] Add comprehensive coverage tests for all dashboards fix: anomaly detection dashboard fallback data and coalition chart time scale Feb 22, 2026
Copilot AI requested a review from pethers February 22, 2026 01:17
@github-actions github-actions bot added deployment Deployment configuration size-xl Extra large change (> 1000 lines) labels Feb 22, 2026
@github-actions
Copy link
Contributor

🔍 Lighthouse Performance Audit

Category Score Status
Performance 85/100 🟡
Accessibility 95/100 🟢
Best Practices 90/100 🟢
SEO 95/100 🟢

📥 Download full Lighthouse report

Budget Compliance: Performance budgets enforced via budget.json

Copilot AI and others added 2 commits February 22, 2026 01:36
- Add showDataSourceDisclaimer() utility in dom-utils.ts with DataSourceType
- Add CSS styling for .data-source-disclaimer (live/synthetic/mock variants)
- Anomaly Detection: tracks 'synthetic' when generateFallbackData() used
- Coalition: tracks 'mock' when any generateMock*() function used
- All other dashboards (party, committee, ministry, risk, election-cycle,
  seasonal-patterns, pre-election, politician): show 'live' disclaimer

Co-authored-by: pethers <[email protected]>
Copilot AI changed the title fix: anomaly detection dashboard fallback data and coalition chart time scale feat: add data source disclaimers to all dashboards Feb 22, 2026
@pethers pethers requested a review from Copilot February 22, 2026 02:06
@github-actions github-actions bot added html-css HTML/CSS changes testing Test coverage labels Feb 22, 2026
@github-actions
Copy link
Contributor

🔍 Lighthouse Performance Audit

Category Score Status
Performance 85/100 🟡
Accessibility 95/100 🟢
Best Practices 90/100 🟢
SEO 95/100 🟢

📥 Download full Lighthouse report

Budget Compliance: Performance budgets enforced via budget.json

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a shared “data source disclaimer” banner across dashboard pages to indicate whether displayed data is live CSV, synthetic fallback, or mock/demo—plus dashboard-specific fixes to improve resilience when CSV fetches fail and to avoid Chart.js time-scale adapter errors.

Changes:

  • Introduces showDataSourceDisclaimer(container, type) and DataSourceType in shared DOM utilities and integrates the banner into multiple dashboards.
  • Updates Anomaly Detection to render with synthetic fallback data when all CSV sources fail (and ensures loading is hidden on init failure).
  • Adds styling and unit tests for the disclaimer banner.

Reviewed changes

Copilot reviewed 13 out of 14 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
tests/data-source-disclaimer.test.ts Adds unit tests for disclaimer creation, positioning, dedupe, accessibility, and type safety.
styles.css Adds .data-source-disclaimer styling and per-type variants.
src/browser/shared/dom-utils.ts Adds DataSourceType and showDataSourceDisclaimer() utility.
src/browser/dashboards/seasonal-patterns.ts Shows “live” disclaimer after successful data fetch.
src/browser/dashboards/risk-dashboard.ts Shows “live” disclaimer before rendering risk visualizations.
src/browser/dashboards/pre-election.ts Shows “live” disclaimer after init validation.
src/browser/dashboards/politician-dashboard.ts Shows “live” disclaimer after data load.
src/browser/dashboards/party-dashboard.ts Shows “live” vs “synthetic” disclaimer based on whether CSV produced data.
src/browser/dashboards/ministry-dashboard.ts Shows “live” disclaimer after loading overlay removal.
src/browser/dashboards/election-cycle.ts Shows “live” disclaimer after successful fetchAllData().
src/browser/dashboards/committees-dashboard.ts Shows “live” disclaimer after successful data load.
src/browser/dashboards/coalition-dashboard.ts Tracks data source type (live/mock) and switches Chart.js x-axis to linear ticks.
src/browser/dashboards/anomaly-detection.ts Adds synthetic fallback data generation, tracks dataSourceType, shows disclaimer, and hides loading on init failure.

Comment on lines 155 to 158
// Insert after the first heading or at the top of the container
const heading = container.querySelector('h2, h3');
if (heading && heading.nextSibling) {
heading.parentNode!.insertBefore(disclaimer, heading.nextSibling);
Copy link

Copilot AI Feb 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

showDataSourceDisclaimer() prepends the banner when a heading exists but has no nextSibling (e.g., container contains only an <h2>). In that case the disclaimer ends up before the heading, contradicting the “insert after heading” intent. Consider inserting after the heading whenever a heading is found (e.g., insertAdjacentElement('afterend', ...) or parentNode.insertBefore(..., heading.nextSibling ?? null)), and add a unit test that covers the “heading is last child” case.

Suggested change
// Insert after the first heading or at the top of the container
const heading = container.querySelector('h2, h3');
if (heading && heading.nextSibling) {
heading.parentNode!.insertBefore(disclaimer, heading.nextSibling);
// Insert after the first heading if present, otherwise at the top of the container
const heading = container.querySelector('h2, h3');
if (heading) {
heading.insertAdjacentElement('afterend', disclaimer);

Copilot uses AI. Check for mistakes.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in c8ac3e6 — switched to heading.insertAdjacentElement('afterend', disclaimer) which works correctly even when the heading is the last child. Also added a dedicated unit test for the "heading is last child" case.

Comment on lines 20 to 25
beforeEach(() => {
container = document.createElement('section');
container.id = 'test-dashboard';
container.innerHTML = '<h2>Test Dashboard</h2><div class="dashboard-grid"></div>';
document.body.appendChild(container);
});
Copy link

Copilot AI Feb 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The beforeEach appends a new container to document.body but never resets/removes it, so the DOM accumulates across tests. Other DOM-based tests in this repo typically reset document.body.innerHTML in beforeEach, which avoids cross-test coupling and memory growth.

Copilot uses AI. Check for mistakes.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in c8ac3e6 — added document.body.innerHTML = ''; at the start of beforeEach to match the pattern used by other DOM-based tests in the repo.

styles.css Outdated
Comment on lines 7600 to 7614
background: rgba(76, 175, 80, 0.1);
border: 1px solid rgba(76, 175, 80, 0.3);
color: #4caf50;
}

.data-source-synthetic {
background: rgba(255, 152, 0, 0.1);
border: 1px solid rgba(255, 152, 0, 0.3);
color: #ff9800;
}

.data-source-mock {
background: rgba(0, 217, 255, 0.1);
border: 1px solid rgba(0, 217, 255, 0.3);
color: #00d9ff;
Copy link

Copilot AI Feb 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These new banner variants introduce hard-coded colors (#4caf50, #ff9800, #00d9ff). The stylesheet already defines accessible theme tokens like --success-color and --warning-color (and uses var(--primary-cyan, #00d9ff) elsewhere). Using those variables here would keep the banner consistent with the site theme and WCAG-tuned contrast values.

Suggested change
background: rgba(76, 175, 80, 0.1);
border: 1px solid rgba(76, 175, 80, 0.3);
color: #4caf50;
}
.data-source-synthetic {
background: rgba(255, 152, 0, 0.1);
border: 1px solid rgba(255, 152, 0, 0.3);
color: #ff9800;
}
.data-source-mock {
background: rgba(0, 217, 255, 0.1);
border: 1px solid rgba(0, 217, 255, 0.3);
color: #00d9ff;
color: var(--success-color);
border: 1px solid color-mix(in srgb, var(--success-color) 40%, transparent);
background-color: color-mix(in srgb, var(--success-color) 10%, transparent);
}
.data-source-synthetic {
color: var(--warning-color);
border: 1px solid color-mix(in srgb, var(--warning-color) 40%, transparent);
background-color: color-mix(in srgb, var(--warning-color) 10%, transparent);
}
.data-source-mock {
color: var(--primary-cyan, #00d9ff);
border: 1px solid color-mix(in srgb, var(--primary-cyan, #00d9ff) 40%, transparent);
background-color: color-mix(in srgb, var(--primary-cyan, #00d9ff) 10%, transparent);

Copilot uses AI. Check for mistakes.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in c8ac3e6 — now uses var(--success-color), var(--warning-color), and var(--primary-cyan, #00d9ff) with color-mix() for border and background opacity.

Comment on lines 285 to 306
const ballotZ = (Math.random() * 4 - 1).toFixed(4);
const docZ = (Math.random() * 4 - 1).toFixed(4);
const absBallotZ = Math.abs(parseFloat(ballotZ));
const absDocZ = Math.abs(parseFloat(docZ));
const maxZ = Math.max(absBallotZ, absDocZ);
let severity = 'LOW';
let anomalyType = 'NO_ANOMALY';
let direction = 'WITHIN_NORMAL_RANGE';
const dominantZ = absBallotZ >= absDocZ ? ballotZ : docZ;

if (maxZ >= 3.0) { severity = 'CRITICAL'; anomalyType = absBallotZ >= absDocZ ? 'BALLOT_ANOMALY' : 'DOCUMENT_ANOMALY'; direction = parseFloat(dominantZ) > 0 ? 'UNUSUALLY_HIGH' : 'UNUSUALLY_LOW'; }
else if (maxZ >= 2.0) { severity = 'HIGH'; anomalyType = absBallotZ >= absDocZ ? 'BALLOT_ANOMALY' : 'DOCUMENT_ANOMALY'; direction = parseFloat(dominantZ) > 0 ? 'UNUSUALLY_HIGH' : 'UNUSUALLY_LOW'; }
else if (maxZ >= 1.0) { severity = 'MODERATE'; }

data.push({
year: String(year),
quarter: String(quarter),
is_election_year: (year === 2022) ? 't' : 'f',
total_ballots: String(100 + Math.floor(Math.random() * 200)),
active_politicians: '349',
attendance_rate: '100.00',
documents_produced: String(200 + Math.floor(Math.random() * 500)),
Copy link

Copilot AI Feb 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

generateFallbackData() uses Math.random() to generate z-scores, which makes the synthetic fallback non-deterministic and can randomly produce HIGH/CRITICAL anomalies. Because checkForCriticalAnomalies() triggers alerts based on the first 2 records, the dashboard can show critical banners unpredictably when running on fallback data. Consider making the fallback deterministic (e.g., fixed seeded values or a simple year/quarter-based function) and/or constraining fallback severities to non-alerting levels.

Suggested change
const ballotZ = (Math.random() * 4 - 1).toFixed(4);
const docZ = (Math.random() * 4 - 1).toFixed(4);
const absBallotZ = Math.abs(parseFloat(ballotZ));
const absDocZ = Math.abs(parseFloat(docZ));
const maxZ = Math.max(absBallotZ, absDocZ);
let severity = 'LOW';
let anomalyType = 'NO_ANOMALY';
let direction = 'WITHIN_NORMAL_RANGE';
const dominantZ = absBallotZ >= absDocZ ? ballotZ : docZ;
if (maxZ >= 3.0) { severity = 'CRITICAL'; anomalyType = absBallotZ >= absDocZ ? 'BALLOT_ANOMALY' : 'DOCUMENT_ANOMALY'; direction = parseFloat(dominantZ) > 0 ? 'UNUSUALLY_HIGH' : 'UNUSUALLY_LOW'; }
else if (maxZ >= 2.0) { severity = 'HIGH'; anomalyType = absBallotZ >= absDocZ ? 'BALLOT_ANOMALY' : 'DOCUMENT_ANOMALY'; direction = parseFloat(dominantZ) > 0 ? 'UNUSUALLY_HIGH' : 'UNUSUALLY_LOW'; }
else if (maxZ >= 1.0) { severity = 'MODERATE'; }
data.push({
year: String(year),
quarter: String(quarter),
is_election_year: (year === 2022) ? 't' : 'f',
total_ballots: String(100 + Math.floor(Math.random() * 200)),
active_politicians: '349',
attendance_rate: '100.00',
documents_produced: String(200 + Math.floor(Math.random() * 500)),
// Deterministic, bounded z-scores to avoid random HIGH/CRITICAL anomalies in fallback data
const ballotZRaw = (((year * 31 + quarter * 17) % 300) / 100) - 1.5; // approx range [-1.5, 1.49]
const docZRaw = (((year * 19 + quarter * 23) % 300) / 100) - 1.5;
const ballotZ = ballotZRaw.toFixed(4);
const docZ = docZRaw.toFixed(4);
const absBallotZ = Math.abs(ballotZRaw);
const absDocZ = Math.abs(docZRaw);
const maxZ = Math.max(absBallotZ, absDocZ);
let severity = 'LOW';
let anomalyType = 'NO_ANOMALY';
let direction = 'WITHIN_NORMAL_RANGE';
const dominantZ = absBallotZ >= absDocZ ? ballotZRaw : docZRaw;
if (maxZ >= 3.0) { severity = 'CRITICAL'; anomalyType = absBallotZ >= absDocZ ? 'BALLOT_ANOMALY' : 'DOCUMENT_ANOMALY'; direction = dominantZ > 0 ? 'UNUSUALLY_HIGH' : 'UNUSUALLY_LOW'; }
else if (maxZ >= 2.0) { severity = 'HIGH'; anomalyType = absBallotZ >= absDocZ ? 'BALLOT_ANOMALY' : 'DOCUMENT_ANOMALY'; direction = dominantZ > 0 ? 'UNUSUALLY_HIGH' : 'UNUSUALLY_LOW'; }
else if (maxZ >= 1.0) { severity = 'MODERATE'; }
data.push({
year: String(year),
quarter: String(quarter),
is_election_year: (year === 2022) ? 't' : 'f',
// Deterministic synthetic counts for reproducible fallback data
total_ballots: String(100 + ((year * 13 + quarter * 7) % 200)),
active_politicians: '349',
attendance_rate: '100.00',
documents_produced: String(200 + ((year * 29 + quarter * 11) % 500)),

Copilot uses AI. Check for mistakes.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in c8ac3e6 — replaced Math.random() with deterministic (year * 31 + quarter * 17) % 300 formulas. Fallback z-scores now range approximately [-1.5, 1.49], which keeps all severities at LOW/MODERATE and avoids triggering critical anomaly banners unpredictably.

@pethers
Copy link
Member

pethers commented Feb 22, 2026

@copilot apply changes based on the comments in this thread

…CSS vars, heading insertion, test cleanup

- dom-utils.ts: Use insertAdjacentElement('afterend') for reliable heading insertion
- anomaly-detection.ts: Replace Math.random() with deterministic year/quarter-based values
- styles.css: Use --success-color, --warning-color, --primary-cyan theme variables
- tests: Reset document.body.innerHTML in beforeEach, add "heading is last child" test

Co-authored-by: pethers <[email protected]>
Copy link
Contributor Author

Copilot AI commented Feb 22, 2026

@copilot apply changes based on the comments in this thread

All 4 review comments addressed in c8ac3e6:

  1. insertAdjacentElement('afterend') for heading insertion + new test for "heading is last child"
  2. document.body.innerHTML = '' reset in beforeEach
  3. Theme CSS variables (--success-color, --warning-color, --primary-cyan)
  4. Deterministic fallback data (no Math.random())

@github-actions github-actions bot added the size-l Large change (250-1000 lines) label Feb 22, 2026
@github-actions
Copy link
Contributor

🔍 Lighthouse Performance Audit

Category Score Status
Performance 85/100 🟡
Accessibility 95/100 🟢
Best Practices 90/100 🟢
SEO 95/100 🟢

📥 Download full Lighthouse report

Budget Compliance: Performance budgets enforced via budget.json

@pethers pethers marked this pull request as ready for review February 22, 2026 02:41
@pethers pethers merged commit ff05b92 into main Feb 22, 2026
24 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

deployment Deployment configuration html-css HTML/CSS changes size-l Large change (250-1000 lines) size-xl Extra large change (> 1000 lines) testing Test coverage

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants