@principal-ai/file-city-react 0.4.1 → 0.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- {"version":3,"file":"ArchitectureMapHighlightLayers.d.ts","sourceRoot":"","sources":["../../src/components/ArchitectureMapHighlightLayers.tsx"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"ArchitectureMapHighlightLayers.d.ts","sourceRoot":"","sources":["../../src/components/ArchitectureMapHighlightLayers.tsx"],"names":[],"mappings":"AAAA,OAAO,KAA4D,MAAM,OAAO,CAAC;AAQjF,OAAO,EAIL,cAAc,EAEf,MAAM,uCAAuC,CAAC;AAC/C,OAAO,EACL,QAAQ,EACR,YAAY,EACZ,YAAY,EACZ,sBAAsB,EACvB,MAAM,iCAAiC,CAAC;AAWzC,MAAM,WAAW,mCAAmC;IAElD,QAAQ,CAAC,EAAE,QAAQ,CAAC;IAGpB,eAAe,CAAC,EAAE,cAAc,EAAE,CAAC;IACnC,aAAa,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,KAAK,IAAI,CAAC;IAC5D,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAC5B,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAG9B,cAAc,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,iBAAiB,CAAC,EAAE,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,KAAK,IAAI,CAAC;IACvD,WAAW,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,WAAW,KAAK,IAAI,CAAC;IACjE,UAAU,CAAC,EAAE,OAAO,CAAC;IAGrB,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,cAAc,CAAC,EAAE,MAAM,IAAI,CAAC;IAC5B,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,eAAe,CAAC,EAAE,OAAO,CAAC;IAG1B,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,SAAS,CAAC,EAAE,MAAM,CAAC;IAGnB,eAAe,CAAC,EAAE,sBAAsB,CAAC;IAGzC,qBAAqB,CAAC,EAAE,MAAM,CAAC;IAG/B,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,qBAAqB,CAAC,EAAE,OAAO,CAAC;IAChC,qBAAqB,CAAC,EAAE,MAAM,CAAC;IAG/B,gBAAgB,CAAC,EAAE;QACjB,OAAO,CAAC,EAAE,OAAO,CAAC;QAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;QAClB,UAAU,CAAC,EAAE,OAAO,CAAC;QACrB,OAAO,CAAC,EAAE,KAAK,CAAC;YAAE,IAAI,EAAE,MAAM,CAAC;YAAC,IAAI,EAAE,SAAS,GAAG,SAAS,CAAA;SAAE,CAAC,CAAC;QAC/D,WAAW,CAAC,EAAE,OAAO,GAAG,cAAc,CAAC;KACxC,GAAG,IAAI,CAAC;IAGT,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAC5B,mBAAmB,CAAC,EAAE,OAAO,CAAC;IAG9B,SAAS,CAAC,EAAE;QACV,QAAQ,CAAC,EAAE,CAAC,GAAG,EAAE,GAAG,GAAG,GAAG,GAAG,CAAC;QAC9B,cAAc,CAAC,EAAE,OAAO,CAAC;QACzB,YAAY,CAAC,EAAE,OAAO,CAAC;KACxB,CAAC;IAEF,OAAO,CAAC,EAAE,CAAC,IAAI,EAAE;QACf,eAAe,EAAE,YAAY,GAAG,IAAI,CAAC;QACrC,eAAe,EAAE,YAAY,GAAG,IAAI,CAAC;QACrC,QAAQ,EAAE;YAAE,CAAC,EAAE,MAAM,CAAC;YAAC,CAAC,EAAE,MAAM,CAAA;SAAE,CAAC;QACnC,WAAW,EAAE;YAAE,IAAI,EAAE,MAAM,CAAA;SAAE,GAAG,IAAI,CAAC;QACrC,gBAAgB,EAAE;YAAE,IAAI,EAAE,MAAM,CAAA;SAAE,GAAG,IAAI,CAAC;QAC1C,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;KAC1B,KAAK,IAAI,CAAC;IAGX,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B,oBAAoB,CAAC,EAAE,MAAM,CAAC;CAC/B;AAsLD,iBAAS,mCAAmC,CAAC,EAC3C,QAAQ,EACR,eAAoB,EACpB,aAAa,EACb,cAAqB,EACrB,iBAAiB,EACjB,iBAAiB,EACjB,WAAW,EACX,UAAkB,EAClB,UAAiB,EACjB,cAAc,EACd,kBAAyB,EACzB,eAAsB,EACtB,QAAgB,EAChB,QAAgB,EAChB,aAAqB,EACrB,SAAc,EACd,eAAe,EACf,qBAAqB,EACrB,gBAAgB,EAChB,qBAA4B,EAC5B,qBAAqB,EACrB,oBAAoB,EACpB,gBAAgB,EAChB,iBAAyB,EACzB,iBAAwB,EACxB,mBAA0B,EAC1B,SAA2B,EAAE,yBAAyB;AACtD,OAAO,EACP,oBAAwB,EACxB,oBAAwB,GACzB,EAAE,mCAAmC,qBA65CrC;AA0BD,eAAO,MAAM,8BAA8B,4CAAsC,CAAC"}
@@ -210,6 +210,9 @@ onHover, buildingBorderRadius = 0, districtBorderRadius = 0, }) {
210
210
  const isAnimating = targetZoom !== null;
211
211
  // Track the last zoomToPath to detect changes
212
212
  const lastZoomToPathRef = (0, react_1.useRef)(null);
213
+ // Throttle ref for hover updates (improves performance with large datasets)
214
+ const lastHoverUpdateRef = (0, react_1.useRef)(0);
215
+ const HOVER_THROTTLE_MS = 16; // ~60fps max for hover updates
213
216
  (0, react_1.useEffect)(() => {
214
217
  // Reset user interaction state when enableZoom is disabled
215
218
  if (!enableZoom) {
@@ -693,6 +696,66 @@ onHover, buildingBorderRadius = 0, districtBorderRadius = 0, }) {
693
696
  }
694
697
  return layers;
695
698
  }, [stableLayers, dynamicLayers, abstractionLayer]);
699
+ // Memoize abstracted paths lookup - only recalculates when abstraction layer changes
700
+ const { abstractedPathsSet, abstractedPathLookup } = (0, react_1.useMemo)(() => {
701
+ const pathsSet = new Set();
702
+ const abstractionLayerDef = allLayers.find(l => l.id === 'directory-abstraction');
703
+ if (abstractionLayerDef && abstractionLayerDef.enabled) {
704
+ abstractionLayerDef.items.forEach(item => {
705
+ if (item.type === 'directory') {
706
+ pathsSet.add(item.path);
707
+ }
708
+ });
709
+ }
710
+ return {
711
+ abstractedPathsSet: pathsSet,
712
+ abstractedPathLookup: new PathHierarchyLookup(pathsSet),
713
+ };
714
+ }, [allLayers]);
715
+ // Memoize visible districts - only recalculates when data or abstraction changes, NOT on hover
716
+ const visibleDistrictsMemo = (0, react_1.useMemo)(() => {
717
+ if (!filteredCityData)
718
+ return [];
719
+ let districts = abstractedPathLookup.size > 0
720
+ ? filteredCityData.districts.filter(district => {
721
+ // Check for root abstraction first
722
+ if (abstractedPathLookup.has('')) {
723
+ return !district.path || district.path === '';
724
+ }
725
+ if (!district.path)
726
+ return true;
727
+ if (abstractedPathLookup.has(district.path))
728
+ return true;
729
+ return !abstractedPathLookup.isChildOfAbstracted(district.path);
730
+ })
731
+ : filteredCityData.districts;
732
+ // If root is abstracted and there's no root district, create one for the cover
733
+ if (abstractedPathsSet.has('')) {
734
+ const hasRootDistrict = districts.some(d => !d.path || d.path === '');
735
+ if (!hasRootDistrict) {
736
+ districts = [
737
+ {
738
+ path: '',
739
+ worldBounds: filteredCityData.bounds,
740
+ fileCount: filteredCityData.buildings.length,
741
+ type: 'directory',
742
+ },
743
+ ...districts,
744
+ ];
745
+ }
746
+ }
747
+ return districts;
748
+ }, [filteredCityData, abstractedPathLookup, abstractedPathsSet]);
749
+ // Memoize visible buildings - only recalculates when data or abstraction changes, NOT on hover
750
+ const visibleBuildingsMemo = (0, react_1.useMemo)(() => {
751
+ if (!filteredCityData)
752
+ return [];
753
+ return abstractedPathLookup.size > 0
754
+ ? filteredCityData.buildings.filter(building => {
755
+ return !abstractedPathLookup.isPathAbstracted(building.path);
756
+ })
757
+ : filteredCityData.buildings;
758
+ }, [filteredCityData, abstractedPathLookup]);
696
759
  // Update hit test cache when geometry or abstraction changes
697
760
  // Note: We don't depend on zoomState here because:
698
761
  // 1. The spatial grid only depends on buildings/districts and abstraction
@@ -762,64 +825,12 @@ onHover, buildingBorderRadius = 0, districtBorderRadius = 0, }) {
762
825
  y: ((z - coordinateSystemData.bounds.minZ) * scale + offsetZ) * zoomState.scale +
763
826
  zoomState.offsetY,
764
827
  });
765
- // Get abstracted paths for filtering child districts
766
- const abstractedPathsForDistricts = new Set();
767
- const abstractionLayerForDistricts = allLayers.find(l => l.id === 'directory-abstraction');
768
- if (abstractionLayerForDistricts && abstractionLayerForDistricts.enabled) {
769
- abstractionLayerForDistricts.items.forEach(item => {
770
- if (item.type === 'directory') {
771
- abstractedPathsForDistricts.add(item.path);
772
- }
773
- });
774
- }
775
- // Create PathHierarchyLookup for O(depth) containment checks
776
- const pathLookup = new PathHierarchyLookup(abstractedPathsForDistricts);
777
- // Keep abstracted districts (for covers) but filter out their children
778
- let visibleDistricts = pathLookup.size > 0
779
- ? filteredCityData.districts.filter(district => {
780
- // Check for root abstraction first
781
- if (pathLookup.has('')) {
782
- // If root is abstracted, only show root district
783
- return !district.path || district.path === '';
784
- }
785
- if (!district.path)
786
- return true; // Keep root
787
- // Keep the abstracted district itself (we need it for the cover)
788
- if (pathLookup.has(district.path)) {
789
- return true;
790
- }
791
- // Filter out children of abstracted directories using O(depth) lookup
792
- return !pathLookup.isChildOfAbstracted(district.path);
793
- })
794
- : filteredCityData.districts;
795
- // If root is abstracted and there's no root district, create one for the cover
796
- if (abstractedPathsForDistricts.has('')) {
797
- const hasRootDistrict = visibleDistricts.some(d => !d.path || d.path === '');
798
- if (!hasRootDistrict) {
799
- visibleDistricts = [
800
- {
801
- path: '',
802
- worldBounds: filteredCityData.bounds,
803
- fileCount: filteredCityData.buildings.length, // Total file count
804
- type: 'directory',
805
- },
806
- ...visibleDistricts,
807
- ];
808
- }
809
- }
828
+ // Use memoized visible districts and buildings (pre-filtered, doesn't recalculate on hover)
810
829
  // Draw districts with layer support
811
- (0, drawLayeredBuildings_1.drawLayeredDistricts)(ctx, visibleDistricts, worldToCanvas, scale * zoomState.scale, allLayers, interactionState.hoveredDistrict, fullSize, resolvedDefaultDirectoryColor, filteredCityData.metadata.layoutConfig, abstractedPathsForDistricts, // Pass abstracted paths to skip labels
830
+ (0, drawLayeredBuildings_1.drawLayeredDistricts)(ctx, visibleDistrictsMemo, worldToCanvas, scale * zoomState.scale, allLayers, interactionState.hoveredDistrict, fullSize, resolvedDefaultDirectoryColor, filteredCityData.metadata.layoutConfig, abstractedPathsSet, // Pass abstracted paths to skip labels
812
831
  showDirectoryLabels, districtBorderRadius);
813
- // Filter out buildings that are in abstracted directories
814
- // Reuse the pathLookup created earlier for O(depth) containment checks
815
- const visibleBuildings = pathLookup.size > 0
816
- ? filteredCityData.buildings.filter(building => {
817
- // Use PathHierarchyLookup for efficient O(depth) check
818
- return !pathLookup.isPathAbstracted(building.path);
819
- })
820
- : filteredCityData.buildings;
821
832
  // Draw buildings with layer support
822
- (0, drawLayeredBuildings_1.drawLayeredBuildings)(ctx, visibleBuildings, worldToCanvas, scale * zoomState.scale, allLayers, interactionState.hoveredBuilding, resolvedDefaultBuildingColor, showFileNames, resolvedHoverBorderColor, disableOpacityDimming, showFileTypeIcons, buildingBorderRadius);
833
+ (0, drawLayeredBuildings_1.drawLayeredBuildings)(ctx, visibleBuildingsMemo, worldToCanvas, scale * zoomState.scale, allLayers, interactionState.hoveredBuilding, resolvedDefaultBuildingColor, showFileNames, resolvedHoverBorderColor, disableOpacityDimming, showFileTypeIcons, buildingBorderRadius);
823
834
  // Performance monitoring end available for debugging
824
835
  // Performance stats available but not logged to reduce console noise
825
836
  // Uncomment for debugging: render time, buildings/districts counts, layer counts
@@ -848,6 +859,10 @@ onHover, buildingBorderRadius = 0, districtBorderRadius = 0, }) {
848
859
  resolvedDefaultBuildingColor,
849
860
  districtBorderRadius,
850
861
  showFileTypeIcons,
862
+ // Memoized values for performance (don't recalculate on hover)
863
+ visibleDistrictsMemo,
864
+ visibleBuildingsMemo,
865
+ abstractedPathsSet,
851
866
  ]);
852
867
  // Optimized hit testing
853
868
  const performHitTest = (0, react_1.useCallback)((canvasX, canvasY) => {
@@ -974,6 +989,12 @@ onHover, buildingBorderRadius = 0, districtBorderRadius = 0, }) {
974
989
  const handleMouseMoveInternal = (0, react_1.useCallback)((e) => {
975
990
  if (!canvasRef.current || !containerRef.current || !filteredCityData || zoomState.isDragging)
976
991
  return;
992
+ // Throttle hover updates to improve performance with large datasets
993
+ const now = performance.now();
994
+ if (now - lastHoverUpdateRef.current < HOVER_THROTTLE_MS) {
995
+ return;
996
+ }
997
+ lastHoverUpdateRef.current = now;
977
998
  // Get the container rect for mouse position
978
999
  const containerRect = containerRef.current.getBoundingClientRect();
979
1000
  // Get mouse position relative to the container
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@principal-ai/file-city-react",
3
- "version": "0.4.1",
3
+ "version": "0.4.2",
4
4
  "description": "React components for File City visualization",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -358,6 +358,10 @@ function ArchitectureMapHighlightLayersInner({
358
358
  // Track the last zoomToPath to detect changes
359
359
  const lastZoomToPathRef = useRef<string | null>(null);
360
360
 
361
+ // Throttle ref for hover updates (improves performance with large datasets)
362
+ const lastHoverUpdateRef = useRef<number>(0);
363
+ const HOVER_THROTTLE_MS = 16; // ~60fps max for hover updates
364
+
361
365
  useEffect(() => {
362
366
  // Reset user interaction state when enableZoom is disabled
363
367
  if (!enableZoom) {
@@ -1003,6 +1007,70 @@ function ArchitectureMapHighlightLayersInner({
1003
1007
  return layers;
1004
1008
  }, [stableLayers, dynamicLayers, abstractionLayer]);
1005
1009
 
1010
+ // Memoize abstracted paths lookup - only recalculates when abstraction layer changes
1011
+ const { abstractedPathsSet, abstractedPathLookup } = useMemo(() => {
1012
+ const pathsSet = new Set<string>();
1013
+ const abstractionLayerDef = allLayers.find(l => l.id === 'directory-abstraction');
1014
+ if (abstractionLayerDef && abstractionLayerDef.enabled) {
1015
+ abstractionLayerDef.items.forEach(item => {
1016
+ if (item.type === 'directory') {
1017
+ pathsSet.add(item.path);
1018
+ }
1019
+ });
1020
+ }
1021
+ return {
1022
+ abstractedPathsSet: pathsSet,
1023
+ abstractedPathLookup: new PathHierarchyLookup(pathsSet),
1024
+ };
1025
+ }, [allLayers]);
1026
+
1027
+ // Memoize visible districts - only recalculates when data or abstraction changes, NOT on hover
1028
+ const visibleDistrictsMemo = useMemo(() => {
1029
+ if (!filteredCityData) return [];
1030
+
1031
+ let districts =
1032
+ abstractedPathLookup.size > 0
1033
+ ? filteredCityData.districts.filter(district => {
1034
+ // Check for root abstraction first
1035
+ if (abstractedPathLookup.has('')) {
1036
+ return !district.path || district.path === '';
1037
+ }
1038
+ if (!district.path) return true;
1039
+ if (abstractedPathLookup.has(district.path)) return true;
1040
+ return !abstractedPathLookup.isChildOfAbstracted(district.path);
1041
+ })
1042
+ : filteredCityData.districts;
1043
+
1044
+ // If root is abstracted and there's no root district, create one for the cover
1045
+ if (abstractedPathsSet.has('')) {
1046
+ const hasRootDistrict = districts.some(d => !d.path || d.path === '');
1047
+ if (!hasRootDistrict) {
1048
+ districts = [
1049
+ {
1050
+ path: '',
1051
+ worldBounds: filteredCityData.bounds,
1052
+ fileCount: filteredCityData.buildings.length,
1053
+ type: 'directory' as const,
1054
+ },
1055
+ ...districts,
1056
+ ];
1057
+ }
1058
+ }
1059
+
1060
+ return districts;
1061
+ }, [filteredCityData, abstractedPathLookup, abstractedPathsSet]);
1062
+
1063
+ // Memoize visible buildings - only recalculates when data or abstraction changes, NOT on hover
1064
+ const visibleBuildingsMemo = useMemo(() => {
1065
+ if (!filteredCityData) return [];
1066
+
1067
+ return abstractedPathLookup.size > 0
1068
+ ? filteredCityData.buildings.filter(building => {
1069
+ return !abstractedPathLookup.isPathAbstracted(building.path);
1070
+ })
1071
+ : filteredCityData.buildings;
1072
+ }, [filteredCityData, abstractedPathLookup]);
1073
+
1006
1074
  // Update hit test cache when geometry or abstraction changes
1007
1075
  // Note: We don't depend on zoomState here because:
1008
1076
  // 1. The spatial grid only depends on buildings/districts and abstraction
@@ -1106,63 +1174,11 @@ function ArchitectureMapHighlightLayersInner({
1106
1174
  zoomState.offsetY,
1107
1175
  });
1108
1176
 
1109
- // Get abstracted paths for filtering child districts
1110
- const abstractedPathsForDistricts = new Set<string>();
1111
- const abstractionLayerForDistricts = allLayers.find(l => l.id === 'directory-abstraction');
1112
- if (abstractionLayerForDistricts && abstractionLayerForDistricts.enabled) {
1113
- abstractionLayerForDistricts.items.forEach(item => {
1114
- if (item.type === 'directory') {
1115
- abstractedPathsForDistricts.add(item.path);
1116
- }
1117
- });
1118
- }
1119
-
1120
- // Create PathHierarchyLookup for O(depth) containment checks
1121
- const pathLookup = new PathHierarchyLookup(abstractedPathsForDistricts);
1122
-
1123
- // Keep abstracted districts (for covers) but filter out their children
1124
- let visibleDistricts =
1125
- pathLookup.size > 0
1126
- ? filteredCityData.districts.filter(district => {
1127
- // Check for root abstraction first
1128
- if (pathLookup.has('')) {
1129
- // If root is abstracted, only show root district
1130
- return !district.path || district.path === '';
1131
- }
1132
-
1133
- if (!district.path) return true; // Keep root
1134
-
1135
- // Keep the abstracted district itself (we need it for the cover)
1136
- if (pathLookup.has(district.path)) {
1137
- return true;
1138
- }
1139
-
1140
- // Filter out children of abstracted directories using O(depth) lookup
1141
- return !pathLookup.isChildOfAbstracted(district.path);
1142
- })
1143
- : filteredCityData.districts;
1144
-
1145
- // If root is abstracted and there's no root district, create one for the cover
1146
- if (abstractedPathsForDistricts.has('')) {
1147
- const hasRootDistrict = visibleDistricts.some(d => !d.path || d.path === '');
1148
-
1149
- if (!hasRootDistrict) {
1150
- visibleDistricts = [
1151
- {
1152
- path: '',
1153
- worldBounds: filteredCityData.bounds,
1154
- fileCount: filteredCityData.buildings.length, // Total file count
1155
- type: 'directory',
1156
- },
1157
- ...visibleDistricts,
1158
- ];
1159
- }
1160
- }
1161
-
1177
+ // Use memoized visible districts and buildings (pre-filtered, doesn't recalculate on hover)
1162
1178
  // Draw districts with layer support
1163
1179
  drawLayeredDistricts(
1164
1180
  ctx,
1165
- visibleDistricts,
1181
+ visibleDistrictsMemo,
1166
1182
  worldToCanvas,
1167
1183
  scale * zoomState.scale,
1168
1184
  allLayers,
@@ -1170,25 +1186,15 @@ function ArchitectureMapHighlightLayersInner({
1170
1186
  fullSize,
1171
1187
  resolvedDefaultDirectoryColor,
1172
1188
  filteredCityData.metadata.layoutConfig,
1173
- abstractedPathsForDistricts, // Pass abstracted paths to skip labels
1189
+ abstractedPathsSet, // Pass abstracted paths to skip labels
1174
1190
  showDirectoryLabels,
1175
1191
  districtBorderRadius,
1176
1192
  );
1177
1193
 
1178
- // Filter out buildings that are in abstracted directories
1179
- // Reuse the pathLookup created earlier for O(depth) containment checks
1180
- const visibleBuildings =
1181
- pathLookup.size > 0
1182
- ? filteredCityData.buildings.filter(building => {
1183
- // Use PathHierarchyLookup for efficient O(depth) check
1184
- return !pathLookup.isPathAbstracted(building.path);
1185
- })
1186
- : filteredCityData.buildings;
1187
-
1188
1194
  // Draw buildings with layer support
1189
1195
  drawLayeredBuildings(
1190
1196
  ctx,
1191
- visibleBuildings,
1197
+ visibleBuildingsMemo,
1192
1198
  worldToCanvas,
1193
1199
  scale * zoomState.scale,
1194
1200
  allLayers,
@@ -1229,6 +1235,10 @@ function ArchitectureMapHighlightLayersInner({
1229
1235
  resolvedDefaultBuildingColor,
1230
1236
  districtBorderRadius,
1231
1237
  showFileTypeIcons,
1238
+ // Memoized values for performance (don't recalculate on hover)
1239
+ visibleDistrictsMemo,
1240
+ visibleBuildingsMemo,
1241
+ abstractedPathsSet,
1232
1242
  ]);
1233
1243
 
1234
1244
  // Optimized hit testing
@@ -1406,6 +1416,13 @@ function ArchitectureMapHighlightLayersInner({
1406
1416
  if (!canvasRef.current || !containerRef.current || !filteredCityData || zoomState.isDragging)
1407
1417
  return;
1408
1418
 
1419
+ // Throttle hover updates to improve performance with large datasets
1420
+ const now = performance.now();
1421
+ if (now - lastHoverUpdateRef.current < HOVER_THROTTLE_MS) {
1422
+ return;
1423
+ }
1424
+ lastHoverUpdateRef.current = now;
1425
+
1409
1426
  // Get the container rect for mouse position
1410
1427
  const containerRect = containerRef.current.getBoundingClientRect();
1411
1428
  // Get mouse position relative to the container