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 { DashboardSkeleton, useMinimumLoading } from '@/components/skeletons'
|
||||||
import FilterBar from '@/components/dashboard/FilterBar'
|
import FilterBar from '@/components/dashboard/FilterBar'
|
||||||
import AddFilterDropdown, { type FilterSuggestion, type FilterSuggestions } from '@/components/dashboard/AddFilterDropdown'
|
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 PerformanceStats = dynamic(() => import('@/components/dashboard/PerformanceStats'))
|
||||||
const GoalStats = dynamic(() => import('@/components/dashboard/GoalStats'))
|
const GoalStats = dynamic(() => import('@/components/dashboard/GoalStats'))
|
||||||
const ScrollDepth = dynamic(() => import('@/components/dashboard/ScrollDepth'))
|
const ScrollDepth = dynamic(() => import('@/components/dashboard/ScrollDepth'))
|
||||||
|
|||||||
@@ -5,57 +5,17 @@ import { createMap } from 'svg-dotted-map'
|
|||||||
import { cn, formatNumber } from '@ciphera-net/ui'
|
import { cn, formatNumber } from '@ciphera-net/ui'
|
||||||
import { countryCentroids } from '@/lib/country-centroids'
|
import { countryCentroids } from '@/lib/country-centroids'
|
||||||
|
|
||||||
interface DottedMapProps {
|
// ─── Module-level constants ────────────────────────────────────────
|
||||||
data: Array<{ country: string; pageviews: number }>
|
// Computed once when the module loads, survives component unmount/remount.
|
||||||
className?: string
|
const MAP_WIDTH = 150
|
||||||
}
|
const MAP_HEIGHT = 68
|
||||||
|
const DOT_RADIUS = 0.25
|
||||||
|
|
||||||
function getCountryName(code: string): string {
|
const { points: MAP_POINTS, addMarkers } = createMap({ width: MAP_WIDTH, height: MAP_HEIGHT, mapSamples: 8000 })
|
||||||
try {
|
|
||||||
const regionNames = new Intl.DisplayNames(['en'], { type: 'region' })
|
|
||||||
return regionNames.of(code) || code
|
|
||||||
} catch {
|
|
||||||
return code
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function DottedMap({ data, className }: DottedMapProps) {
|
// Pre-compute stagger helpers (row offsets for hex-grid pattern)
|
||||||
const width = 150
|
const _stagger = (() => {
|
||||||
const height = 68
|
const sorted = [...MAP_POINTS].sort((a, b) => a.y - b.y || a.x - b.x)
|
||||||
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 []
|
|
||||||
|
|
||||||
const max = Math.max(...data.map((d) => d.pageviews))
|
|
||||||
if (max === 0) return []
|
|
||||||
|
|
||||||
return data
|
|
||||||
.filter((d) => d.country && d.country !== 'Unknown' && countryCentroids[d.country])
|
|
||||||
.map((d) => ({
|
|
||||||
lat: countryCentroids[d.country].lat,
|
|
||||||
lng: countryCentroids[d.country].lng,
|
|
||||||
size: 0.4 + (d.pageviews / max) * 0.8,
|
|
||||||
country: d.country,
|
|
||||||
pageviews: d.pageviews,
|
|
||||||
}))
|
|
||||||
}, [data])
|
|
||||||
|
|
||||||
const markerInputs = useMemo(
|
|
||||||
() => 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>()
|
const rowMap = new Map<number, number>()
|
||||||
let step = 0
|
let step = 0
|
||||||
let prevY = Number.NaN
|
let prevY = Number.NaN
|
||||||
@@ -75,27 +35,68 @@ export default function DottedMap({ data, className }: DottedMapProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return { xStep: step || 1, yToRowIndex: rowMap }
|
return { xStep: step || 1, yToRowIndex: rowMap }
|
||||||
}, [points])
|
})()
|
||||||
|
|
||||||
// Batch all 8000 base dots into a single <path> instead of 8000 <circle> elements
|
// Pre-compute the base map dots as a single SVG path string (~8000 circles → 1 path)
|
||||||
const dotsPath = useMemo(() => {
|
const BASE_DOTS_PATH = (() => {
|
||||||
const r = dotRadius
|
const r = DOT_RADIUS
|
||||||
const d = r * 2
|
const d = r * 2
|
||||||
const parts: string[] = []
|
const parts: string[] = []
|
||||||
for (const point of points) {
|
for (const point of MAP_POINTS) {
|
||||||
const rowIndex = yToRowIndex.get(point.y) ?? 0
|
const rowIndex = _stagger.yToRowIndex.get(point.y) ?? 0
|
||||||
const offsetX = rowIndex % 2 === 1 ? xStep / 2 : 0
|
const offsetX = rowIndex % 2 === 1 ? _stagger.xStep / 2 : 0
|
||||||
const cx = point.x + offsetX
|
const cx = point.x + offsetX
|
||||||
const cy = point.y
|
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`)
|
parts.push(`M${cx - r},${cy}a${r},${r} 0 1,0 ${d},0a${r},${r} 0 1,0 ${-d},0`)
|
||||||
}
|
}
|
||||||
return parts.join('')
|
return parts.join('')
|
||||||
}, [points, dotRadius, xStep, yToRowIndex])
|
})()
|
||||||
|
|
||||||
|
// ─── Component ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
interface DottedMapProps {
|
||||||
|
data: Array<{ country: string; pageviews: number }>
|
||||||
|
className?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
function getCountryName(code: string): string {
|
||||||
|
try {
|
||||||
|
const regionNames = new Intl.DisplayNames(['en'], { type: 'region' })
|
||||||
|
return regionNames.of(code) || code
|
||||||
|
} catch {
|
||||||
|
return code
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function DottedMap({ data, className }: DottedMapProps) {
|
||||||
|
const [tooltip, setTooltip] = useState<{ x: number; y: number; country: string; pageviews: number } | null>(null)
|
||||||
|
|
||||||
|
const markerData = useMemo(() => {
|
||||||
|
if (!data.length) return []
|
||||||
|
|
||||||
|
const max = Math.max(...data.map((d) => d.pageviews))
|
||||||
|
if (max === 0) return []
|
||||||
|
|
||||||
|
return data
|
||||||
|
.filter((d) => d.country && d.country !== 'Unknown' && countryCentroids[d.country])
|
||||||
|
.map((d) => ({
|
||||||
|
lat: countryCentroids[d.country].lat,
|
||||||
|
lng: countryCentroids[d.country].lng,
|
||||||
|
size: 0.4 + (d.pageviews / max) * 0.8,
|
||||||
|
country: d.country,
|
||||||
|
pageviews: d.pageviews,
|
||||||
|
}))
|
||||||
|
}, [data])
|
||||||
|
|
||||||
|
const processedMarkers = useMemo(
|
||||||
|
() => addMarkers(markerData.map((d) => ({ lat: d.lat, lng: d.lng, size: d.size }))),
|
||||||
|
[markerData],
|
||||||
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative w-full h-full flex items-center justify-center">
|
<div className="relative w-full h-full flex items-center justify-center">
|
||||||
<svg
|
<svg
|
||||||
viewBox={`0 0 ${width} ${height}`}
|
viewBox={`0 0 ${MAP_WIDTH} ${MAP_HEIGHT}`}
|
||||||
className={cn('text-neutral-400 dark:text-neutral-500', className)}
|
className={cn('text-neutral-400 dark:text-neutral-500', className)}
|
||||||
style={{ width: '100%', height: '100%' }}
|
style={{ width: '100%', height: '100%' }}
|
||||||
>
|
>
|
||||||
@@ -110,18 +111,18 @@ export default function DottedMap({ data, className }: DottedMapProps) {
|
|||||||
</filter>
|
</filter>
|
||||||
</defs>
|
</defs>
|
||||||
<path
|
<path
|
||||||
d={dotsPath}
|
d={BASE_DOTS_PATH}
|
||||||
fill="currentColor"
|
fill="currentColor"
|
||||||
/>
|
/>
|
||||||
{processedMarkers.map((marker, index) => {
|
{processedMarkers.map((marker, index) => {
|
||||||
const rowIndex = yToRowIndex.get(marker.y) ?? 0
|
const rowIndex = _stagger.yToRowIndex.get(marker.y) ?? 0
|
||||||
const offsetX = rowIndex % 2 === 1 ? xStep / 2 : 0
|
const offsetX = rowIndex % 2 === 1 ? _stagger.xStep / 2 : 0
|
||||||
const info = markerData[index]
|
const info = markerData[index]
|
||||||
return (
|
return (
|
||||||
<circle
|
<circle
|
||||||
cx={marker.x + offsetX}
|
cx={marker.x + offsetX}
|
||||||
cy={marker.y}
|
cy={marker.y}
|
||||||
r={marker.size ?? dotRadius}
|
r={marker.size ?? DOT_RADIUS}
|
||||||
fill="#FD5E0F"
|
fill="#FD5E0F"
|
||||||
filter="url(#marker-glow)"
|
filter="url(#marker-glow)"
|
||||||
className="cursor-pointer"
|
className="cursor-pointer"
|
||||||
@@ -132,8 +133,8 @@ export default function DottedMap({ data, className }: DottedMapProps) {
|
|||||||
const svgX = marker.x + offsetX
|
const svgX = marker.x + offsetX
|
||||||
const svgY = marker.y
|
const svgY = marker.y
|
||||||
setTooltip({
|
setTooltip({
|
||||||
x: rect.left + (svgX / width) * rect.width,
|
x: rect.left + (svgX / MAP_WIDTH) * rect.width,
|
||||||
y: rect.top + (svgY / height) * rect.height,
|
y: rect.top + (svgY / MAP_HEIGHT) * rect.height,
|
||||||
country: info.country,
|
country: info.country,
|
||||||
pageviews: info.pageviews,
|
pageviews: info.pageviews,
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user