UNPKG

analytica-frontend-lib

Version:

Repositório público dos componentes utilizados nas plataformas da Analytica Ensino

623 lines (612 loc) 23.7 kB
"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