Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
c8fd568
correct naming of regions
elvbom Nov 11, 2025
6d9dda4
add kpis to ranked page
elvbom Nov 11, 2025
ceb4004
normalize region names for map
elvbom Nov 11, 2025
de71a81
clean up use regions
elvbom Nov 11, 2025
652fa51
remove total emissions and paris gap kpis
elvbom Nov 12, 2025
877acbf
correct source strings
elvbom Nov 12, 2025
8b9cb7d
correct kpi strings
elvbom Nov 12, 2025
0c1e873
cleanup of regional ranked list
elvbom Nov 12, 2025
ae155a1
break out duplicated ranked list and adjust map zoom
elvbom Nov 12, 2025
bf9d036
rename muni map colors to map colors
elvbom Nov 12, 2025
338bb07
merge in main
elvbom Nov 12, 2025
c149125
fix translations for regions in map tooltip and legend
elvbom Nov 12, 2025
2485812
add type for ranked entity type
elvbom Nov 12, 2025
f77a0b9
remove companies from type for entity type for now
elvbom Nov 12, 2025
c7eb013
break out region kpi hook
elvbom Nov 12, 2025
94b01c6
minor cleanup
elvbom Nov 12, 2025
89acdbb
break out regional hooks
elvbom Nov 12, 2025
129dcde
break out hooks and utils from ranked regions page
elvbom Nov 12, 2025
f8dfa8c
update api types
elvbom Nov 17, 2025
09f605c
path update in ranked reg page
elvbom Nov 17, 2025
3064689
cleanup in use regions
elvbom Nov 17, 2025
fbafd6c
update api call to regional kpis
elvbom Nov 17, 2025
b2ca70e
cleanup up unused functions
elvbom Nov 17, 2025
f9636ba
combine regional hooks to one file
elvbom Nov 17, 2025
be61e42
minor cleanup
elvbom Nov 17, 2025
23ed6e1
Merge branch 'main' into feat-add-kpis-to-regions
elvbom Nov 17, 2025
344253d
minor cleanup in swe map
elvbom Nov 17, 2025
190f1d4
Merge branch 'main' into feat-add-kpis-to-regions
elvbom Dec 2, 2025
e83b17a
use createStatisticalGradient and default stat gradient colors in map…
elvbom Dec 2, 2025
40696a6
remove unused map constants
elvbom Dec 2, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 19 additions & 13 deletions src/components/maps/MapLegend.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
import { t } from "i18next";
import { MUNICIPALITY_MAP_COLORS } from "./constants";
import { KPIValue } from "@/types/entity-rankings";
import { DEFAULT_STATISTICAL_GRADIENT_COLORS } from "@/utils/ui/colorGradients";
import { KPIValue, MapEntityType } from "@/types/entity-rankings";

export function MapLegend({
entityType,
unit,
leftValue,
rightValue,
selectedKPI,
hasNullValues,
}: {
entityType: MapEntityType;
unit: string;
leftValue: number;
rightValue: number;
Expand All @@ -24,9 +26,7 @@ export function MapLegend({
}}
/>
<span className="text-gray-500 text-xs">
{t(
`municipalities.list.kpis.${selectedKPI.key}.booleanLabels.${label}`,
)}
{t(`${entityType}.list.kpis.${selectedKPI.key}.booleanLabels.${label}`)}
</span>
</div>
);
Expand All @@ -37,8 +37,14 @@ export function MapLegend({
<div className="flex items-center w-full md:w-auto gap-2">
{selectedKPI.isBoolean ? (
<>
{booleanItem(MUNICIPALITY_MAP_COLORS.gradientEnd, "true")}
{booleanItem(MUNICIPALITY_MAP_COLORS.gradientMidLow, "false")}
{booleanItem(
DEFAULT_STATISTICAL_GRADIENT_COLORS.gradientEnd,
"true",
)}
{booleanItem(
DEFAULT_STATISTICAL_GRADIENT_COLORS.gradientMidLow,
"false",
)}
</>
) : (
<>
Expand All @@ -51,11 +57,11 @@ export function MapLegend({
className="absolute inset-0 rounded-full"
style={{
background: `linear-gradient(to right,
${MUNICIPALITY_MAP_COLORS.gradientStart} 0%,
${MUNICIPALITY_MAP_COLORS.gradientMidLow} 33%,
${MUNICIPALITY_MAP_COLORS.gradientMidHigh} 66%,
${MUNICIPALITY_MAP_COLORS.gradientEnd} 100%
)`,
${DEFAULT_STATISTICAL_GRADIENT_COLORS.gradientStart} 0%,
${DEFAULT_STATISTICAL_GRADIENT_COLORS.gradientMidLow} 33%,
${DEFAULT_STATISTICAL_GRADIENT_COLORS.gradientMidHigh} 66%,
${DEFAULT_STATISTICAL_GRADIENT_COLORS.gradientEnd} 100%
)`,
}}
/>
</div>
Expand All @@ -70,7 +76,7 @@ export function MapLegend({
<div className="flex items-center mb-2 md:mb-0 md:ml-6">
<div className="w-3 h-3 rounded-full bg-gray-600 mr-1" />
<span className="text-gray-500 text-xs italic">
{t("municipalities.map.legend.null")}
{t(`${entityType}.map.legend.null`)}
</span>
</div>
)}
Expand Down
8 changes: 5 additions & 3 deletions src/components/maps/MapTooltip.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { t } from "i18next";
import { KPIValue } from "@/types/entity-rankings";
import { KPIValue, MapEntityType } from "@/types/entity-rankings";

export function MapTooltip({
entityType,
name,
value,
rank,
Expand All @@ -10,6 +11,7 @@ export function MapTooltip({
nullValue,
selectedKPI,
}: {
entityType: MapEntityType;
name: string;
value: number | boolean | null | undefined;
rank: number | null;
Expand All @@ -23,10 +25,10 @@ export function MapTooltip({
? typeof value === "boolean"
? value
? t(
`municipalities.list.kpis.${selectedKPI?.key}.booleanLabels.true`,
`${entityType}.list.kpis.${selectedKPI?.key}.booleanLabels.true`,
) || t("yes")
: t(
`municipalities.list.kpis.${selectedKPI?.key}.booleanLabels.false`,
`${entityType}.list.kpis.${selectedKPI?.key}.booleanLabels.false`,
) || t("no")
: `${(value as number).toFixed(1)}${unit}`
: nullValue;
Expand Down
82 changes: 35 additions & 47 deletions src/components/maps/SwedenMap.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,13 @@ import {
Geometry,
GeoJsonProperties,
} from "geojson";
import { MUNICIPALITY_MAP_COLORS } from "./constants";
import { createStatisticalGradient } from "@/utils/ui/colorGradients";
import { calculateGeoBounds } from "./utils/geoBounds";
import { isMobile } from "react-device-detect";
import { t } from "i18next";
import "leaflet/dist/leaflet.css";
import L from "leaflet";
import { KPIValue } from "@/types/entity-rankings";
import { KPIValue, MapEntityType } from "@/types/entity-rankings";

export interface DataKPI {
key: string;
Expand All @@ -38,6 +38,7 @@ export interface DataItem {
}

interface SwedenMapProps {
entityType: MapEntityType;
geoData: FeatureCollection;
data: DataItem[];
selectedKPI: DataKPI;
Expand Down Expand Up @@ -81,14 +82,21 @@ function MapController({
}

function MapOfSweden({
entityType,
geoData,
data,
selectedKPI,
onAreaClick = () => {},
defaultCenter = [63, 17],
defaultZoom,
propertyNameField = "name",
colors = MUNICIPALITY_MAP_COLORS,
colors = {
null: "var(--grey)",
gradientStart: "var(--pink-5)",
gradientMidLow: "var(--pink-4)",
gradientMidHigh: "var(--pink-3)",
gradientEnd: "var(--blue-3)",
},
}: SwedenMapProps) {
const [hoveredArea, setHoveredArea] = React.useState<string | null>(null);
const [hoveredValue, setHoveredValue] = useState<number | boolean | null>(
Expand Down Expand Up @@ -221,50 +229,27 @@ function MapOfSweden({
);

const getColorByValue = (value: number | boolean | null): string => {
if (value === null || value === undefined) {
if (
value === null ||
value === undefined ||
values.length === 0 ||
Number.isNaN(value)
) {
return colors.null;
}

const { gradientStart, gradientMidLow, gradientMidHigh, gradientEnd } =
colors;
const { gradientMidLow, gradientEnd } = colors;

if (typeof value === "boolean") {
return value === true ? gradientEnd : gradientMidLow;
}

const mean = values.reduce((sum, val) => sum + val, 0) / values.length;
const stdDev = Math.sqrt(
values.reduce((sum, val) => sum + Math.pow(val - mean, 2), 0) /
values.length,
return createStatisticalGradient(
values,
value,
selectedKPI.higherIsBetter ?? false,
colors,
);

const zScore = (value - mean) / stdDev;

if (selectedKPI.higherIsBetter) {
if (zScore <= -1) {
const t = Math.max(0, (zScore + 2) / 1);
return `color-mix(in srgb, ${gradientStart} ${(1 - t) * 100}%, ${gradientMidLow} ${t * 100}%)`;
} else if (zScore <= 0) {
const t = Math.max(0, zScore + 1);
return `color-mix(in srgb, ${gradientMidLow} ${(1 - t) * 100}%, ${gradientMidHigh} ${t * 100}%)`;
} else if (zScore <= 1) {
const t = Math.max(0, zScore);
return `color-mix(in srgb, ${gradientMidHigh} ${(1 - t) * 100}%, ${gradientEnd} ${t * 100}%)`;
} else {
return gradientEnd;
}
} else if (zScore >= 1) {
const t = Math.max(0, (2 - zScore) / 1);
return `color-mix(in srgb, ${gradientStart} ${(1 - t) * 100}%, ${gradientMidLow} ${t * 100}%)`;
} else if (zScore >= 0) {
const t = Math.max(0, 1 - zScore);
return `color-mix(in srgb, ${gradientMidLow} ${(1 - t) * 100}%, ${gradientMidHigh} ${t * 100}%)`;
} else if (zScore >= -1) {
const t = Math.max(0, -zScore);
return `color-mix(in srgb, ${gradientMidHigh} ${(1 - t) * 100}%, ${gradientEnd} ${t * 100}%)`;
} else {
return gradientEnd;
}
};

const renderGradientLegend = () => {
Expand All @@ -275,6 +260,7 @@ function MapOfSweden({

return (
<MapLegend
entityType={entityType}
leftValue={leftValue}
rightValue={rightValue}
unit={selectedKPI.unit ?? ""}
Expand Down Expand Up @@ -303,13 +289,10 @@ function MapOfSweden({
) => {
if (feature?.properties?.[propertyNameField]) {
const areaName = feature.properties[propertyNameField];
const { value, rank } = getAreaData(areaName);

(layer as L.Path).on({
mouseover: () => {
setHoveredArea(areaName);
setHoveredValue(value);
setHoveredRank(rank);
},
mouseout: () => {
setHoveredArea(null);
Expand Down Expand Up @@ -346,12 +329,16 @@ function MapOfSweden({
};

useEffect(() => {
if (hoveredArea) {
const { value, rank } = getAreaData(hoveredArea);
setHoveredValue(value);
setHoveredRank(rank);
if (!hoveredArea) {
setHoveredValue(null);
setHoveredRank(null);
return;
}
}, [selectedKPI, hoveredArea, getAreaData]);

const { value, rank } = getAreaData(hoveredArea);
setHoveredValue(value);
setHoveredRank(rank);
}, [hoveredArea, getAreaData]);

return (
<div className="relative flex-1 h-full max-w-screen-lg">
Expand Down Expand Up @@ -382,14 +369,15 @@ function MapOfSweden({

{hoveredArea && (
<MapTooltip
entityType={entityType}
name={hoveredArea}
value={hoveredValue}
rank={hoveredRank}
unit={selectedKPI.unit ?? ""}
total={data.length}
nullValue={
selectedKPI.key && t
? t(`municipalities.list.kpis.${selectedKPI.key}.nullValues`)
? t(`${entityType}.list.kpis.${selectedKPI.key}.nullValues`)
: "No data"
}
selectedKPI={selectedKPI as KPIValue}
Expand Down
7 changes: 0 additions & 7 deletions src/components/maps/constants.ts

This file was deleted.

2 changes: 1 addition & 1 deletion src/components/regions/RegionalInsightsPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import {

interface InsightsPanelProps {
regionData: Region[];
selectedKPI: KPIValue;
selectedKPI: KPIValue<Region>;
}

function RegionalInsightsPanel({
Expand Down
54 changes: 54 additions & 0 deletions src/components/regions/RegionalRankedList.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { useTranslation } from "react-i18next";
import RankedList from "@/components/ranked/RankedList";
import { DataPoint, KPIValue } from "@/types/entity-rankings";
import { Region, RegionListItem } from "@/types/region";

interface RegionalRankedListProps {
regionEntities: RegionListItem[];
selectedKPI: KPIValue<Region>;
}

export function RegionalRankedList({ regionEntities, selectedKPI }: RegionalRankedListProps) {
const { t } = useTranslation();

const asDataPoint = (kpi: unknown): DataPoint<RegionListItem> =>
kpi as DataPoint<RegionListItem>;

return (
<RankedList
data={regionEntities}
selectedDataPoint={asDataPoint({
label: selectedKPI.label,
key: selectedKPI.key as keyof RegionListItem,
unit: selectedKPI.unit,
description: selectedKPI.description,
higherIsBetter: selectedKPI.higherIsBetter,
nullValues: selectedKPI.nullValues,
isBoolean: selectedKPI.isBoolean,
booleanLabels: selectedKPI.booleanLabels,
formatter: (value: unknown) => {
if (value === null || value === undefined) {
return selectedKPI.nullValues
? t(selectedKPI.nullValues)
: t("noData");
}

if (typeof value === "boolean") {
return value
? t(`regions.list.kpis.${selectedKPI.key}.booleanLabels.true`)
: t(`regions.list.kpis.${selectedKPI.key}.booleanLabels.false`);
}

if (typeof value === "number") {
return value.toFixed(1);
}

return String(value);
},
})}
onItemClick={() => {}}
searchKey="displayName"
searchPlaceholder={t("rankedList.search.placeholder")}
/>
);
}
Loading
Loading