fix: move null is zero to data source, fix bugs#340
Conversation
There was a problem hiding this comment.
Pull request overview
This PR moves the “treat empty values as zero” behavior from per-map-view configuration to the DataSource model/config, and updates stats calculation + UI to reflect the new source of truth.
Changes:
- Persist
nullIsZeroonDataSource(schema + migration) and allow updating it viadataSource.updateConfig. - Remove
nullIsZero/areaDataNullIsZerofrom area stats inputs and map view config, and update area stats computation to usedataSource.nullIsZero. - Update UI: remove the per-view toggle, add a DataSource configuration toggle, and improve Legend label rendering with value labels.
Reviewed changes
Copilot reviewed 11 out of 11 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| src/server/trpc/routers/dataSource.ts | Includes nullIsZero in DataSource config updates. |
| src/server/trpc/routers/area.ts | Removes nullIsZero from the area stats input. |
| src/server/stats/index.ts | Stats now use dataSource.nullIsZero (and adjust MODE selection). |
| src/server/models/MapView.ts | Removes areaDataNullIsZero from persisted view config schema. |
| src/server/models/DataSource.ts | Adds nullIsZero to the DataSource Zod schema/type. |
| src/app/map/[id]/utils/mapView.ts | Removes default areaDataNullIsZero from new view configs. |
| src/app/map/[id]/data.ts | Stops sending nullIsZero to the stats query and removes it from reset deps. |
| src/app/map/[id]/components/controls/VisualisationPanel/VisualisationPanel.tsx | Removes the per-view “Treat empty values as zero” switch; minor className update. |
| src/app/map/[id]/components/Legend.tsx | Uses valueLabels for legend labels; adjusts tick generation based on labels. |
| src/app/(private)/data-sources/[id]/components/ConfigurationForm.tsx | Adds a DataSource-level “Treat empty values as zero” switch and includes it in updates. |
| migrations/1772117945260_data_source_null_is_zero.ts | Adds nullIsZero column to dataSource. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| if (hasValueLabels) { | ||
| numTicks = Object.keys(valueLabels).length; | ||
| values = Object.keys(valueLabels) | ||
| .map(Number) | ||
| .filter((v) => v >= colorScheme.minValue && v <= colorScheme.maxValue) | ||
| .toSorted(); | ||
| numTicks = values.length; | ||
| denom = Math.max(numTicks - 1, 1); | ||
| values = Object.keys(valueLabels).map(Number).toSorted(); | ||
| } |
There was a problem hiding this comment.
When hasValueLabels is true, keys are filtered to those within [minValue, maxValue]. If none of the configured labels fall in-range, this sets numTicks to 0 and renders no tick labels (but still reserves the taller h-10 layout). Consider falling back to the default tick generation when the filtered list is empty (or at least keeping a minimum of 2 ticks).
| dateFormat: input.dateFormat, | ||
| public: input.public, | ||
| naIsNull: input.naIsNull, | ||
| nullIsZero: input.nullIsZero, | ||
| } as DataSourceUpdate; |
There was a problem hiding this comment.
The new nullIsZero flag is persisted via updateConfig, but the mutation handler only enqueues an importDataSource job when certain config fields change. If naIsNull (and possibly nullIsZero, depending on intent) should take effect on existing imported records, it likely needs to be included in the “configFieldsChanged” check; otherwise the UI’s “Save and import” flow won’t actually import when toggling these flags.
| updateDataSourceConfig({ | ||
| dataSourceId: dataSource.id, | ||
| columnRoles, | ||
| geocodingConfig: validGeocodingConfig, | ||
| autoImport, | ||
| dateFormat, | ||
| public: isPublic, | ||
| naIsNull, | ||
| nullIsZero, | ||
| }); |
There was a problem hiding this comment.
After saving nullIsZero, the client only invalidates dataSource.checkWebhookStatus. Since map rendering/stats now depend on dataSource.nullIsZero, consider also invalidating cached data source queries (e.g. dataSource.byId, dataSource.listReadable) and/or area.stats so other parts of the app don’t keep using stale cached results.
There was a problem hiding this comment.
@copilot open a new pull request to apply changes based on this feedback
| const { | ||
| calculationType, | ||
| areaDataColumn: column, | ||
| areaDataSecondaryColumn: secondaryColumn, | ||
| areaDataSourceId: dataSourceId, | ||
| areaDataNullIsZero: nullIsZero, | ||
| areaSetGroupCode, | ||
| } = viewConfig; | ||
|
|
There was a problem hiding this comment.
nullIsZero no longer participates in the area.stats query input/query key. Since the server-side stats result now depends on dataSource.nullIsZero, React Query can keep serving cached stats after the flag changes (e.g., user updates the data source config in another tab) until a manual invalidation/refetch happens. Consider invalidating trpc.area.stats queries when dataSource.updateConfig succeeds, or include a stable dataSource config version (or nullIsZero) in the query key/reset key so stats recompute immediately after this setting changes.
|
@joaquimds I've opened a new pull request, #341, to work on those changes. Once the pull request is ready, I'll request review from you. |
…onfig save Co-authored-by: joaquimds <12935136+joaquimds@users.noreply.github.com>
Invalidate additional caches after data source config save
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 11 out of 11 changed files in this pull request and generated 2 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| // Coalesce mode empty values to 0 if the column is numeric and nullIsZero is set | ||
| const modeSelect = | ||
| columnDef.type === ColumnType.Number && dataSource.nullIsZero | ||
| ? sql`MODE () WITHIN GROUP (ORDER BY COALESCE(NULLIF(json->>${column}, ''), '0'))` | ||
| : sql`MODE () WITHIN GROUP (ORDER BY NULLIF(json->>${column}, ''))`; |
There was a problem hiding this comment.
The new MODE query path also changes behavior when nullIsZero is enabled (and now treats empty strings as NULL when disabled). Please add a focused test for CalculationType.Mode on a numeric column with blank values to lock in the intended behavior for both nullIsZero=true and nullIsZero=false.
| // Coalesce numeric empty values to 0 if set | ||
| const numberSelect = dataSource.nullIsZero | ||
| ? sql`(COALESCE(NULLIF(json->>${column}, ''), '0'))::float` | ||
| : sql`(NULLIF(json->>${column}, ''))::float`; |
There was a problem hiding this comment.
getColumnValueByArea now changes numeric aggregation semantics based on dataSource.nullIsZero (coalescing empty strings to 0). There are existing Vitest feature tests that exercise getAreaStats, but none cover the new nullIsZero behavior; please add a test case that imports a CSV with blank numeric values and asserts that Sum/Avg differ when nullIsZero is enabled vs disabled.
…edge/ts-mapped into feat/fix-null-is-zero-issues
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 16 out of 16 changed files in this pull request and generated 2 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| > | ||
| <SelectTrigger | ||
| className="w-full min-w-0" | ||
| className={cn("w-full min-w-0", SELECT_TO_BUTTON_CLASSES)} |
There was a problem hiding this comment.
The constant SELECT_TO_BUTTON_CLASSES is referenced but not visible in the diff. Ensure this constant is properly imported or defined in this file.
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
No description provided.