GSoC 2026 Final Report: Data Density Heatmap, PCDC Data Portal

This is my Google Summer of Code 2026 work product with Data for the Common Good (D4CG). It covers the project goals, what shipped into the PCDC Data Portal, where the code lives, the current state, and what remains.
Contents
- Project information
- Goals
- Approach
- What shipped
- Current state
- What’s left
- Code
- Challenges and lessons
- About the author
Project information
| Contributor | Suryansh Singh ( @devSuryansh) |
| Organization | Data for the Common Good (D4CG) |
| Project | Data Density Heatmap Application |
| Size | 350 hours |
| Ticket | PEDS-1599 |
| Upstream repository | chicagopcdc/data-portal |
| Fork | devSuryansh/data-portal |
| Product location | Explorer, density heatmap tab, behind heatmapConfig.enabled |
| Mentors | Brian Furner ( @bfurner), Luca Graglia ( @grugna), Tianyun Zhang ( @skyzhangty) |
| Coding period | 25 May 2026 to 16 August 2026. Final submission week 17 to 24 August. |
Goals
From D4CG’s GSoC ideas list, Project 5:
Develop a web-based heatmap visualization tool that represents the completeness and distribution of data across an entire dataset. Display data “density” by GraphQL node types and their specific attributes, so users can quickly identify areas with high or low data availability. Support researchers and data managers in assessing dataset quality and guiding curation.
Expected outcome: a configuration-driven heatmap generated from live GraphQL. Project size: 350 hours.
The PCDC Explorer already shows what is in a cohort. Summary charts, tables, and survival views do not show which fields are sparsely filled. Density here is completeness, not geography:
completeness = availableCount / totalCountavailableCount comes from Guppy histogram aggregations. totalCount is the filtered cohort size. Values are clipped at 1 so nested histograms cannot overshoot. Percentages use at most two decimal places, with trailing zeroes dropped.
In practice: out of every hundred subjects in the current cohort, how many have this field filled in? A data manager should be able to see that before designing a study around a sparse variable.
Approach
During proposal season I built a standalone prototype,
data-density-heatmap-application (
demo): Next.js, TypeScript, D3, a seeded GraphQL endpoint, and CSV/SVG export. It matches the original idea list: nodes on one axis, attributes on the other, cells colored by nonNullCount / totalRecords.
The product is not that sidecar. Researchers already work in the PCDC Data Portal. The useful path was a new Explorer view on the same cohort, the same filters, and the same portal config.
Downloading every subject row to count nulls would freeze the portal. The live view therefore uses Guppy _aggregation queries: histogram counts per field, grouped by GraphQL node, rendered as completeness strips using the portal’s rose / bee / lime tokens.
The prototype remained a design reference. The D3 cell matrix did not ship in Explorer.
What shipped
Work landed in sequence so each step could go upstream on its own.
Tab and configuration.
PR 709, merged 22 June 2026. A density heatmap view in ExplorerVisualization, a placeholder card, field utilities, tests, and:
"heatmapConfig": {
"enabled": true
}Enabled in pcdc.json. Disabled in default.json, so other commons do not get the tab unless they opt in.
User agreement. PR 716, merged 4 July 2026. The view uses the same agreement pattern as Survival Analysis before showing cohort completeness.
Functional heatmap. PR 722, merged 15 July 2026. The live view:
- Shows total records and fields tracked.
- Groups rows by the first segment of the GraphQL path (
histologies.*,survival_characteristics.*, subject-level fields). - Renders each field as a label, the raw path, a completeness strip, percentage, available count, and missing count.
- Defaults to curated fields from
Object.keys(chartConfig). - Adds a Filter-related fields section for active OPTION, RANGE, and ANCHORED filters via
extractFieldsFromFilter. - Offers Show all fields for the full Guppy mapping.
The aggregation query, simplified:
query ($filter_main: JSON) {
_aggregation {
main: subject(filter: $filter_main, accessibility: all) {
sex { histogram { key count } }
histologies {
age_at_course_anc_500 { histogram { key count } }
}
}
}
}The client walks the response along each field path, sums histogram counts, divides by cohort size, and paints the strip.
Field labels.
PR 729, merged 6 August 2026. Nested paths such as histologies.age_at_course_anc_500 were rendering as “Histologies Age At Course Anc 500”, repeating the node already used as the section header.
- Prefer
fieldMapping/filterConfig.infolabels when present. - Otherwise strip the first path segment and capitalize the attribute.
- Leave top-level fields such as
file_typeasFile Type. - Keep the raw GraphQL path on the second line.
Before:
After:
Progressive loading. PR 735. Show all fields currently issues one GraphQL document for every mapped attribute. That query can take minutes and block Summary and Table while the response is in flight.
The follow-up splits fields by category, fetches with concurrency 2, stores the job in Redux as densityHeatmapResult so leaving the tab does not cancel work, reuses cache for the same filter and field set, starts a new job on filter change, paints skeletons then rows, and prioritizes visible sections. New files live on
feat/density-heatmap-progressive-load:
src/redux/explorer/densityHeatmapAPI.jssrc/redux/explorer/densityHeatmapThunks.js- updates in
slice.jsand the heatmap component
This work is not on pcdc_dev yet.
Current state
On pcdc_dev, Explorer exposes a density heatmap tab when heatmapConfig.enabled is true. PCDC config already has that on. A researcher can open the view, see completeness for curated fields on the current cohort, read labels that match the rest of the portal, and see filter-related fields in their own section. Nested fields no longer repeat the node name.
What it is not:
- Not the D3 cell matrix from the prototype. Explorer uses completeness strips grouped by node, which scans better for a nested clinical model.
buildDensityHeatmapModelremains inutils.jsfrom the first design and is unused by the live view. - Not finished for Show all fields. That path still uses a single aggregation query.
- Not a row-level data download. Density is a map of missingness.
Clone
chicagopcdc/data-portal on pcdc_dev for the live behavior. Check out feat/density-heatmap-progressive-load on the fork for the progressive loader.
What’s left
- Land progressive loading. Failed category fetches should not count as loaded. Retry up to three times, then show an error for that slice. Visible sections should take the next worker slot. Then merge to
pcdc_dev. - Harden Show all fields. After chunking, slow categories still need timeouts, partial paint, and a retry control in the UI.
buildDensityHeatmapModel. Either wire a bucketed raw-data mode, or remove the unused helper.- Export. The prototype could write CSV and SVG. The portal view cannot yet.
- Empty cohorts. Completeness is already guarded at
totalCount === 0. The empty-state copy can be clearer than “No density data available for the configured fields.” - User documentation. A short note in the portal guide: what the colors mean, why curated fields are the default, and when to use Show all fields.
The idea list asked for a configuration-driven heatmap on live GraphQL. That view is in Explorer. The remaining work is keeping the rest of the explorer usable when the full field set is requested.
Code
Merged upstream
| Date | PR | What |
|---|---|---|
| 22 Jun 2026 | #709 | Tab, placeholder, heatmapConfig, utils and tests |
| 4 Jul 2026 | #716 | User agreement, same pattern as Survival Analysis |
| 15 Jul 2026 | #722 | Functional heatmap: aggregations, strips, curated / filter / show-all |
| 6 Aug 2026 | #729 | Field labels: mapping first, then strip node prefix |
On a branch
| Date | PR | What |
|---|---|---|
| 21 Aug 2026 | #735 | Progressive category loading in Redux |
Commits
73ef3c53feat(explorer): add density heatmap placeholder tabb6d8468dfeat(heatmapConfig): toggle visibility from config65fbc6a6feat(density-heatmap): user agreement, same pattern as Survival Analysisbcb72a1cfeat(ExplorerDensityHeatmap): functional view9f70dad1fix: ESLintf54f716dfix(density-heatmap): strip node prefix from field labels1286d9a2feat(density-heatmap): load densities by category in the background
Files to extend
src/GuppyDataExplorer/ExplorerDensityHeatmap/index.jsxsrc/GuppyDataExplorer/ExplorerDensityHeatmap/utils.jssrc/GuppyDataExplorer/ExplorerDensityHeatmap/utils.test.jssrc/GuppyDataExplorer/ExplorerDensityHeatmap/ExplorerDensityHeatmap.css- Wiring:
ExplorerVisualization/index.jsx(heatmapConfig.enabled,primaryFields={Object.keys(chartConfig)},expandByFilter) - Config:
data/config/pcdc.json,data/config/default.json - Types:
src/redux/explorer/types.d.ts,src/redux/explorer/utils.js
Prototype
- Repository: devSuryansh/data-density-heatmap-application
- Demo: data-density-heatmap-application.vercel.app
Challenges and lessons
Ship in the portal, not beside it. A standalone heatmap would have been easier to demo. It would not have been used. Density has to sit on the same cohort and filters as Summary and Table.
Aggregate, do not scan rows. Completeness for a commons this size belongs in histogram counts, not in a browser-side null tally.
One query does not scale to the full mapping. Curated fields are a reasonable default. Show all fields needs batched, cancellable loads from the start.
Reuse portal conventions. Colors from Gen3 tokens, curated fields from chart config, field lists from GuppyWrapper, agreement flow from Survival Analysis. Parallel pipes go stale when a data release lands.
Labels are part of the feature. histologies.age_at_course_anc_500 is a correct path. It is a poor title. Mapping first, then strip the node.
Test the pure helpers. Histogram walking in the component is messy. formatDensityPercentage, getDensityHeatmapFieldLabel, and extractFieldsFromFilter are not. Sixteen cases in utils.test.js are why the label change did not break the heatmap.
The original idea was a matrix. The lasting design problem is a queue: how to tell the truth about every field without locking Explorer.
What’s next
I still intend to land progressive loading on pcdc_dev, add export for curation tickets, and keep the view usable when Show all fields is on. The heatmap is in the portal. The full-field path still needs a slower, safer engine.
About the author
Suryansh Singh is a Computer Science student at the Noida Institute of Engineering and Technology. During GSoC 2026 he worked with Data for the Common Good on the PCDC Data Portal.
- Website: devsuryansh.in
- GitHub: @devSuryansh
- LinkedIn: suryansh–singh
- X: @0xSuryanshHere
- Email:
Acknowledgements
Thank you to Brian Furner, Luca Graglia, and Tianyun Zhang for mentorship this summer, to the D4CG and PCDC teams for making room for this view in Explorer, and to Google Summer of Code. The researchers who use this tab now have a color for the fields that cannot yet answer their questions.
Google Summer of Code 2026 · Data for the Common Good · PEDS-1599 · chicagopcdc/data-portal