Skip to content

Feature - Added region comparison tool for village-level analysis#366

Open
DharanSJIT wants to merge 5 commits intocore-stack-org:developmentfrom
DharanSJIT:feature/ai-comparison-insights
Open

Feature - Added region comparison tool for village-level analysis#366
DharanSJIT wants to merge 5 commits intocore-stack-org:developmentfrom
DharanSJIT:feature/ai-comparison-insights

Conversation

@DharanSJIT
Copy link
Copy Markdown

📊 Overview

This PR introduces a Region Comparison Tool that allows users to compare two villages based on key environmental and agricultural metrics such as rainfall, crop health, and water levels.

Features

  • Select region hierarchy:
  • State → District → Block → Villages
  • Compare two villages side-by-side
  • Metrics included:
    • Rainfall (Average Precipitation)
    • Crop Health (Double Cropped Area)
    • Water Levels (Surface Water Availability)
  • Automatic winner detection for each metric
  • Clear comparison table with values and scores
  • Summary section highlighting overall better-performing village

🌐 Localization

  • Supports multiple languages:
    • English (EN)
    • Hindi (HI)
    • Tamil (TA)
    • Marathi (MR)
    • Bengali (BN)
  • Fully integrated with existing i18n setup

🧠 Motivation

Understanding regional differences is crucial for planning, resource allocation, and decision-making in geospatial and agricultural domains. This feature enables users to make data-driven comparisons between villages in an intuitive and interactive way.

🛠️ Implementation Details

  • Built a dynamic comparison interface with dependent dropdowns (State → District → Block → Village)
  • Integrated API/data sources to fetch village-level metrics
  • Implemented comparison logic to evaluate and determine the better-performing village per metric
  • Designed a clean UI for tabular comparison and summary insights
  • Added fallback handling when metric data is unavailable (MWS-derived estimates)

⚠️ Known Limitations

  • Some blocks use MWS-derived estimates when village-level metrics are unavailable
  • In fallback mode, values are approximate and indicative, not exact

📂 Changed Files

  • src/pages/RegionComparison.jsx
  • src/components/utils/compareMetrics.js
  • src/App.jsx
  • src/pages/LE_homepage.jsx
  • src/i18n/locales/{en,hi,ta,mr,bn}.json

📸 Demo

image

✅ How to Test

  1. Navigate to the Region Comparison Tool
  2. Select State, District, and Block
  3. Choose two villages for comparison
  4. Verify comparison table displays correct values and winners
  5. Check summary section for overall result

🔁 Fallback Case Test

  1. Select a block where village-level metrics are missing
  2. Verify that:
    • A notice about missing metrics is displayed
    • MWS-derived estimates are used for comparison

🚀 Impact

  • Enables data-driven decision making at village level
  • Helps identify better-performing regions based on key indicators
  • Improves usability of geospatial data for planners and analysts
  • Adds analytical capability to the platform

📌 Notes

  • Designed to be scalable for adding more metrics in the future
  • Handles missing data gracefully using derived estimates
  • UI is optimized for clarity and readability

Copilot AI review requested due to automatic review settings March 27, 2026 05:25
Copy link
Copy Markdown

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

This PR adds a new Region Comparison Tool page for village-level side-by-side comparison, and introduces a full i18n + language switching setup that localizes location selectors and multiple UI surfaces across the app.

Changes:

  • Added RegionComparison page + routing and metric comparison utilities (including MWS-derived fallback logic).
  • Introduced i18n initialization, language switcher, and locale JSONs (EN/HI/TA/MR/BN).
  • Updated existing pages/components (homepage, nav, sidebars, selectors, footer) to use translated strings and translated location labels.

Reviewed changes

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

Show a summary per file
File Description
src/pages/RegionComparison.jsx New comparison UI + data fetching/fallback logic for village metrics.
src/components/utils/compareMetrics.js Metric-key selection, scoring, and winner logic for comparisons.
src/App.jsx Adds route for /region-comparison.
src/pages/LE_homepage.jsx Localizes homepage content and adds entry point button to the comparison tool.
src/index.js Boots i18n initialization at app startup.
src/i18n/index.js New i18next configuration and resource registration.
src/components/LanguageSwitcher.jsx Adds UI control to switch languages.
src/components/buttons/select_button.jsx Adds translated option labels + placeholder handling for dropdowns.
src/components/mapArea.jsx Passes translation namespaces into SelectButton for locations.
src/components/landscape-explorer/sidebar/RightSidebar.jsx Localizes labels, download actions, and selection placeholders.
src/components/kyl_rightSidebar.jsx Localizes KYL sidebar strings and selection placeholders.
src/components/kyl_leftSidebar.jsx Localizes tab labels, hints, and category labels.
src/components/landing_navbar.jsx Adds LanguageSwitcher + localizes nav labels/tooltips.
src/components/footer.jsx Localizes footer copyright.
src/i18n/locales/{en,hi,ta,mr,bn}.json Adds translation resources for new/existing UI strings + location names.
src/i18n/useTranslatedLocations.js Adds a helper hook for translating location structures.
package.json Adds i18n dependencies.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +319 to +343
const villageHasMetrics = hasComparisonSignals(resolvedVillages);

const mwsUrls = buildMwsUrls(
selectedState.label,
selectedDistrict.label,
selectedBlock.label
);

let derivedVillages = null;

for (const url of mwsUrls) {
const response = await fetch(url);
if (!response.ok) continue;

const payload = await response.json();
const parsed = parseVillagePayload(payload);
if (!parsed || !parsed.length) continue;

const transformed = deriveVillageRecordsFromMws(parsed);
if (transformed.length) {
derivedVillages = transformed;
break;
}
}

Copy link

Copilot AI Mar 27, 2026

Choose a reason for hiding this comment

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

Even when village-level data is found and contains all required metrics, the code still performs the full MWS fallback fetch loop (up to 4 more network calls). This increases latency and load unnecessarily. Consider fetching MWS data only when !resolvedVillages or !villageHasMetrics (or running attempts in parallel).

Copilot uses AI. Check for mistakes.
Comment on lines +68 to 75
// currVal is a real selection only when it has a `value` field (API data)
// placeholder objects passed from parent only have `label`
const selectedValue =
currVal && typeof currVal === "object" && !Array.isArray(currVal)
? currVal
: null;
const placeholderText = placeholder || label || currVal?.label || "Select...";

Copy link

Copilot AI Mar 27, 2026

Choose a reason for hiding this comment

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

The selectedValue logic doesn’t match the comment: it treats any object currVal as a selected option, including placeholder objects like { label: "Select State" }. This prevents placeholder from showing and can set a value that doesn’t exist in options. Consider resolving selectedValue by finding the matching option via getOptionValue, or treat label-only placeholder objects as null.

Suggested change
// currVal is a real selection only when it has a `value` field (API data)
// placeholder objects passed from parent only have `label`
const selectedValue =
currVal && typeof currVal === "object" && !Array.isArray(currVal)
? currVal
: null;
const placeholderText = placeholder || label || currVal?.label || "Select...";
// currVal is a real selection only when it has an identifier field (API data)
// placeholder objects passed from parent only have `label`
const hasIdentifier = (option) =>
option &&
typeof option === "object" &&
!Array.isArray(option) &&
(option.value != null ||
option.block_id != null ||
option.tehsil_id != null ||
option.district_id != null ||
option.state_id != null ||
option.id != null);
const selectedValue = hasIdentifier(currVal)
? sortedOptions.find(
(opt) => getOptionValue(opt) === getOptionValue(currVal)
) ?? null
: null;
const placeholderText = placeholder || label || "Select...";

Copilot uses AI. Check for mistakes.
Comment thread src/pages/LE_homepage.jsx
Comment on lines +211 to +214
handleNavigate("/region-comparison", "Region Comparison")
}
>
Region Comparison
Copy link

Copilot AI Mar 27, 2026

Choose a reason for hiding this comment

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

This button label is still hard-coded in English, so it won’t change with the selected language. Use an i18n key (and add it to the locale files) for both the button text and the analytics label if needed.

Suggested change
handleNavigate("/region-comparison", "Region Comparison")
}
>
Region Comparison
handleNavigate(
"/region-comparison",
t("home.know.regionComparison")
)
}
>
{t("home.know.regionComparison")}

Copilot uses AI. Check for mistakes.
Comment on lines +1 to +44
import { useTranslation } from "react-i18next";
import { useMemo } from "react";

// i18next uses '.' as separator, so keys with spaces like "Andhra Pradesh"
// must be looked up with the separator disabled or via direct object access.
// We use the defaultValue fallback: t(key, { defaultValue: original })
// and disable key separator for location lookups by using the raw resource.

function translateLabel(i18n, namespace, label) {
const lang = i18n.language;
const resources = i18n.getResourceBundle(lang, "translation");
if (resources && resources[namespace] && resources[namespace][label]) {
return resources[namespace][label];
}
// fallback to English
const enResources = i18n.getResourceBundle("en", "translation");
if (enResources && enResources[namespace] && enResources[namespace][label]) {
return enResources[namespace][label];
}
return label;
}

export function useTranslatedLocations(statesData) {
const { i18n } = useTranslation();
const language = i18n.language;

return useMemo(() => {
if (!statesData) return statesData;

return statesData.map((state) => ({
...state,
label: translateLabel(i18n, "states", state.label),
district: state.district?.map((d) => ({
...d,
label: translateLabel(i18n, "districts", d.label),
blocks: d.blocks?.map((b) => ({
...b,
label: translateLabel(i18n, "blocks", b.label),
})),
})),
}));
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [statesData, language]);
}
Copy link

Copilot AI Mar 27, 2026

Choose a reason for hiding this comment

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

useTranslatedLocations is added but not referenced anywhere in the repo. If it’s not intended for near-term use, consider removing it to avoid dead code; otherwise, wire it into the state/district/block selection flows to centralize translation logic.

Suggested change
import { useTranslation } from "react-i18next";
import { useMemo } from "react";
// i18next uses '.' as separator, so keys with spaces like "Andhra Pradesh"
// must be looked up with the separator disabled or via direct object access.
// We use the defaultValue fallback: t(key, { defaultValue: original })
// and disable key separator for location lookups by using the raw resource.
function translateLabel(i18n, namespace, label) {
const lang = i18n.language;
const resources = i18n.getResourceBundle(lang, "translation");
if (resources && resources[namespace] && resources[namespace][label]) {
return resources[namespace][label];
}
// fallback to English
const enResources = i18n.getResourceBundle("en", "translation");
if (enResources && enResources[namespace] && enResources[namespace][label]) {
return enResources[namespace][label];
}
return label;
}
export function useTranslatedLocations(statesData) {
const { i18n } = useTranslation();
const language = i18n.language;
return useMemo(() => {
if (!statesData) return statesData;
return statesData.map((state) => ({
...state,
label: translateLabel(i18n, "states", state.label),
district: state.district?.map((d) => ({
...d,
label: translateLabel(i18n, "districts", d.label),
blocks: d.blocks?.map((b) => ({
...b,
label: translateLabel(i18n, "blocks", b.label),
})),
})),
}));
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [statesData, language]);
}
// This file previously exported `useTranslatedLocations`, which was not referenced
// anywhere in the repository and was therefore removed to avoid dead code.
// Reintroduce the hook here only when it is actually wired into the
// state/district/block selection flows.

Copilot uses AI. Check for mistakes.
Comment thread src/i18n/index.js
ta: { translation: ta },
bn: { translation: bn },
mr: { translation: mr },
},
Copy link

Copilot AI Mar 27, 2026

Choose a reason for hiding this comment

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

With LanguageDetector enabled, i18n.language may be en-US/hi-IN, but only base resources (en, hi, ...) are registered. Consider adding supportedLngs and nonExplicitSupportedLngs: true (or load: "languageOnly") so detected regional variants resolve to the bundled resources. This also avoids breaking any manual getResourceBundle(i18n.language, ...) lookups.

Suggested change
},
},
supportedLngs: ["en", "hi", "ta", "bn", "mr"],
nonExplicitSupportedLngs: true,

Copilot uses AI. Check for mistakes.
Comment on lines +22 to +40
return (option) => {
const rawLabel = String(option?.label ?? "");
const resources = i18n.getResourceBundle(language, "translation");
if (
resources &&
resources[translateNamespace] &&
resources[translateNamespace][rawLabel]
) {
return resources[translateNamespace][rawLabel];
}

const enResources = i18n.getResourceBundle("en", "translation");
if (
enResources &&
enResources[translateNamespace] &&
enResources[translateNamespace][rawLabel]
) {
return enResources[translateNamespace][rawLabel];
}
Copy link

Copilot AI Mar 27, 2026

Choose a reason for hiding this comment

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

getResourceBundle(language, ...) uses i18n.language, which may be a regional tag (e.g. en-US, hi-IN) when LanguageDetector is enabled. In that case resources will be undefined and location labels will incorrectly fall back to English. Use i18n.resolvedLanguage (or iterate i18n.languages) when reading bundles, or normalize via i18n init options.

Copilot uses AI. Check for mistakes.
Comment on lines 1397 to +1420
@@ -1407,6 +1408,7 @@ const MapArea = ({ selectedTehsil }) => {
district === null ? { label: "Select District" } : district
}
stateData={state !== null ? state.district : null}
translateNamespace="districts"
handleItemSelect={handleItemSelect}
setState={setDistrict}
/>
@@ -1415,6 +1417,7 @@ const MapArea = ({ selectedTehsil }) => {
<SelectButton
currVal={block === null ? { label: "Select Block" } : block}
stateData={district !== null ? district.blocks : null}
translateNamespace="blocks"
Copy link

Copilot AI Mar 27, 2026

Choose a reason for hiding this comment

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

These currVal props pass placeholder objects (e.g. { label: "Select State" }). With the updated SelectButton behavior (custom placeholder + translation), this will be treated as an actual selected value and can mask the real placeholder/disable proper clearing. Prefer passing currVal={state} (nullable) and a localized placeholder, as done in other updated call sites.

Copilot uses AI. Check for mistakes.
Comment on lines +20 to +25
onClick={() => i18n.changeLanguage(lang.code)}
className={`px-3 py-2 text-sm font-medium transition-colors duration-150 ${
i18n.language === lang.code
? "bg-purple-600 text-white"
: "text-purple-700 hover:bg-purple-50"
}`}
Copy link

Copilot AI Mar 27, 2026

Choose a reason for hiding this comment

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

LanguageDetector can set i18n.language to regional tags like en-US. Comparing strictly to lang.code will fail to highlight the active language button. Consider comparing against i18n.resolvedLanguage or i18n.language.split('-')[0].

Copilot uses AI. Check for mistakes.
Comment thread src/pages/LE_homepage.jsx
onClick={() => {
if (item.link) {
handleNavigate(item.link, item.title);
handleNavigate(item.link, t(item.titleKey));
Copy link

Copilot AI Mar 27, 2026

Choose a reason for hiding this comment

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

handleNavigate uses React Router navigation for all links, but trackCards includes an absolute external URL (https://welllabs.org/jaltol/). Navigating to external URLs via navigate() is unreliable and can produce an in-app path like /https://.... For external links, use window.open / window.location.href or render an <a> element.

Suggested change
handleNavigate(item.link, t(item.titleKey));
if (/^https?:\/\//i.test(item.link)) {
window.open(item.link, "_blank", "noopener,noreferrer");
} else {
handleNavigate(item.link, t(item.titleKey));
}

Copilot uses AI. Check for mistakes.
Comment on lines +286 to +296
useEffect(() => {
if (!selectedState || !selectedDistrict || !selectedBlock) {
setVillages([]);
return;
}

const fetchVillageData = async () => {
setLoadingVillages(true);
setFetchError("");
setFetchNotice("");

Copy link

Copilot AI Mar 27, 2026

Choose a reason for hiding this comment

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

The village-fetch effect can race when users change selections quickly: an earlier request can resolve after a later one and overwrite villages/notices for the wrong block, and can also set state after unmount. Add cancellation (AbortController) and/or a request "token" check before calling setVillages / setFetchNotice / setFetchError.

Copilot uses AI. Check for mistakes.
@Ksheetiz-24
Copy link
Copy Markdown
Member

Already merged this Feature in a Recent Commit so closing this PR.

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.

3 participants