Hoist DottedMap constants to module scope, static-import above-fold components
DottedMap: move createMap, stagger helpers, and base dots path to module scope so they compute once on module load and survive unmount/remount cycles — eliminates all recomputation when switching tabs. Dashboard: restore static imports for the 5 above-fold components (Chart, ContentStats, TopReferrers, Locations, TechSpecs) now that their heavy computations are memoized. Keeps below-fold components (PerformanceStats, GoalStats, ScrollDepth, Campaigns, etc.) dynamic. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -26,12 +26,12 @@ import dynamic from 'next/dynamic'
|
||||
import { DashboardSkeleton, useMinimumLoading } from '@/components/skeletons'
|
||||
import FilterBar from '@/components/dashboard/FilterBar'
|
||||
import AddFilterDropdown, { type FilterSuggestion, type FilterSuggestions } from '@/components/dashboard/AddFilterDropdown'
|
||||
import Chart from '@/components/dashboard/Chart'
|
||||
import ContentStats from '@/components/dashboard/ContentStats'
|
||||
import TopReferrers from '@/components/dashboard/TopReferrers'
|
||||
import Locations from '@/components/dashboard/Locations'
|
||||
import TechSpecs from '@/components/dashboard/TechSpecs'
|
||||
|
||||
const Chart = dynamic(() => import('@/components/dashboard/Chart'))
|
||||
const ContentStats = dynamic(() => import('@/components/dashboard/ContentStats'))
|
||||
const TopReferrers = dynamic(() => import('@/components/dashboard/TopReferrers'))
|
||||
const Locations = dynamic(() => import('@/components/dashboard/Locations'))
|
||||
const TechSpecs = dynamic(() => import('@/components/dashboard/TechSpecs'))
|
||||
const PerformanceStats = dynamic(() => import('@/components/dashboard/PerformanceStats'))
|
||||
const GoalStats = dynamic(() => import('@/components/dashboard/GoalStats'))
|
||||
const ScrollDepth = dynamic(() => import('@/components/dashboard/ScrollDepth'))
|
||||
|
||||
@@ -5,6 +5,55 @@ import { createMap } from 'svg-dotted-map'
|
||||
import { cn, formatNumber } from '@ciphera-net/ui'
|
||||
import { countryCentroids } from '@/lib/country-centroids'
|
||||
|
||||
// ─── Module-level constants ────────────────────────────────────────
|
||||
// Computed once when the module loads, survives component unmount/remount.
|
||||
const MAP_WIDTH = 150
|
||||
const MAP_HEIGHT = 68
|
||||
const DOT_RADIUS = 0.25
|
||||
|
||||
const { points: MAP_POINTS, addMarkers } = createMap({ width: MAP_WIDTH, height: MAP_HEIGHT, mapSamples: 8000 })
|
||||
|
||||
// Pre-compute stagger helpers (row offsets for hex-grid pattern)
|
||||
const _stagger = (() => {
|
||||
const sorted = [...MAP_POINTS].sort((a, b) => a.y - b.y || a.x - b.x)
|
||||
const rowMap = new Map<number, number>()
|
||||
let step = 0
|
||||
let prevY = Number.NaN
|
||||
let prevXInRow = Number.NaN
|
||||
|
||||
for (const p of sorted) {
|
||||
if (p.y !== prevY) {
|
||||
prevY = p.y
|
||||
prevXInRow = Number.NaN
|
||||
if (!rowMap.has(p.y)) rowMap.set(p.y, rowMap.size)
|
||||
}
|
||||
if (!Number.isNaN(prevXInRow)) {
|
||||
const delta = p.x - prevXInRow
|
||||
if (delta > 0) step = step === 0 ? delta : Math.min(step, delta)
|
||||
}
|
||||
prevXInRow = p.x
|
||||
}
|
||||
|
||||
return { xStep: step || 1, yToRowIndex: rowMap }
|
||||
})()
|
||||
|
||||
// Pre-compute the base map dots as a single SVG path string (~8000 circles → 1 path)
|
||||
const BASE_DOTS_PATH = (() => {
|
||||
const r = DOT_RADIUS
|
||||
const d = r * 2
|
||||
const parts: string[] = []
|
||||
for (const point of MAP_POINTS) {
|
||||
const rowIndex = _stagger.yToRowIndex.get(point.y) ?? 0
|
||||
const offsetX = rowIndex % 2 === 1 ? _stagger.xStep / 2 : 0
|
||||
const cx = point.x + offsetX
|
||||
const cy = point.y
|
||||
parts.push(`M${cx - r},${cy}a${r},${r} 0 1,0 ${d},0a${r},${r} 0 1,0 ${-d},0`)
|
||||
}
|
||||
return parts.join('')
|
||||
})()
|
||||
|
||||
// ─── Component ─────────────────────────────────────────────────────
|
||||
|
||||
interface DottedMapProps {
|
||||
data: Array<{ country: string; pageviews: number }>
|
||||
className?: string
|
||||
@@ -20,16 +69,8 @@ function getCountryName(code: string): string {
|
||||
}
|
||||
|
||||
export default function DottedMap({ data, className }: DottedMapProps) {
|
||||
const width = 150
|
||||
const height = 68
|
||||
const dotRadius = 0.25
|
||||
const [tooltip, setTooltip] = useState<{ x: number; y: number; country: string; pageviews: number } | null>(null)
|
||||
|
||||
const { points, addMarkers } = useMemo(
|
||||
() => createMap({ width, height, mapSamples: 8000 }),
|
||||
[width, height],
|
||||
)
|
||||
|
||||
const markerData = useMemo(() => {
|
||||
if (!data.length) return []
|
||||
|
||||
@@ -47,55 +88,15 @@ export default function DottedMap({ data, className }: DottedMapProps) {
|
||||
}))
|
||||
}, [data])
|
||||
|
||||
const markerInputs = useMemo(
|
||||
() => markerData.map((d) => ({ lat: d.lat, lng: d.lng, size: d.size })),
|
||||
const processedMarkers = useMemo(
|
||||
() => addMarkers(markerData.map((d) => ({ lat: d.lat, lng: d.lng, size: d.size }))),
|
||||
[markerData],
|
||||
)
|
||||
const processedMarkers = useMemo(() => addMarkers(markerInputs), [addMarkers, markerInputs])
|
||||
|
||||
// Compute stagger helpers
|
||||
const { xStep, yToRowIndex } = useMemo(() => {
|
||||
const sorted = [...points].sort((a, b) => a.y - b.y || a.x - b.x)
|
||||
const rowMap = new Map<number, number>()
|
||||
let step = 0
|
||||
let prevY = Number.NaN
|
||||
let prevXInRow = Number.NaN
|
||||
|
||||
for (const p of sorted) {
|
||||
if (p.y !== prevY) {
|
||||
prevY = p.y
|
||||
prevXInRow = Number.NaN
|
||||
if (!rowMap.has(p.y)) rowMap.set(p.y, rowMap.size)
|
||||
}
|
||||
if (!Number.isNaN(prevXInRow)) {
|
||||
const delta = p.x - prevXInRow
|
||||
if (delta > 0) step = step === 0 ? delta : Math.min(step, delta)
|
||||
}
|
||||
prevXInRow = p.x
|
||||
}
|
||||
|
||||
return { xStep: step || 1, yToRowIndex: rowMap }
|
||||
}, [points])
|
||||
|
||||
// Batch all 8000 base dots into a single <path> instead of 8000 <circle> elements
|
||||
const dotsPath = useMemo(() => {
|
||||
const r = dotRadius
|
||||
const d = r * 2
|
||||
const parts: string[] = []
|
||||
for (const point of points) {
|
||||
const rowIndex = yToRowIndex.get(point.y) ?? 0
|
||||
const offsetX = rowIndex % 2 === 1 ? xStep / 2 : 0
|
||||
const cx = point.x + offsetX
|
||||
const cy = point.y
|
||||
parts.push(`M${cx - r},${cy}a${r},${r} 0 1,0 ${d},0a${r},${r} 0 1,0 ${-d},0`)
|
||||
}
|
||||
return parts.join('')
|
||||
}, [points, dotRadius, xStep, yToRowIndex])
|
||||
|
||||
return (
|
||||
<div className="relative w-full h-full flex items-center justify-center">
|
||||
<svg
|
||||
viewBox={`0 0 ${width} ${height}`}
|
||||
viewBox={`0 0 ${MAP_WIDTH} ${MAP_HEIGHT}`}
|
||||
className={cn('text-neutral-400 dark:text-neutral-500', className)}
|
||||
style={{ width: '100%', height: '100%' }}
|
||||
>
|
||||
@@ -110,18 +111,18 @@ export default function DottedMap({ data, className }: DottedMapProps) {
|
||||
</filter>
|
||||
</defs>
|
||||
<path
|
||||
d={dotsPath}
|
||||
d={BASE_DOTS_PATH}
|
||||
fill="currentColor"
|
||||
/>
|
||||
{processedMarkers.map((marker, index) => {
|
||||
const rowIndex = yToRowIndex.get(marker.y) ?? 0
|
||||
const offsetX = rowIndex % 2 === 1 ? xStep / 2 : 0
|
||||
const rowIndex = _stagger.yToRowIndex.get(marker.y) ?? 0
|
||||
const offsetX = rowIndex % 2 === 1 ? _stagger.xStep / 2 : 0
|
||||
const info = markerData[index]
|
||||
return (
|
||||
<circle
|
||||
cx={marker.x + offsetX}
|
||||
cy={marker.y}
|
||||
r={marker.size ?? dotRadius}
|
||||
r={marker.size ?? DOT_RADIUS}
|
||||
fill="#FD5E0F"
|
||||
filter="url(#marker-glow)"
|
||||
className="cursor-pointer"
|
||||
@@ -132,8 +133,8 @@ export default function DottedMap({ data, className }: DottedMapProps) {
|
||||
const svgX = marker.x + offsetX
|
||||
const svgY = marker.y
|
||||
setTooltip({
|
||||
x: rect.left + (svgX / width) * rect.width,
|
||||
y: rect.top + (svgY / height) * rect.height,
|
||||
x: rect.left + (svgX / MAP_WIDTH) * rect.width,
|
||||
y: rect.top + (svgY / MAP_HEIGHT) * rect.height,
|
||||
country: info.country,
|
||||
pageviews: info.pageviews,
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user