analytica-frontend-lib
Version:
Repositório público dos componentes utilizados nas plataformas da Analytica Ensino
623 lines (612 loc) • 23.7 kB
JavaScript
"use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } function _nullishCoalesce(lhs, rhsFn) { if (lhs != null) { return lhs; } else { return rhsFn(); } } function _optionalChain(ops) { let lastAccessLHS = undefined; let value = ops[0]; let i = 1; while (i < ops.length) { const op = ops[i]; const fn = ops[i + 1]; i += 2; if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) { return undefined; } if (op === 'access' || op === 'optionalAccess') { lastAccessLHS = value; value = fn(value); } else if (op === 'call' || op === 'optionalCall') { value = fn((...args) => value.call(lastAccessLHS, ...args)); lastAccessLHS = undefined; } } return value; }
var _chunkCFD6NHL7js = require('./chunk-CFD6NHL7.js');
var _chunkORRSVKFKjs = require('./chunk-ORRSVKFK.js');
var _chunkANT66KVKjs = require('./chunk-ANT66KVK.js');
var _chunkTN3AYOMVjs = require('./chunk-TN3AYOMV.js');
// src/components/ChoroplethMap/ChoroplethMap.tsx
var _react = require('react');
var _api = require('@react-google-maps/api');
var _union = require('@turf/union'); var _union2 = _interopRequireDefault(_union);
var _jsxruntime = require('react/jsx-runtime');
var GOOGLE_MAPS_LOADER_ID = "google-maps-script";
var TOOLTIP_PROFILE_LINES = [
{ key: "students", label: "Estudantes" },
{ key: "teachers", label: "Professores(as)" },
{ key: "managers", label: "Diretores(as)" }
];
var FADE_DURATION = 400;
var HOVER_DURATION = 200;
var TARGET_OPACITY = 0.8;
var getCssVar = (name, fallback = "") => {
if (typeof document === "undefined") return fallback;
return getComputedStyle(document.documentElement).getPropertyValue(name).trim() || fallback;
};
var getColorClasses = () => [
{
min: 0.75,
max: 1.01,
fillColor: getCssVar("--color-map-highlight", "#66b584"),
strokeColor: getCssVar("--color-map-highlight", "#66b584"),
label: "Destaque (75% com acesso)"
},
{
min: 0.5,
max: 0.75,
fillColor: getCssVar("--color-map-above-avg", "#f8cc2e"),
strokeColor: getCssVar("--color-map-above-avg", "#f8cc2e"),
label: "Acima da m\xE9dia (50 at\xE9 74% com acesso)"
},
{
min: 0.25,
max: 0.5,
fillColor: getCssVar("--color-map-below-avg", "#fb954b"),
strokeColor: getCssVar("--color-map-below-avg", "#fb954b"),
label: "Abaixo da m\xE9dia (25 at\xE9 49% com acesso)"
},
{
min: 0.01,
max: 0.25,
fillColor: getCssVar("--color-map-attention", "#b91c1c"),
strokeColor: getCssVar("--color-map-attention", "#b91c1c"),
label: "Ponto de aten\xE7\xE3o (Abaixo de 25% com acesso)"
},
{
min: -0.01,
max: 0.01,
fillColor: getCssVar("--color-map-no-access", "#2f2f2f"),
strokeColor: getCssVar("--color-map-no-access", "#2f2f2f"),
label: "Sem acesso (0%)"
}
];
var getColorClass = (value, colorClasses) => {
for (const colorClass of colorClasses) {
if (value >= colorClass.min && value < colorClass.max) {
return colorClass;
}
}
return colorClasses[0];
};
var computeNREBoundaries = (data) => {
const groups = /* @__PURE__ */ new Map();
data.forEach((region) => {
const groupKey = _nullishCoalesce(region.groupName, () => ( region.name));
const existing = _nullishCoalesce(groups.get(groupKey), () => ( []));
existing.push(region);
groups.set(groupKey, existing);
});
const boundaries = [];
groups.forEach((regions) => {
let merged = null;
for (const region of regions) {
if (region.geoJson.type !== "Feature") continue;
const feature = region.geoJson;
if (merged) {
const result = _union2.default.call(void 0, {
type: "FeatureCollection",
features: [merged, feature]
});
if (result) merged = result;
} else {
merged = feature;
}
}
if (merged) boundaries.push(merged);
});
return boundaries;
};
var createStyleFunction = (opacity, colorClasses, strokeCityColor, unmanagedFillColor) => {
return (feature) => {
if (feature.getProperty("regionIsManaged") === false) {
return {
fillColor: unmanagedFillColor,
fillOpacity: opacity,
strokeColor: strokeCityColor,
strokeWeight: 0.3,
cursor: "default"
};
}
const value = feature.getProperty("regionValue");
const colorClass = getColorClass(_nullishCoalesce(value, () => ( 0)), colorClasses);
return {
fillColor: colorClass.fillColor,
fillOpacity: opacity,
strokeColor: strokeCityColor,
strokeWeight: 0.3,
cursor: "pointer"
};
};
};
var containerStyle = {
width: "100%",
height: "415px",
borderRadius: "0.5rem"
};
var defaultCenter = {
lat: -24.7,
lng: -51.5
};
var LegendItem = ({
color,
label,
borderColor,
active = true,
onClick
}) => /* @__PURE__ */ _jsxruntime.jsxs.call(void 0,
_chunkANT66KVKjs.Text_default,
{
as: "button",
type: "button",
"aria-pressed": active,
className: "flex items-center gap-2 cursor-pointer transition-opacity duration-200",
style: { opacity: active ? 1 : 0.4 },
onClick,
children: [
/* @__PURE__ */ _jsxruntime.jsx.call(void 0,
"div",
{
className: "w-3 h-3 rounded-full",
style: {
backgroundColor: color,
border: borderColor ? `1px solid ${borderColor}` : "none"
}
}
),
/* @__PURE__ */ _jsxruntime.jsx.call(void 0, _chunkANT66KVKjs.Text_default, { as: "span", size: "sm", weight: "medium", color: "text-text-600", children: label })
]
}
);
var LoadingSkeleton = () => /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "div", { className: "w-full h-full flex items-center justify-center bg-background-50 rounded-lg animate-pulse", children: /* @__PURE__ */ _jsxruntime.jsx.call(void 0, _chunkANT66KVKjs.Text_default, { size: "sm", color: "text-text-400", children: "Carregando mapa..." }) });
var ChoroplethMap = ({
data,
apiKey,
title = "Performance por regi\xE3o",
countLabel = "Acessos",
loading = false,
bounds,
onRegionClick,
headerAction,
infoText,
activeProfile,
className
}) => {
const mapId = GOOGLE_MAPS_LOADER_ID;
const [map, setMap] = _react.useState.call(void 0, null);
const [hoveredRegion, setHoveredRegion] = _react.useState.call(void 0, null);
const [infoPosition, setInfoPosition] = _react.useState.call(void 0, null);
const [activeClasses, setActiveClasses] = _react.useState.call(void 0,
/* @__PURE__ */ new Set([0, 1, 2, 3, 4])
);
const fadeAnimationRef = _react.useRef.call(void 0, null);
const hoverAnimationsRef = _react.useRef.call(void 0, /* @__PURE__ */ new Map());
const nreBoundaryLayerRef = _react.useRef.call(void 0, null);
const onRegionClickRef = _react.useRef.call(void 0, onRegionClick);
onRegionClickRef.current = onRegionClick;
const { isDark } = _chunkORRSVKFKjs.useTheme.call(void 0, );
const dataSignature = _react.useMemo.call(void 0,
() => data.map((d) => {
const b = d.accessBreakdown;
const breakdown = b ? `${b.students.withAccess}/${b.students.withoutAccess}/${b.teachers.withAccess}/${b.teachers.withoutAccess}/${b.managers.withAccess}/${b.managers.withoutAccess}` : "";
return `${d.id}:${d.value}:${d.name}:${_nullishCoalesce(d.groupName, () => ( ""))}:${d.accessCount}:${d.isManagedRegion === false ? 0 : 1}:${breakdown}`;
}).join("|"),
[data]
);
const stableData = _react.useMemo.call(void 0, () => data, [dataSignature]);
const colorClasses = _react.useMemo.call(void 0, () => getColorClasses(), [isDark]);
const mapOptions = _react.useMemo.call(void 0, () => {
const bgColor = getCssVar("--color-background-50", "#F6F6F6");
return {
disableDefaultUI: true,
zoomControl: true,
scrollwheel: true,
draggable: true,
backgroundColor: bgColor,
styles: [
{
stylers: [{ color: bgColor }]
},
{
elementType: "labels",
stylers: [{ visibility: "off" }]
},
{
elementType: "geometry.stroke",
stylers: [{ visibility: "off" }]
}
]
};
}, [isDark]);
const { isLoaded, loadError } = _api.useJsApiLoader.call(void 0, {
id: mapId,
googleMapsApiKey: apiKey
});
const onLoad = _react.useCallback.call(void 0,
(mapInstance) => {
setMap(mapInstance);
if (bounds) {
const googleBounds = new google.maps.LatLngBounds(
{ lat: bounds.south, lng: bounds.west },
{ lat: bounds.north, lng: bounds.east }
);
mapInstance.fitBounds(googleBounds, 20);
google.maps.event.addListenerOnce(mapInstance, "idle", () => {
const view = mapInstance.getBounds();
const zoom = mapInstance.getZoom();
if (!view || zoom == null) return;
const viewSpanLat = view.getNorthEast().lat() - view.getSouthWest().lat();
const viewSpanLng = view.getNorthEast().lng() - view.getSouthWest().lng();
const targetSpanLat = bounds.north - bounds.south;
const targetSpanLng = bounds.east - bounds.west;
if (targetSpanLat <= 0 || targetSpanLng <= 0) return;
const extraZoom = Math.min(
Math.log2(viewSpanLat / targetSpanLat),
Math.log2(viewSpanLng / targetSpanLng)
) - 0.05;
if (extraZoom > 0) {
mapInstance.setZoom(zoom + Math.min(extraZoom, 1));
}
});
}
},
[bounds]
);
const onUnmount = _react.useCallback.call(void 0, () => {
if (nreBoundaryLayerRef.current) {
nreBoundaryLayerRef.current.setMap(null);
}
setMap(null);
}, []);
const nreBoundaries = _react.useMemo.call(void 0,
() => computeNREBoundaries(stableData),
[stableData]
);
_react.useEffect.call(void 0, () => {
if (!map || !stableData.length) return;
const strokeCityColor = getCssVar("--color-map-stroke-city", "#ffffff");
const unmanagedFillColor = getCssVar(
"--color-map-unmanaged-region",
"#e0e0e0"
);
const strokeNreColor = getCssVar("--color-map-stroke-nre", "#ffffff");
map.data.forEach((feature) => {
map.data.remove(feature);
});
if (nreBoundaryLayerRef.current) {
nreBoundaryLayerRef.current.setMap(null);
nreBoundaryLayerRef.current = null;
}
stableData.forEach((region) => {
try {
const feature = map.data.addGeoJson(region.geoJson);
if (feature && feature.length > 0) {
feature.forEach((f) => {
f.setProperty("regionId", region.id);
f.setProperty("regionName", region.name);
f.setProperty("regionValue", region.value);
f.setProperty("regionAccessCount", region.accessCount);
f.setProperty("regionIsManaged", region.isManagedRegion !== false);
});
}
} catch (error) {
console.error(
`Failed to add GeoJSON for region ${region.name}:`,
error
);
}
});
const cityFeatureIndex = /* @__PURE__ */ new Map();
map.data.forEach((f) => {
const id = f.getProperty("regionId");
if (id) {
const list = _nullishCoalesce(cityFeatureIndex.get(id), () => ( []));
list.push(f);
cityFeatureIndex.set(id, list);
}
});
map.data.setStyle(
createStyleFunction(0, colorClasses, strokeCityColor, unmanagedFillColor)
);
const nreLayer = new google.maps.Data();
nreBoundaries.forEach((boundary) => {
nreLayer.addGeoJson(boundary);
});
nreLayer.setStyle({
fillOpacity: 0,
strokeColor: strokeNreColor,
strokeWeight: 1,
clickable: false
});
nreLayer.setMap(map);
nreBoundaryLayerRef.current = nreLayer;
const startTime = performance.now();
const animateFadeIn = (currentTime) => {
const elapsed = currentTime - startTime;
const progress = Math.min(elapsed / FADE_DURATION, 1);
const currentOpacity = progress * TARGET_OPACITY;
map.data.setStyle(
createStyleFunction(
currentOpacity,
colorClasses,
strokeCityColor,
unmanagedFillColor
)
);
if (progress < 1) {
fadeAnimationRef.current = requestAnimationFrame(animateFadeIn);
} else {
fadeAnimationRef.current = null;
}
};
fadeAnimationRef.current = requestAnimationFrame(animateFadeIn);
const applyHoverStyle = (features, opacity, weight) => {
features.forEach((f) => {
map.data.overrideStyle(f, {
fillOpacity: opacity,
strokeColor: strokeCityColor,
strokeWeight: weight
});
});
};
const animateHover = (groupKey, features, from, to, fromWeight, toWeight) => {
const running = hoverAnimationsRef.current.get(groupKey);
if (running) {
cancelAnimationFrame(running);
}
const start = performance.now();
const animate = (now) => {
const progress = Math.min((now - start) / HOVER_DURATION, 1);
const opacity = from + (to - from) * progress;
const weight = fromWeight + (toWeight - fromWeight) * progress;
applyHoverStyle(features, opacity, weight);
if (progress < 1) {
hoverAnimationsRef.current.set(
groupKey,
requestAnimationFrame(animate)
);
} else {
hoverAnimationsRef.current.delete(groupKey);
}
};
hoverAnimationsRef.current.set(groupKey, requestAnimationFrame(animate));
};
let currentCityId = null;
let revertTimeout = null;
const collectCityFeatures = (cityId) => {
return _nullishCoalesce(cityFeatureIndex.get(cityId), () => ( []));
};
const mouseoverListener = map.data.addListener(
"mouseover",
(event) => {
if (revertTimeout) {
clearTimeout(revertTimeout);
revertTimeout = null;
}
const regionId = event.feature.getProperty("regionId");
const region = stableData.find((r) => r.id === regionId);
if (region) {
setHoveredRegion(region);
if (event.domEvent) {
setInfoPosition({
x: event.domEvent.clientX,
y: event.domEvent.clientY
});
}
}
if (currentCityId !== regionId) {
if (currentCityId) {
const prevFeatures = collectCityFeatures(currentCityId);
animateHover(
currentCityId,
prevFeatures,
1,
TARGET_OPACITY,
1,
0.5
);
}
currentCityId = regionId;
const cityFeatures = collectCityFeatures(regionId);
animateHover(regionId, cityFeatures, TARGET_OPACITY, 1, 0.5, 1);
}
}
);
const mouseoutListener = map.data.addListener("mouseout", () => {
revertTimeout = setTimeout(() => {
setHoveredRegion(null);
setInfoPosition(null);
if (currentCityId) {
const prevFeatures = collectCityFeatures(currentCityId);
animateHover(currentCityId, prevFeatures, 1, TARGET_OPACITY, 1, 0.5);
currentCityId = null;
}
}, 50);
});
const mousemoveListener = map.data.addListener(
"mousemove",
(event) => {
if (event.domEvent) {
setInfoPosition({
x: event.domEvent.clientX,
y: event.domEvent.clientY
});
}
}
);
const computeCityBounds = (cityId) => {
const cityBounds = new google.maps.LatLngBounds();
for (const f of collectCityFeatures(cityId)) {
_optionalChain([f, 'access', _ => _.getGeometry, 'call', _2 => _2(), 'optionalAccess', _3 => _3.forEachLatLng, 'call', _4 => _4((latLng) => {
cityBounds.extend(latLng);
})]);
}
return cityBounds;
};
const clickListener = map.data.addListener(
"click",
(event) => {
const regionId = event.feature.getProperty("regionId");
const region = stableData.find((r) => r.id === regionId);
if (region && region.isManagedRegion !== false) {
_optionalChain([onRegionClickRef, 'access', _5 => _5.current, 'optionalCall', _6 => _6(region)]);
}
const cityBounds = computeCityBounds(regionId);
if (!cityBounds.isEmpty()) {
map.fitBounds(cityBounds, 20);
}
}
);
return () => {
if (fadeAnimationRef.current)
cancelAnimationFrame(fadeAnimationRef.current);
hoverAnimationsRef.current.forEach((id) => cancelAnimationFrame(id));
hoverAnimationsRef.current.clear();
if (revertTimeout) clearTimeout(revertTimeout);
if (nreBoundaryLayerRef.current) {
nreBoundaryLayerRef.current.setMap(null);
nreBoundaryLayerRef.current = null;
}
google.maps.event.removeListener(mouseoverListener);
google.maps.event.removeListener(mouseoutListener);
google.maps.event.removeListener(mousemoveListener);
google.maps.event.removeListener(clickListener);
};
}, [map, stableData, colorClasses, nreBoundaries]);
_react.useEffect.call(void 0, () => {
if (!map || !stableData.length) return;
const isFiltering = activeClasses.size < colorClasses.length;
const visibleBounds = new google.maps.LatLngBounds();
let hasVisibleFeatures = false;
map.data.forEach((feature) => {
if (feature.getProperty("regionIsManaged") === false) {
map.data.overrideStyle(feature, { visible: true });
return;
}
const value = feature.getProperty("regionValue");
const colorClass = getColorClass(_nullishCoalesce(value, () => ( 0)), colorClasses);
const classIndex = colorClasses.indexOf(colorClass);
if (activeClasses.has(classIndex)) {
map.data.overrideStyle(feature, { visible: true });
_optionalChain([feature, 'access', _7 => _7.getGeometry, 'call', _8 => _8(), 'optionalAccess', _9 => _9.forEachLatLng, 'call', _10 => _10((latLng) => {
visibleBounds.extend(latLng);
})]);
hasVisibleFeatures = true;
} else {
map.data.overrideStyle(feature, { visible: false });
}
});
if (isFiltering && hasVisibleFeatures && !visibleBounds.isEmpty()) {
map.fitBounds(visibleBounds, 20);
}
}, [map, stableData, activeClasses, colorClasses]);
const toggleClass = (index) => {
setActiveClasses((prev) => {
const next = new Set(prev);
if (next.has(index)) {
next.delete(index);
} else {
next.add(index);
}
return next;
});
};
if (loadError) {
return /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "div", { className: "p-5 bg-background border border-border-50 rounded-xl", children: /* @__PURE__ */ _jsxruntime.jsx.call(void 0, _chunkANT66KVKjs.Text_default, { color: "text-error-700", children: "Erro ao carregar o mapa" }) });
}
return /* @__PURE__ */ _jsxruntime.jsxs.call(void 0,
"div",
{
className: _chunkTN3AYOMVjs.cn.call(void 0,
"p-5 bg-background border border-border-50 rounded-xl flex flex-col gap-4",
className
),
children: [
/* @__PURE__ */ _jsxruntime.jsxs.call(void 0, "div", { className: "flex flex-col gap-4", children: [
/* @__PURE__ */ _jsxruntime.jsxs.call(void 0, "div", { className: "flex flex-row items-center gap-4", children: [
/* @__PURE__ */ _jsxruntime.jsx.call(void 0,
_chunkANT66KVKjs.Text_default,
{
as: "h2",
size: "lg",
weight: "bold",
className: "flex-1 leading-[21px] tracking-[0.2px]",
children: title
}
),
headerAction
] }),
/* @__PURE__ */ _jsxruntime.jsx.call(void 0, "div", { className: "flex flex-wrap gap-8", children: colorClasses.map((colorClass, index) => /* @__PURE__ */ _jsxruntime.jsx.call(void 0,
LegendItem,
{
color: colorClass.fillColor,
borderColor: colorClass.fillColor === colorClass.strokeColor ? void 0 : colorClass.strokeColor,
label: colorClass.label,
active: activeClasses.has(index),
onClick: () => toggleClass(index)
},
colorClass.label
)) })
] }),
/* @__PURE__ */ _jsxruntime.jsxs.call(void 0, "div", { className: "bg-background-50 rounded-lg h-[415px] relative overflow-hidden", children: [
loading || !isLoaded ? /* @__PURE__ */ _jsxruntime.jsx.call(void 0, LoadingSkeleton, {}) : /* @__PURE__ */ _jsxruntime.jsx.call(void 0,
_api.GoogleMap,
{
mapContainerStyle: containerStyle,
center: defaultCenter,
zoom: 7,
onLoad,
onUnmount,
options: mapOptions
}
),
hoveredRegion && infoPosition && /* @__PURE__ */ _jsxruntime.jsxs.call(void 0,
"div",
{
className: "fixed z-50 flex flex-col gap-2 bg-background-900 shadow-hard-shadow-2 rounded px-3 py-1 pointer-events-none",
style: {
left: Math.min(infoPosition.x + 10, window.innerWidth - 460),
top: Math.min(infoPosition.y + 10, window.innerHeight - 160)
},
children: [
/* @__PURE__ */ _jsxruntime.jsx.call(void 0, _chunkANT66KVKjs.Text_default, { size: "md", weight: "semibold", color: "text-text-50", children: hoveredRegion.name }),
hoveredRegion.isManagedRegion !== false && /* @__PURE__ */ _jsxruntime.jsxs.call(void 0, _jsxruntime.Fragment, { children: [
/* @__PURE__ */ _jsxruntime.jsx.call(void 0, "div", { className: "h-px self-stretch bg-border-200" }),
hoveredRegion.accessBreakdown ? /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "div", { className: "flex flex-col gap-1", children: TOOLTIP_PROFILE_LINES.filter(
(line) => !activeProfile || line.key === activeProfile
).map((line) => {
const entry = hoveredRegion.accessBreakdown[line.key];
return /* @__PURE__ */ _jsxruntime.jsxs.call(void 0, _chunkANT66KVKjs.Text_default, { size: "md", color: "text-text-50", children: [
line.label,
":",
" ",
entry.withAccess.toLocaleString("pt-BR"),
" com acesso,",
" ",
entry.withoutAccess.toLocaleString("pt-BR"),
" sem acessos"
] }, line.key);
}) }) : /* @__PURE__ */ _jsxruntime.jsxs.call(void 0, _chunkANT66KVKjs.Text_default, { size: "md", color: "text-text-50", children: [
countLabel,
":",
" ",
hoveredRegion.accessCount.toLocaleString("pt-BR")
] })
] })
]
}
)
] }),
infoText && /* @__PURE__ */ _jsxruntime.jsx.call(void 0,
_chunkCFD6NHL7js.Alert_default,
{
action: "info",
variant: "solid",
description: infoText,
className: "w-full"
}
)
]
}
);
};
var ChoroplethMap_default = ChoroplethMap;
exports.ChoroplethMap_default = ChoroplethMap_default;
//# sourceMappingURL=chunk-KRIEI3QT.js.map