Skip to content

feat: breakdown map by provinces/states on click#334

Open
maoshuorz wants to merge 2 commits intodatabuddy-analytics:mainfrom
maoshuorz:feat/map-subdivisions
Open

feat: breakdown map by provinces/states on click#334
maoshuorz wants to merge 2 commits intodatabuddy-analytics:mainfrom
maoshuorz:feat/map-subdivisions

Conversation

@maoshuorz
Copy link

Summary

  • Clicking a country on the choropleth map now drills down to show its states/provinces (subdivisions) with region-level visitor data coloring
  • A back button with country flag appears in the top-right corner to return to the world view
  • Leverages the existing subdivisions.json GeoJSON data and useSubdivisions() hook already in the codebase
  • Region visitor data from the existing map-regions query is used to color subdivision polygons

Changes

  • apps/dashboard/components/analytics/map-component.tsx:
    • Added drillDownCountry state to track which country is being viewed at subdivision level
    • Country click handler now triggers drill-down instead of just zooming
    • Added subdivision GeoJSON rendering with filtered features matching the selected country
    • Added subdivision-specific tooltip, styling, and interaction handlers
    • Added back button with ArrowLeftIcon and country flag for navigation

How it works

  1. User clicks a country on the world map
  2. Map zooms to the country and renders its state/province boundaries from subdivisions.json
  3. Each subdivision is colored by region visitor data (if available)
  4. Hovering shows region name and visitor count
  5. Clicking the back button returns to the world view

Closes #312

Test plan

  • Click a country with visitor data → should drill down showing colored subdivisions
  • Click a country without data → should still show subdivision boundaries
  • Hover over subdivisions → tooltip should show region name and visitor count
  • Click back button → should return to world map view at default zoom
  • Verify dark/light theme colors work correctly in subdivision view

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
@vercel
Copy link

vercel bot commented Mar 6, 2026

@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-apps
Copy link
Contributor

greptile-apps bot commented Mar 6, 2026

Greptile Summary

This 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 useSubdivisions() and regionData infrastructure cleanly.

However, there are two related logic bugs that will produce visually incorrect output in practice:

  • Color scale skewed by global region data — when in subdivision view, colorScale is calibrated against all regions from all countries. If any other country has a region with disproportionately high visitor counts, every subdivision in the drilled-down country will appear near-zero intensity (nearly invisible).
  • Tooltip percentages are globally scoped — the percentage value in each subdivision tooltip is computed relative to total worldwide region visitors, not the current country's total. A US state capturing 80% of US traffic may show as "0.2%" because it represents 0.2% of all global regional traffic.

Additional smaller issues: setHoveredId may receive undefined instead of null, and there is no empty-state UI when the subdivision filter yields zero features for a given country.

Confidence Score: 2/5

  • Not safe to merge as-is — the subdivision color scale and tooltip percentages are both driven by global region data, producing misleading visualizations for every drilled-down country view.
  • The feature works end-to-end (drill-down triggers, subdivisions render, back button works), but two logic bugs mean the core value of the feature — seeing which subdivisions have high/low traffic — is broken. Colors will be nearly uniform and percentages will be meaningless relative to global traffic rather than country traffic. These are not edge cases; they affect every use of the drill-down.
  • apps/dashboard/components/analytics/map-component.tsx — specifically the colorScale useMemo (line 133) and regionData useMemo (line 84).

Important Files Changed

Filename Overview
apps/dashboard/components/analytics/map-component.tsx Adds country drill-down to subdivision view. Two logic bugs: colorScale and tooltip percentages are both computed against all global regions instead of only the drilled-down country's data, making subdivision colours and tooltip percentages misleading. Additionally, setHoveredId receives undefined instead of null, and there is no empty-state UI when the subdivision filter returns zero features.

Sequence Diagram

sequenceDiagram
    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)
Loading

Last reviewed commit: 92c81de

Comment on lines +133 to +166
@@ -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]);
Copy link
Contributor

Choose a reason for hiding this comment

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

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.

Comment on lines +84 to +103
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]);
Copy link
Contributor

Choose a reason for hiding this comment

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

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;

Comment on lines +172 to +187
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]);
Copy link
Contributor

Choose a reason for hiding this comment

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

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>
  )}

Comment on lines +396 to +398
mouseover: () => {
const code = feature.properties?.iso_3166_2;
setHoveredId(code);
Copy link
Contributor

Choose a reason for hiding this comment

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

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).

Suggested change
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.
@maoshuorz
Copy link
Author

Thanks for the thorough review @greptile-apps! All issues have been addressed in the latest push:

  1. Color scale now country-scoped — Added countryRegionData memo that filters region data to only the drilled-down country's subdivisions. The colorScale now uses this filtered data, so intensity is calibrated correctly per-country.

  2. Tooltip percentages now country-relative — Both getSubdivisionFeatureData and handleEachSubdivisionFeature now use the country-scoped data, so percentages reflect the subdivision's share of that country's traffic, not global.

  3. Fixed setHoveredId undefined → null — Now uses code ?? null to prevent passing undefined.

  4. Added empty-state UI — Shows a helpful message when no subdivision boundaries are available for a given country.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 7, 2026

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: d7a9620e-8955-424a-82e1-04185846ec9e

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@greptile-apps
Copy link
Contributor

greptile-apps bot commented Mar 7, 2026

Excellent work! All issues have been properly addressed:

✅ Color scale is country-scoped — The countryRegionData memo (lines 106-157) correctly filters region data to only the drilled-down country, and colorScale uses this filtered dataset, so color intensity is now calibrated per-country rather than globally.

✅ Tooltip percentages are country-relative — Both getSubdivisionFeatureData and handleEachSubdivisionFeature now use countryRegionData, so percentages correctly represent each subdivision's share of that country's traffic.

✅ Fixed the undefined bug — Line 436 now uses code ?? null to prevent passing undefined to setHoveredId.

✅ 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! 🚀

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Breakdown the map by providences/states

1 participant