feat: breakdown map by provinces/states on click#334
feat: breakdown map by provinces/states on click#334maoshuorz wants to merge 2 commits intodatabuddy-analytics:mainfrom
Conversation
Clicking a country on the choropleth map now drills down to show its subdivisions (states/provinces) with region-level visitor data. A back button allows returning to the world view. Closes databuddy-analytics#312
|
@maoshuorz is attempting to deploy a commit to the Databuddy OSS Team on Vercel. A member of the Team first needs to authorize it. |
Greptile SummaryThis PR adds an interactive drill-down feature to the choropleth map: clicking a country switches the view to its states/provinces (subdivisions), colors them by region visitor data, shows a per-subdivision hover tooltip, and provides a flagged back button to return to the world view. The implementation is well-structured and reuses existing However, there are two related logic bugs that will produce visually incorrect output in practice:
Additional smaller issues: Confidence Score: 2/5
Important Files Changed
Sequence DiagramsequenceDiagram
participant User
participant MapComponent
participant LeafletGeoJSON
participant useCountries
participant useSubdivisions
participant regionData
User->>LeafletGeoJSON: click country feature
LeafletGeoJSON->>MapComponent: handleEachFeature → click (ISO_A2, ADMIN)
MapComponent->>MapComponent: setDrillDownCountry({ code, name })
MapComponent->>MapComponent: mapView = "subdivisions"
MapComponent->>useSubdivisions: fetch subdivisions.json (cached)
useSubdivisions-->>MapComponent: subdivisionsGeoData
MapComponent->>MapComponent: filteredSubdivisions = features where<br/>admin === name OR iso_3166_2.startsWith(code+"-")
MapComponent->>useCountries: find countryFeature for centroid
MapComponent->>MapComponent: mapRef.setView(centroid, zoom=5)
MapComponent->>LeafletGeoJSON: render filteredSubdivisions with handleSubdivisionStyle
Note over MapComponent,regionData: colorScale uses ALL regionData (all countries)<br/>⚠️ should be filtered to drillDownCountry only
User->>LeafletGeoJSON: hover subdivision
LeafletGeoJSON->>MapComponent: handleEachSubdivisionFeature → mouseover
MapComponent->>regionData: find by iso_3166_2 or name
MapComponent->>MapComponent: setTooltipContent (name, count, percentage)
Note over MapComponent: percentage = visitors / ALL_global_regions_total<br/>⚠️ should be country-scoped
User->>MapComponent: click Back button
MapComponent->>MapComponent: setDrillDownCountry(null)
MapComponent->>MapComponent: mapView = "countries"
MapComponent->>MapComponent: mapRef.setView([20,10], 1.8)
Last reviewed commit: 92c81de |
| @@ -135,9 +163,28 @@ export function MapComponent({ | |||
| } | |||
| return `${themeColors.chart3} / ${(0.8 + intensity * 0.2).toFixed(2)})`; | |||
| }; | |||
| }, [countryData?.data, themeColors]); | |||
| }, [countryData?.data, regionData?.data, mapView, themeColors]); | |||
There was a problem hiding this comment.
colorScale calibrated against all global regions, not current country
When mapView === "subdivisions", colorScale is computed using regionData?.data — which contains regions from every country — to determine the min/max range. This means if France has a region with 50,000 visitors and you're viewing US subdivisions with at most 500, every US state will render with near-zero intensity (barely distinguishable from "no data") because the scale tops out at 50,000.
The fix is to filter regionData?.data to only the current country's subdivisions before computing the scale:
const dataSource = useMemo(() => {
if (mapView === "subdivisions") {
return regionData?.data?.filter((d) =>
d.value.startsWith(`${drillDownCountry?.code}-`)
);
}
return countryData?.data;
}, [mapView, regionData?.data, countryData?.data, drillDownCountry?.code]);Then use dataSource (instead of re-deriving inside colorScale) to drive both the scale and percentage denominator.
| const regionData = useMemo(() => { | ||
| if (!locationsData?.regions) { | ||
| return null; | ||
| } | ||
|
|
||
| const validRegions = locationsData.regions.filter( | ||
| (r) => r.country && r.country.trim() !== "" | ||
| ); | ||
|
|
||
| const totalVisitors = | ||
| validRegions.reduce((sum, r) => sum + r.visitors, 0) || 1; | ||
|
|
||
| return { | ||
| data: validRegions.map((region) => ({ | ||
| value: region.country, | ||
| count: region.visitors, | ||
| percentage: (region.visitors / totalVisitors) * 100, | ||
| })), | ||
| }; | ||
| }, [locationsData?.regions]); |
There was a problem hiding this comment.
Tooltip percentage calculated against global region total
totalVisitors here sums visitors from every region worldwide. When a subdivision tooltip is shown, foundData.percentage will therefore represent a fraction of global traffic rather than a fraction of the country's traffic. For example, a US state with 90% of US traffic might show "0.3%" in the tooltip because it's 0.3% of all worldwide regions.
The percentage should be re-computed (or at least re-derived) within the context of the selected country's regions, not globally. One approach is to re-compute totalVisitors as the sum of only the country's matching subdivisions at tooltip time:
const countrySubdivisionTotal = regionData?.data
?.filter((d) => d.value.startsWith(`${drillDownCountry?.code}-`))
.reduce((sum, d) => sum + d.count, 0) || 1;
const percentage = foundData ? (foundData.count / countrySubdivisionTotal) * 100 : 0;| const filteredSubdivisions = useMemo(() => { | ||
| if (!drillDownCountry || !subdivisionsGeoData) { | ||
| return null; | ||
| } | ||
|
|
||
| const filtered = subdivisionsGeoData.features.filter( | ||
| (f) => | ||
| f.properties.admin === drillDownCountry.name || | ||
| f.properties.iso_3166_2?.startsWith(`${drillDownCountry.code}-`) | ||
| ); | ||
|
|
||
| return { | ||
| type: "FeatureCollection" as const, | ||
| features: filtered, | ||
| } as FeatureCollection; | ||
| }, [drillDownCountry, subdivisionsGeoData]); |
There was a problem hiding this comment.
Silent blank map if subdivision filter yields no features
If both filter conditions (admin name match and iso_3166_2 prefix match) produce zero results — e.g., for a country whose names differ between the two GeoJSON datasets — filteredSubdivisions.features will be an empty array. The component renders nothing in that case without any feedback to the user, and the back button is the only hint that something went wrong.
Consider adding a UI fallback similar to the existing "No map data yet" overlay when mapView === "subdivisions" and filteredSubdivisions?.features.length === 0:
{mapView === "subdivisions" &&
filteredSubdivisions?.features.length === 0 && (
<div className="pointer-events-none absolute inset-0 z-10 flex items-center justify-center bg-background/70 text-center text-muted-foreground text-sm">
<p>No subdivision data available for {drillDownCountry?.name}</p>
</div>
)}| mouseover: () => { | ||
| const code = feature.properties?.iso_3166_2; | ||
| setHoveredId(code); |
There was a problem hiding this comment.
setHoveredId receives string | undefined, typed as string | null
feature.properties?.iso_3166_2 evaluates to string | undefined, but setHoveredId is typed as Dispatch<SetStateAction<string | null>>. Passing undefined instead of null can cause subtle equality issues later (e.g., hoveredId === regionCode where regionCode is undefined and hoveredId is null).
| mouseover: () => { | |
| const code = feature.properties?.iso_3166_2; | |
| setHoveredId(code); | |
| const code = feature.properties?.iso_3166_2 ?? null; | |
| setHoveredId(code); |
- Filter region data to only include subdivisions of the current country instead of using global region data for the color scale - Compute tooltip percentages relative to country total, not global total - Fix setHoveredId receiving undefined instead of null - Add empty-state UI when subdivision filter yields zero features Fixes Greptile review feedback on color intensity and percentage accuracy.
|
Thanks for the thorough review @greptile-apps! All issues have been addressed in the latest push:
|
|
Important Review skippedAuto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Repository UI Review profile: ASSERTIVE Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
Excellent work! All issues have been properly addressed: ✅ Color scale is country-scoped — The ✅ Tooltip percentages are country-relative — Both ✅ Fixed the ✅ Empty state UI added — Lines 639-655 display a helpful message when no subdivision boundaries are available for a country. The drill-down feature is now working as expected with accurate data visualization. Nice implementation! 🚀 |
Summary
subdivisions.jsonGeoJSON data anduseSubdivisions()hook already in the codebasemap-regionsquery is used to color subdivision polygonsChanges
apps/dashboard/components/analytics/map-component.tsx:drillDownCountrystate to track which country is being viewed at subdivision levelArrowLeftIconand country flag for navigationHow it works
subdivisions.jsonCloses #312
Test plan