Skip to content

Commit

Permalink
feat: add fiber node map
Browse files Browse the repository at this point in the history
  • Loading branch information
Keith-CY committed Feb 7, 2025
1 parent 56d6daa commit 5861ead
Show file tree
Hide file tree
Showing 11 changed files with 209 additions and 130 deletions.
163 changes: 87 additions & 76 deletions src/components/GraphNodeIps/NodesMap.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,20 @@
import { useEffect, useRef } from 'react'
import 'echarts/lib/component/tooltip'
import 'echarts-gl'
import 'echarts/map/js/world' // Load the world map
import echarts from 'echarts/lib/echarts'
import styles from './index.module.scss'
import { getPrimaryColor } from '../../constants/common'

const LAUNCH_TIME_OF_MAINNET = 0x16e70e6985c
const primaryColor = getPrimaryColor()

export type IpPoint = {
id: string
ip: string
lon: number
lat: number
city: string
connections: string[] // other node id
}

type EchartPoint = [long: number, lat: number, city: string]
Expand All @@ -27,58 +31,31 @@ export const isValidIpPoint = (ip: unknown): ip is IpPoint => {

const option = {
backgroundColor: '#000',
// tooltip: {
// show: true,
// formatter: (params: { data: EchartPoint }) => {
// return params.data[2]
// },
// },
globe: {
environment: '/images/chart/dark.webp',
baseTexture: '/images/chart/earth.jpg',
heightTexture: '/images/chart/earth.jpg',
displacementScale: 0.04,
displacementQuality: 'high',
shading: 'realistic',
realisticMaterial: {
roughness: 0.9,
metalness: 0,
},
temporalSuperSampling: {
enable: true,
},
postEffect: {
enable: true,
depthOfField: {
enable: false,
focalDistance: 150,
},
geo: {
silent: true,
map: 'world',
roam: true,
zoom: 2,
label: {
show: false,
},
light: {
main: {
intensity: 10,
shadow: true,
time: new Date(LAUNCH_TIME_OF_MAINNET),
},
itemStyle: {
areaColor: '#1b1b1b',
borderColor: '#555',
},
viewControl: {
autoRotate: true,
autoRotateSpeed: 1,
distance: 800,
emphasis: {
areaColor: '#444',
},
silent: true,
environment: '#333',
},
}

const color = 'red'

export const NodesMap = ({ ips }: { ips: IpPoint[] }) => {
const containerRef = useRef<HTMLDivElement | null>(null)

const points: EchartPoint[] = ips.map(i => [i.lon, i.lat, i.city])

useEffect(() => {
if (!containerRef.current) return
const points: EchartPoint[] = ips.map(i => [i.lon, i.lat, i.city])
if (!points.length) return
let ins = echarts.getInstanceByDom(containerRef.current)
if (!ins) {
Expand All @@ -87,57 +64,91 @@ export const NodesMap = ({ ips }: { ips: IpPoint[] }) => {

const lines: EchartLine[] = []

for (let i = 0; i < points.length - 1; i++) {
lines.push([points[i], points[i + 1]])
}
ips.forEach(ip => {
ip.connections.forEach(connId => {
const conn = ips.find(i => i.id === connId)

if (!conn) {
return
}

lines.push([
[ip.lon, ip.lat, ip.city],
[conn.lon, conn.lat, conn.city],
])
})
})

const series = [
{
type: 'lines3D',
name: 'blocks',
coordinateSystem: 'globe',
blendMode: 'lighter',
symbolSize: 2,
type: 'effectScatter',
coordinateSystem: 'geo',
data: points,
symbolSize: 8,
rippleEffect: {
scale: 3,
brushType: 'stroke',
},
itemStyle: {
color,
opacity: 0.1,
color: primaryColor,
shadowBlur: 10,
shadowColor: primaryColor,
},
effect: {
tooltip: {
show: true,
trailWidth: 1,
trailLength: 0.15,
trailOpacity: 0.1,
constantSpeed: 10,
},
lineStyle: {
width: 1,
color,
opacity: 0.02,
label: {
show: true,
position: 'right',
formatter: (p: { data: EchartPoint }) => {
return p.data[2]
},
color: '#fff',
fontSize: 8,
backgroundColor: 'rgba(0, 0, 0, 0.5)',
padding: [4, 6],
broderRadius: 3,
},
data: lines,
},
{
type: 'scatter3D',
coordinateSystem: 'globe',
blendMode: 'lighter',
symbolSize: 10,
itemStyle: {
color,
opacity: 0.2,
},
label: {
type: 'lines',
coordinateSystem: 'geo',
zlevel: 2,
effect: {
show: true,
formatter: '{b}',
period: 1,
trailLength: 0.1, // Shorter trail
symbol: 'arrow',
symbolSize: 3,
},
data: points,
animationEasing: 'cubicOut',

lineStyle: {
curveness: 0.2,
color: {
type: 'linear',
x: 0,
y: 0,
x2: 1,
y2: 0,
colorStops: [
{ offset: 0, color: primaryColor },
{ offset: 0.5, color: '#00ffea' },
{ offset: 1, color: primaryColor },
],
},
width: 1,
opacity: 0.4,
},
data: lines,
},
]

ins.setOption({
...option,
series,
} as any)
}, [points])
}, [ips])

useEffect(() => {
if (!containerRef.current) return
Expand All @@ -153,8 +164,8 @@ export const NodesMap = ({ ips }: { ips: IpPoint[] }) => {
}
})

if (!points) {
return <div>Fail to load data</div>
if (!ips) {
return <div>Data not found</div>
}

return <div className={styles.container} ref={containerRef} />
Expand Down
2 changes: 2 additions & 0 deletions src/components/GraphNodeIps/index.module.scss
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
.container {
width: 100%;
height: 400px;
border-radius: 4px;
overflow: hidden;
}
61 changes: 33 additions & 28 deletions src/components/GraphNodeIps/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,24 +5,26 @@ import NodesMap, { isValidIpPoint, type IpPoint } from './NodesMap'

const IP_REGEXP = /\/ip4\/(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})/

const getIpFromP2pAddr = (p2pAddr: string) => {
const match = p2pAddr.match(IP_REGEXP)
if (match) {
return match[1]
}
return null
}

const GraphNodeIps = () => {
const { data: ipsData } = useQuery({
queryKey: ['fiber_graph_node_ips'],
const { data: nodes } = useQuery({
queryKey: ['fiber_graph_node_addresses'],
queryFn: explorerService.api.getGraphNodeIPs,
refetchInterval: 60 * 1000 * 10, // 10 minutes
})

const ips =
(ipsData?.data
(nodes?.data
.map(i => i.addresses)
.flat()
.map(addr => {
const match = addr.match(IP_REGEXP)
if (match) {
return match[1]
}
return null
})
.map(getIpFromP2pAddr)
.filter(ip => !!ip) as string[]) ?? []

const { data: ipInfos } = useQuery({
Expand All @@ -31,24 +33,27 @@ const GraphNodeIps = () => {
enabled: !!ips.length,
})

const list = ips
.map(ip => {
const ipInfo = ipInfos?.ips[ip as keyof typeof ipInfos.ips]
return {
ip,
...ipInfo,
}
})
.filter(ip => isValidIpPoint(ip)) as IpPoint[]

return (
<div>
<div>{list.length}</div>
<div>
<NodesMap ips={list} />
</div>
</div>
)
const list =
nodes?.data
?.map(node => {
const ips = node.addresses.map(getIpFromP2pAddr)

const infos = ips
.map(ip => {
const ipInfo = ipInfos?.ips[ip as keyof typeof ipInfos.ips]
return {
...ipInfo,
ip,
connections: node.connections,
id: node.nodeId,
}
})
.filter(p => isValidIpPoint(p)) as IpPoint[]
return infos
})
.flat() ?? []

return <NodesMap ips={list} />
}

export default GraphNodeIps
2 changes: 2 additions & 0 deletions src/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -1174,6 +1174,8 @@
"mean_fee_rate": "Mean Fee Rate",
"median_locked_capacity": "Median Locked Capacity",
"median_fee_rate": "Median Fee Rate",
"public_fiber_node_world_map": "Public Fiber Node World Map",
"public_fiber_nodes": "Public Fiber Nodes",
"node": {
"id": "Node ID",
"name": "Name",
Expand Down
2 changes: 2 additions & 0 deletions src/locales/zh.json
Original file line number Diff line number Diff line change
Expand Up @@ -1149,6 +1149,8 @@
"mean_fee_rate": "平均费率",
"median_locked_capacity": "中位数锁定 Capacity",
"median_fee_rate": "中位数费率",
"public_fiber_node_world_map": "公开 Fiber 节点地图",
"public_fiber_nodes": "公开 Fiber 节点",
"node": {
"id": "节点 ID",
"name": "名称",
Expand Down
7 changes: 7 additions & 0 deletions src/pages/Fiber/Graph/MeanAndMedium/index.module.scss
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
@import '../../../../styles//variables.module.scss';

.container {
font-size: 14px;
background: #fafafa;
Expand Down Expand Up @@ -49,6 +51,11 @@
margin-left: 4px;
}
}

@media screen and (width < $extraLargeBreakPoint) {
display: flex;
gap: 20px;
}
}

hr {
Expand Down
Loading

0 comments on commit 5861ead

Please sign in to comment.