An interactive map of the BGP network topology. Each node represents an Autonomous System. The positioning of each AS is determined by the size of its customer cone (radius), and by the longitude of its geolocation (angle).
See also the 3D force-simulated version
Project developed during the CAIDA BGP hackathon 2016
this iteration updates the code to ES2017+ with the help of our friend lebab
an iteration on the Internet Interactive Map from @vastur
this iteration makes the code nice to work with, for my subjective definition of nice to work with :-)
prettier formatting and 2-space indentation
<head>
<link rel="stylesheet" href="//maxcdn.bootstrapcdn.com/font-awesome/4.5.0/css/font-awesome.min.css">
<link rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/cytoscape-panzoom/2.2.0/cytoscape.js-panzoom.min.css">
<link rel="stylesheet" href="//cdn.rawgit.com/cytoscape/cytoscape.js-navigator/1.0.1/cytoscape.js-navigator.css">
<link rel="stylesheet" href="main.css">
<script src="//cdnjs.cloudflare.com/ajax/libs/require.js/2.1.22/require.min.js"></script>
<script src="require-config.js"></script>
<script>
require([
'jquery',
'react',
'react-dom',
'jsx!intermap'
], function($, React, ReactDOM, IntermapViz) {
$.getJSON("data-subset.json", function(data) {
$(function() {
ReactDOM.render(
React.createElement(IntermapViz, {
data: data
}),
document.getElementById('viz')
);
});
});
});
</script>
</head>
<body id="viz"></body>
define(
[
'react',
'react-dom',
'jquery',
'underscore',
'cytoscape',
'cytoscape-panzoom',
'colors'
],
(React, ReactDOM, $, _, cytoscape, panzoom, Colors) => {
panzoom(cytoscape, $) // Register panzoom
const CytoscapeGraph = React.createClass({
const: {
REFRESH_STYLE_PAUSE: 300, // ms
MIN_EDGE_WIDTH: 0.15,
MAX_EDGE_WIDTH: 0.25,
NODE_SIZE: 2
},
componentDidMount() {
const props = this.props
const consts = this.const
const cs = (this._csGraph = cytoscape({
container: ReactDOM.findDOMNode(this),
layout: {
name: 'preset',
fit: false
},
minZoom: 1,
maxZoom: 100,
autoungrabify: true,
autolock: true,
hideEdgesOnViewport: true,
hideLabelsOnViewport: true,
textureOnViewport: true,
motionBlur: true,
style: [
{
selector: 'node',
style: {
width: this.const.NODE_SIZE,
height: this.const.NODE_SIZE,
'border-width': this.const.NODE_SIZE * 0.1,
'border-color': 'orange',
'background-color': 'yellow',
'background-opacity': 0.3
}
},
{
selector: 'edge',
style: {
'curve-style': 'haystack', // 'bezier'
width: 0.05,
opacity(el) {
return el.data('opacity')
},
'line-color': function(el) {
return el.data('color')
}
}
}
],
elements: {
nodes: props.nodes.map(node => ({
data: $.extend({ id: node.id }, node.nodeData),
position: {
x: node.x,
y: node.y
}
})),
edges: props.edges.map(edge => ({
data: {
source: edge.src,
target: edge.dst,
color: edge.color || 'lightgrey',
opacity: edge.opacity || 1
}
}))
}
})
.on('zoom', () => {
adjustElementSizes()
zoomOrPan()
})
.on('pan', zoomOrPan)
.on('mouseover', 'node', function() {
props.onNodeHover(this.data())
})
.on('select', 'node', function() {
props.onNodeClick(this.data())
}))
cs.panzoom({
zoomFactor: 0.1, // zoom factor per zoom tick
zoomDelay: 50, // how many ms between zoom ticks
minZoom: 1, // min zoom level
maxZoom: 100, // max zoom level
fitPadding: 0, // padding when fitting
panSpeed: 20, // how many ms in between pan ticks
panDistance: 40 // max pan distance per tick
})
function zoomOrPan() {
const pan = cs.pan()
props.onZoomOrPan(cs.zoom(), pan.x, pan.y)
}
var adjustElementSizes = _.debounce(
this.resetStyle,
consts.REFRESH_STYLE_PAUSE
)
},
render() {
return (
<div
style={{
width: this.props.width,
height: this.props.height
}}
/>
)
},
zoom(ratio) {
this._csGraph.zoom(ratio)
},
pan(x, y) {
this._csGraph.pan({ x, y })
},
getNodeById(id) {
return this._csGraph.getElementById(id)
},
resetStyle() {
const cs = this._csGraph
const zoom = cs.zoom()
const nodeSize = this.const.NODE_SIZE / zoom
cs
.style()
.selector('node')
.style({
width: nodeSize,
height: nodeSize,
'background-color': 'yellow',
'background-opacity': 0.3
})
.selector('edge')
.style({
width:
Math.min(
this.const.MIN_EDGE_WIDTH * zoom,
this.const.MAX_EDGE_WIDTH
) / zoom
})
.update()
}
})
return React.createClass({
getDefaultProps() {
return {
graphData: graphRandomGenerator(5, 10),
width: window.innerWidth,
height: window.innerHeight,
margin: 0,
selectedAs: null
}
},
getInitialState() {
return {
radialNodes: this._genRadialNodes(),
edges: this._getEdges()
}
},
componentDidMount() {
this.refs.radialGraph.zoom(1)
this.refs.radialGraph.pan(this.props.width / 2, this.props.height / 2)
},
componentWillReceiveProps(nextProps) {
if (
nextProps.width !== this.props.width ||
nextProps.height !== this.props.height ||
nextProps.graphData !== this.props.graphData
) {
this.setState({ radialNodes: this._genRadialNodes() })
}
},
render() {
return (
<CytoscapeGraph
ref="radialGraph"
nodes={this.state.radialNodes}
edges={this.state.edges}
width={this.props.width}
height={this.props.height}
onZoomOrPan={this._onZoomOrPan}
onNodeHover={this.props.onAsHover}
onNodeClick={this.props.onAsClick}
/>
)
},
_genRadialNodes() {
const rThis = this
const maxR =
Math.min(this.props.width, this.props.height) / 2 - this.props.margin
const maxConeSize = Math.max.apply(
null,
this.props.graphData.ases.map(asNode => asNode.customerConeSize)
)
return this.props.graphData.ases.map(node => {
const radius = rThis._getRadius(node.customerConeSize, maxConeSize)
console.log('radius from _genRadialNodes', radius)
return {
// Convert to radial coords
id: node.asn,
x: maxR * radius * Math.cos(-node.lon * Math.PI / 180),
y: maxR * radius * Math.sin(-node.lon * Math.PI / 180),
nodeData: node
}
})
},
_getEdges() {
const customerCones = {}
const maxConeSize = Math.max.apply(
null,
this.props.graphData.ases.map(asNode => {
customerCones[asNode.asn] = asNode.customerConeSize
return asNode.customerConeSize
})
)
return this.props.graphData.relationships.map(rel => {
if (!rel.hasOwnProperty('customerConeSize')) {
rel.customerConeSize = Math.min(
customerCones[rel.src],
customerCones[rel.dst]
)
}
return {
src: rel.src,
dst: rel.dst,
color: Colors.valueRgb(rel.customerConeSize, maxConeSize),
opacity: Colors.valueOpacity(rel.customerConeSize, maxConeSize)
}
})
},
_getRadius(coneSize, maxConeSize) {
// 0<=result<=1
return (
(Math.log(maxConeSize) - Math.log(coneSize)) /
(Math.log(maxConeSize) - Math.log(1)) *
0.99 +
0.01
)
},
_onZoomOrPan(zoom, panX, panY) {
const r = Math.min(this.props.width, this.props.height) / 2
const offsetX = -(panX - this.props.width / 2) / zoom / r
const offsetY = -(panY - this.props.height / 2) / zoom / r
const offsetR = Math.sqrt(Math.pow(offsetX, 2) + Math.pow(offsetY, 2))
let offsetAng = offsetR
? -Math.acos(offsetX / offsetR) / Math.PI * 180
: 0
const zoomRadius = 1 / zoom
if (offsetY < 0) {
// Complementary angle
offsetAng = 360 - offsetAng
}
this.props.onRadialViewportChange(zoomRadius, offsetR, offsetAng)
}
})
}
)
////
function graphRandomGenerator(nNodes, nEdges) {
const nodes = []
const edges = []
nNodes = Math.max(nNodes, 1)
nEdges = Math.abs(nEdges)
while (nEdges--) {
edges.push({
src: Math.round((nNodes - 1) * Math.random()),
dst: Math.round((nNodes - 1) * Math.random()),
type: 'peer'
})
}
while (nNodes--) {
nodes.push({
asn: nNodes,
customerConeSize: Math.random(),
lat: Math.random() * 180 - 90,
lon: Math.random() * 360 - 180
//x: Math.random(),
//y: Math.random()
})
}
return {
ases: nodes,
relationships: edges
}
}
define([], function() {
return [
{
population: '417910',
country: 'NZ',
city: 'Auckland',
lat: '-36.86667',
name: 'Auckland, NZ',
lon: '174.76667'
},
{
population: '363926',
country: 'NZ',
city: 'Christchurch',
lat: '-43.53333',
name: 'Christchurch, NZ',
lon: '172.63333'
},
{
population: '187282',
country: 'RU',
city: 'Petropavlovsk-kamchatsky',
lat: '53.04444',
name: 'Petropavlovsk-kamchatsky, RU',
lon: '158.65076'
},
{
population: '4627345',
country: 'AU',
city: 'Sydney',
lat: '-33.86785',
name: 'Sydney, AU',
lon: '151.20732'
},
{
population: '367752',
country: 'AU',
city: 'Canberra',
lat: '-35.28346',
name: 'Canberra, AU',
lon: '149.12807'
},
{
population: '4246375',
country: 'AU',
city: 'Melbourne',
lat: '-37.814',
name: 'Melbourne, AU',
lon: '144.96332'
},
{
population: '1883027',
country: 'JP',
city: 'Sapporo',
lat: '43.06667',
name: 'Sapporo, JP',
lon: '141.35'
},
{
population: '8336599',
country: 'JP',
city: 'Tokyo',
lat: '35.6895',
name: 'Tokyo, JP',
lon: '139.69171'
},
{
population: '2592413',
country: 'JP',
city: 'Osaka',
lat: '34.69374',
name: 'Osaka, JP',
lon: '135.50218'
},
{
population: '1392289',
country: 'JP',
city: 'Fukuoka',
lat: '33.6',
name: 'Fukuoka, JP',
lon: '130.41667'
},
{
population: '10349312',
country: 'KR',
city: 'Seoul',
lat: '37.566',
name: 'Seoul, KR',
lon: '126.9784'
},
{
population: '6255921',
country: 'CN',
city: 'Shenyang',
lat: '41.79222',
name: 'Shenyang, CN',
lon: '123.43278'
},
{
population: '22315474',
country: 'CN',
city: 'Shanghai',
lat: '31.22222',
name: 'Shanghai, CN',
lon: '121.45806'
},
{
population: '11716620',
country: 'CN',
city: 'Beijing',
lat: '39.9075',
name: 'Beijing, CN',
lon: '116.39723'
},
{
population: '11071424',
country: 'CN',
city: 'Guangzhou',
lat: '23.11667',
name: 'Guangzhou, CN',
lon: '113.25'
},
{
population: '6501190',
country: 'CN',
city: "Xi'an",
lat: '34.25833',
name: "Xi'an, CN",
lon: '108.92861'
},
{
population: '8540121',
country: 'ID',
city: 'Jakarta',
lat: '-6.21462',
name: 'Jakarta, ID',
lon: '106.84513'
},
{
population: '7415590',
country: 'CN',
city: 'Chengdu',
lat: '30.66667',
name: 'Chengdu, CN',
lon: '104.06667'
},
{
population: '5104476',
country: 'TH',
city: 'Bangkok',
lat: '13.75398',
name: 'Bangkok, TH',
lon: '100.50144'
},
{
population: '4477638',
country: 'MM',
city: 'Yangon',
lat: '16.80528',
name: 'Yangon, MM',
lon: '96.15611'
},
{
population: '10356500',
country: 'BD',
city: 'Dhaka',
lat: '23.7104',
name: 'Dhaka, BD',
lon: '90.40744'
},
{
population: '4631392',
country: 'IN',
city: 'Kolkata',
lat: '22.56263',
name: 'Kolkata, IN',
lon: '88.36304'
},
{
population: '1599920',
country: 'IN',
city: 'Patna',
lat: '25.61538',
name: 'Patna, IN',
lon: '85.10103'
},
{
population: '4328063',
country: 'IN',
city: 'Chennai',
lat: '13.08784',
name: 'Chennai, IN',
lon: '80.27847'
},
{
population: '10927986',
country: 'IN',
city: 'Delhi',
lat: '28.65195',
name: 'Delhi, IN',
lon: '77.23149'
},
{
population: '12691836',
country: 'IN',
city: 'Mumbai',
lat: '19.07283',
name: 'Mumbai, IN',
lon: '72.88261'
},
{
population: '3043532',
country: 'AF',
city: 'Kabul',
lat: '34.52813',
name: 'Kabul, AF',
lon: '69.17233'
},
{
population: '11624219',
country: 'PK',
city: 'Karachi',
lat: '24.9056',
name: 'Karachi, PK',
lon: '67.0822'
},
{
population: '1062919',
country: 'RU',
city: 'Chelyabinsk',
lat: '55.15402',
name: 'Chelyabinsk, RU',
lon: '61.42915'
},
{
population: '2307177',
country: 'IR',
city: 'Mashhad',
lat: '36.31559',
name: 'Mashhad, IR',
lon: '59.56796'
},
{
population: '1137347',
country: 'AE',
city: 'Dubai',
lat: '25.0657',
name: 'Dubai, AE',
lon: '55.17128'
},
{
population: '44099591',
country: 'IR',
city: 'Godarzi It',
lat: '35.73328',
name: 'Godarzi It, IR',
lon: '51.30714'
},
{
population: '2600000',
country: 'IQ',
city: 'Basrah',
lat: '30.50852',
name: 'Basrah, IQ',
lon: '47.7804'
},
{
population: '7216000',
country: 'IQ',
city: 'Baghdad',
lat: '33.34058',
name: 'Baghdad, IQ',
lon: '44.40088'
},
{
population: '2065597',
country: 'IQ',
city: 'Al Mawsil Al Jadidah',
lat: '36.33306',
name: 'Al Mawsil Al Jadidah, IQ',
lon: '43.1049'
},
{
population: '10381222',
country: 'RU',
city: 'Moscow',
lat: '55.75222',
name: 'Moscow, RU',
lon: '37.61556'
},
{
population: '3517182',
country: 'TR',
city: 'Ankara',
lat: '39.91987',
name: 'Ankara, TR',
lon: '32.85427'
},
{
population: '11174257',
country: 'TR',
city: 'Istanbul',
lat: '41.01384',
name: 'Istanbul, TR',
lon: '28.94966'
},
{
population: '2500603',
country: 'TR',
city: 'Izmir',
lat: '38.41273',
name: 'Izmir, TR',
lon: '27.13838'
},
{
population: '1152556',
country: 'BG',
city: 'Sofia',
lat: '42.69751',
name: 'Sofia, BG',
lon: '23.32415'
},
{
population: '3433441',
country: 'ZA',
city: 'Cape Town',
lat: '-33.92584',
name: 'Cape Town, ZA',
lon: '18.42322'
},
{
population: '7785965',
country: 'CD',
city: 'Kinshasa',
lat: '-4.32758',
name: 'Kinshasa, CD',
lon: '15.31357'
},
{
population: '3426354',
country: 'DE',
city: 'Berlin',
lat: '52.52437',
name: 'Berlin, DE',
lon: '13.41053'
},
{
population: '3626068',
country: 'NG',
city: 'Kano',
lat: '12.00012',
name: 'Kano, NG',
lon: '8.51672'
},
{
population: '3565108',
country: 'NG',
city: 'Ibadan',
lat: '7.37756',
name: 'Ibadan, NG',
lon: '3.90591'
},
{
population: '9000000',
country: 'NG',
city: 'Lagos',
lat: '6.45407',
name: 'Lagos, NG',
lon: '3.39467'
},
{
population: '3677115',
country: 'CI',
city: 'Abidjan',
lat: '5.30966',
name: 'Abidjan, CI',
lon: '-4.01266'
},
{
population: '3144909',
country: 'MA',
city: 'Casablanca',
lat: '33.58831',
name: 'Casablanca, MA',
lon: '-7.61138'
},
{
population: '1871242',
country: 'GN',
city: 'Camayenne',
lat: '9.535',
name: 'Camayenne, GN',
lon: '-13.68778'
},
{
population: '2476400',
country: 'SN',
city: 'Dakar',
lat: '14.6937',
name: 'Dakar, SN',
lon: '-17.44406'
},
{
population: '118918',
country: 'IS',
city: 'Reykjavik',
lat: '64.13548',
name: 'Reykjavik, IS',
lon: '-21.89541'
},
{
population: '1478098',
country: 'BR',
city: 'Recife',
lat: '-8.05389',
name: 'Recife, BR',
lon: '-34.88111'
},
{
population: '2711840',
country: 'BR',
city: 'Salvador',
lat: '-12.97111',
name: 'Salvador, BR',
lon: '-38.51083'
},
{
population: '744512',
country: 'BR',
city: 'Teresina',
lat: '-5.08917',
name: 'Teresina, BR',
lon: '-42.80194'
},
{
population: '10021295',
country: 'BR',
city: 'Sao Paulo',
lat: '-23.5475',
name: 'Sao Paulo, BR',
lon: '-46.63611'
},
{
population: '2207718',
country: 'BR',
city: 'Brasilia',
lat: '-15.77972',
name: 'Brasilia, BR',
lon: '-47.92972'
},
{
population: '1718421',
country: 'BR',
city: 'Curitiba',
lat: '-25.42778',
name: 'Curitiba, BR',
lon: '-49.27306'
},
{
population: '471832',
country: 'BR',
city: 'Londrina',
lat: '-23.31028',
name: 'Londrina, BR',
lon: '-51.16278'
},
{
population: '1372741',
country: 'BR',
city: 'Porto Alegre',
lat: '-30.03306',
name: 'Porto Alegre, BR',
lon: '-51.23'
},
{
population: '729151',
country: 'BR',
city: 'Campo Grande',
lat: '-20.44278',
name: 'Campo Grande, BR',
lon: '-54.64639'
},
{
population: '1270737',
country: 'UY',
city: 'Montevideo',
lat: '-34.90328',
name: 'Montevideo, UY',
lon: '-56.18816'
},
{
population: '13076300',
country: 'AR',
city: 'Buenos Aires',
lat: '-34.61315',
name: 'Buenos Aires, AR',
lon: '-58.37723'
},
{
population: '1598210',
country: 'BR',
city: 'Manaus',
lat: '-3.10194',
name: 'Manaus, BR',
lon: '-60.025'
},
{
population: '1364389',
country: 'BO',
city: 'Santa Cruz De La Sierra',
lat: '-17.78629',
name: 'Santa Cruz De La Sierra, BO',
lon: '-63.18117'
},
{
population: '1428214',
country: 'AR',
city: 'Cordoba',
lat: '-31.4135',
name: 'Cordoba, AR',
lon: '-64.18105'
},
{
population: '3000000',
country: 'VE',
city: 'Caracas',
lat: '10.48801',
name: 'Caracas, VE',
lon: '-66.87919'
},
{
population: '1754256',
country: 'VE',
city: 'Maracay',
lat: '10.23535',
name: 'Maracay, VE',
lon: '-67.59113'
},
{
population: '4837295',
country: 'CL',
city: 'Santiago',
lat: '-33.45694',
name: 'Santiago, CL',
lon: '-70.64827'
},
{
population: '2225000',
country: 'VE',
city: 'Maracaibo',
lat: '10.66663',
name: 'Maracaibo, VE',
lon: '-71.61245'
},
{
population: '8175133',
country: 'US',
city: 'New York City',
lat: '40.71427',
name: 'New York City, US',
lon: '-74.00597'
},
{
population: '7674366',
country: 'CO',
city: 'Bogota',
lat: '4.60971',
name: 'Bogota, CO',
lon: '-74.08175'
},
{
population: '3000000',
country: 'JM',
city: "Fitzroy's Elecctronics",
lat: '17.98234',
name: "Fitzroy's Elecctronics, JM",
lon: '-76.86918'
},
{
population: '7737002',
country: 'PE',
city: 'Lima',
lat: '-12.04318',
name: 'Lima, PE',
lon: '-77.02824'
},
{
population: '2600000',
country: 'CA',
city: 'Toronto',
lat: '43.70011',
name: 'Toronto, CA',
lon: '-79.4163'
},
{
population: '2163824',
country: 'CU',
city: 'Havana',
lat: '23.13302',
name: 'Havana, CU',
lon: '-82.38304'
},
{
population: '829718',
country: 'US',
city: 'Indianapolis',
lat: '39.76838',
name: 'Indianapolis, US',
lon: '-86.15804'
},
{
population: '973087',
country: 'NI',
city: 'Managua',
lat: '12.13282',
name: 'Managua, NI',
lon: '-86.2504'
},
{
population: '850848',
country: 'HN',
city: 'Tegucigalpa',
lat: '14.0818',
name: 'Tegucigalpa, HN',
lon: '-87.20681'
},
{
population: '2695598',
country: 'US',
city: 'Chicago',
lat: '41.85003',
name: 'Chicago, US',
lon: '-87.65005'
},
{
population: '646889',
country: 'US',
city: 'Memphis',
lat: '35.14953',
name: 'Memphis, US',
lon: '-90.04898'
},
{
population: '994938',
country: 'GT',
city: 'Guatemala City',
lat: '14.64072',
name: 'Guatemala City, GT',
lon: '-90.51327'
},
{
population: '2099451',
country: 'US',
city: 'Houston',
lat: '29.76328',
name: 'Houston, US',
lon: '-95.36327'
},
{
population: '1197816',
country: 'US',
city: 'Dallas',
lat: '32.78306',
name: 'Dallas, US',
lon: '-96.80667'
},
{
population: '1820888',
country: 'MX',
city: 'Iztapalapa',
lat: '19.35738',
name: 'Iztapalapa, MX',
lon: '-99.0671'
},
{
population: '12294193',
country: 'MX',
city: 'Mexico City',
lat: '19.42847',
name: 'Mexico City, MX',
lon: '-99.12766'
},
{
population: '1114626',
country: 'MX',
city: 'Leon',
lat: '21.13052',
name: 'Leon, MX',
lon: '-101.671'
},
{
population: '1640589',
country: 'MX',
city: 'Guadalajara',
lat: '20.66682',
name: 'Guadalajara, MX',
lon: '-103.39182'
},
{
population: '809232',
country: 'MX',
city: 'Chihuahua',
lat: '28.63528',
name: 'Chihuahua, MX',
lon: '-106.08889'
},
{
population: '1512354',
country: 'MX',
city: 'Ciudad Juarez',
lat: '31.73333',
name: 'Ciudad Juarez, MX',
lon: '-106.48333'
},
{
population: '520116',
country: 'US',
city: 'Tucson',
lat: '32.22174',
name: 'Tucson, US',
lon: '-110.92648'
},
{
population: '595811',
country: 'MX',
city: 'Hermosillo',
lat: '29.1026',
name: 'Hermosillo, MX',
lon: '-110.97732'
},
{
population: '1445632',
country: 'US',
city: 'Phoenix',
lat: '33.44838',
name: 'Phoenix, US',
lon: '-112.07404'
},
{
population: '1019942',
country: 'CA',
city: 'Calgary',
lat: '51.05011',
name: 'Calgary, CA',
lon: '-114.08529'
},
{
population: '1376457',
country: 'MX',
city: 'Tijuana',
lat: '32.5027',
name: 'Tijuana, MX',
lon: '-117.00371'
},
{
population: '3792621',
country: 'US',
city: 'Los Angeles',
lat: '34.05223',
name: 'Los Angeles, US',
lon: '-118.24368'
},
{
population: '945942',
country: 'US',
city: 'San Jose',
lat: '37.33939',
name: 'San Jose, US',
lon: '-121.89496'
},
{
population: '608660',
country: 'US',
city: 'Seattle',
lat: '47.60621',
name: 'Seattle, US',
lon: '-122.33207'
},
{
population: '805235',
country: 'US',
city: 'San Francisco',
lat: '37.77493',
name: 'San Francisco, US',
lon: '-122.41942'
},
{
population: '600000',
country: 'CA',
city: 'Vancouver',
lat: '49.24966',
name: 'Vancouver, CA',
lon: '-123.11934'
},
{
population: '291826',
country: 'US',
city: 'Anchorage',
lat: '61.21806',
name: 'Anchorage, US',
lon: '-149.90028'
},
{
population: '371657',
country: 'US',
city: 'Honolulu',
lat: '21.30694',
name: 'Honolulu, US',
lon: '-157.85833'
}
]
})
define([], () => {
function valueOpacity(value, value_max) {
if (value == null) {
value = 1
}
return Math.log(value) / Math.log(value_max) * 0.6 + 0.3
}
function valueRgb(value, value_max) {
if (value == null) {
value = 1
} else if (value <= 0.000001) {
value = 0.000001
}
const temp = Math.log(value) / Math.log(value_max);
//var hue = (360*(4+5+temp/8))%360;
const hue = temp * 0.7 + 0.5; //value;
const sat = 1;
const bri = 1;
// return "hsl("+hue+",100%,100%)";
// bri = .80*temp+.20;
//var color = "hsl("+hue+","+(100*sat)+"%,"+(100*bri)+"%)";
const rgb = hsvRgb(hue, sat, bri);
for (let i = 0; i < 3; i++) {
rgb[i] = rgb[i].toString(16)
while (rgb[i].length < 2) {
rgb[i] = `0${rgb[i]}`
}
}
const color = `#${rgb[0]}${rgb[1]}${rgb[2]}`;
return color
}
function hsvRgb(hue, sat, bri) {
let h = hue;
let s = sat;
let v = bri;
let r;
let g;
let b;
let i;
let f;
let p;
let q;
let t;
if (arguments.length === 1) {
;(s = h.s), (v = h.v), (h = h.h)
}
i = Math.floor(h * 6)
f = h * 6 - i
p = v * (1 - s)
q = v * (1 - f * s)
t = v * (1 - (1 - f) * s)
switch (i % 6) {
case 0:
;(r = v), (g = t), (b = p)
break
case 1:
;(r = q), (g = v), (b = p)
break
case 2:
;(r = p), (g = v), (b = t)
break
case 3:
;(r = p), (g = q), (b = v)
break
case 4:
;(r = t), (g = p), (b = v)
break
case 5:
;(r = v), (g = p), (b = q)
break
}
const rgb = [Math.round(r * 255), Math.round(g * 255), Math.round(b * 255)];
return rgb
}
return {
valueRgb,
valueOpacity
}
})
define(
['react', 'react-dom', 'jsx!asgraph', 'jsx!polar-layout'],
(React, ReactDOM, AsGraph, PolarLayout, _) => {
const Controls = React.createClass({
render() {
return null
return (
<div
style={{
margin: 10
}}
>
this.props.search
<input
type="text"
size="20"
value={this.props.search}
placeholder="Search for an AS"
onChange={this._handleSearchUpdate}
/>
</div>
)
},
_handleSearchUpdate(event) {
this.props.onSearchChange(event.target.value)
}
})
const Logger = React.createClass({
render() {
const asInfo = this.props.asInfo
if (!asInfo) {
return null
}
return (
<div
style={{
backgroundColor: 'lightgray',
margin: 5,
padding: 5,
borderRadius: 5
}}
>
AS: <b>{asInfo.id}</b>
<br />
<div style={{ 'text-overflow': 'ellipsis', width: '300' }}>
(<i>
{asInfo.orgName} - {asInfo.country}
</i>)
</div>
Customer Cone: <b>{asInfo.customerConeSize}</b> AS
{asInfo.customerConeSize > 1 ? 'es' : ''}
<br />
Rank: <b>{asInfo.rank}</b>
<br />
<table
border="1"
style={{
'font-size': '50%',
'text-align': 'center'
}}
>
<tr>
<td
rowSpan="2"
style={{
'font-size': '200%',
'v-align': 'center'
}}
>
{' '}Degree:{' '}
</td>
<td>
<b>
{' '}{asInfo.degreeProvider}
</b>
</td>
<td>
<b>
{' '}{asInfo.degreePeer}
</b>
</td>
<td>
<b>
{' '}{asInfo.degreeCustomer}
</b>
</td>
</tr>
<tr>
<td>provider</td>
<td>peer</td>
<td>customer</td>
</tr>
</table>
</div>
)
}
})
return React.createClass({
getInitialState() {
return {
width: null, //window.innerWidth,
height: null, //window.innerHeight - 20,
layoutMargin: 30,
offsetAngle: 0,
offsetRadius: 0,
zoomRadius: 1,
srcHighlight: null,
dstHighlight: null,
selectedAsInfo: null
}
},
componentWillMount() {
this._setSize()
window.addEventListener('resize', this._setSize)
},
componentWillUnmount() {
window.removeEventListener('resize', this._setSize)
},
render() {
const baseOuterMargin = 70
const pointDiameter = 4
const outerMargin = baseOuterMargin + pointDiameter
return (
<div style={{ position: 'relative' }}>
<div style={{ position: 'absolute' }}>
<AsGraph
graphData={this.props.data}
width={this.state.width}
height={this.state.height}
margin={this.state.layoutMargin + outerMargin}
selectedAs={this.state.srcHighlight}
onRadialViewportChange={this._onRadialViewportChange}
onAsHover={this._onAsHover}
onAsClick={this._onAsClick}
/>
</div>
<div
style={{
position: 'absolute',
pointerEvents: 'none'
}}
>
<PolarLayout
width={this.state.width}
height={this.state.height}
margin={this.state.layoutMargin}
bckgColor="#001"
zoomCenter={[this.state.offsetRadius, this.state.offsetAngle]}
zoomRadius={this.state.zoomRadius}
/>
</div>
<div
style={{
right: 0,
position: 'absolute'
}}
>
<Controls
search={this.state.srcHighlight}
onSearchChange={this._onSrcChange}
/>
</div>
<div
style={{
left: 0,
top: this.state.height - 120,
position: 'absolute'
}}
>
<Logger asInfo={this.state.selectedAsInfo} />
</div>
</div>
)
},
_setSize() {
this.setState({
width: window.innerWidth,
height: window.innerHeight - 20
})
},
_onAsHover(asInfo) {
this.setState({ selectedAsInfo: asInfo })
},
_onAsClick(asInfo) {
this.setState({ srcHighlight: asInfo.id })
},
_onSrcChange(src) {
this.setState({ srcHighlight: src })
},
_onRadialViewportChange(zoom, offsetR, offsetAngle) {
this.setState({
offsetRadius: offsetR,
offsetAngle,
zoomRadius: zoom
})
}
})
}
)
const fs = require('fs')
const execSync = require('child_process').execSync
const path = './'
const files = fs.readdirSync(path, {})
// js
const jsFiles = files
.filter(
file => file.substr(file.length - 2) === 'js' // ||
// file.substr(file.length - 3) === 'jsx'
)
.filter(file => file !== 'lebab.js' && file !== 'cities.js')
console.log('jsFiles', jsFiles)
// jsx
const jsxFiles = files.filter(file => file.substr(file.length - 3) === 'jsx')
console.log('jsxFiles', jsxFiles)
// call lebab.sh for each js file
let command
jsFiles.forEach(file => {
command = `sh lebab.sh ${file}`
console.log('current command', command)
execSync(command)
})
// call lebab.sh for each jsx file
let command
jsxFiles.forEach(file => {
command = `sh lebab.sh ${file}`
console.log('current command', command)
execSync(command)
})
FILE=$1
# safe
lebab --replace $FILE --transform arrow
lebab --replace $FILE --transform for-of
lebab --replace $FILE --transform for-each
lebab --replace $FILE --transform arg-rest
lebab --replace $FILE --transform arg-spread
lebab --replace $FILE --transform obj-method
lebab --replace $FILE --transform obj-shorthand
lebab --replace $FILE --transform multi-var
# unsafe
lebab --replace $FILE --transform let
lebab --replace $FILE --transform template
define(['cities'], locs => locs
.map(loc => ({
text: `${loc.city} (${loc.country})`,
angle: loc.lon,
weight: +loc.population
}))
.sort((a, b) => b.weight - a.weight))
body {
padding: 0;
font-family: Sans-Serif;
font-size: 80%;
background-color: #001;
}
.d3-tooltip {
color: lightgrey;
background: rgba(0, 0, 100, .7);
padding: 5px;
border-radius: 3px;
font: 11.5px sans-serif;
text-align: center;
}
.fade-enter,
.fade-leave.fade-leave-active {
opacity: 0.01;
}
.fade-leave,
.fade-enter.fade-enter-active {
opacity: 1;
}
.fade-enter,
.fade-leave {
transition: opacity .35s ease-in;
}
define(
['react', 'react-dom', 'triangle-solver', 'locations'],
(React, ReactDOM, solveTriangle, locations) => {
const DirectionMarker = React.createClass({
propTypes: {
length: React.PropTypes.number.isRequired,
padding: React.PropTypes.number,
angle: React.PropTypes.number.isRequired,
text: React.PropTypes.string
},
getDefaultProps() {
return {
padding: 0,
text: ''
}
},
render() {
const txtX =
(this.props.padding + this.props.length / 2) *
Math.cos(this.props.angle)
const txtY =
(this.props.padding + this.props.length / 2) *
Math.sin(this.props.angle)
let txtRotate = this.props.angle * 180 / Math.PI
while (txtRotate >= 180) {
txtRotate -= 360
}
while (txtRotate < -180) {
txtRotate += 360
}
txtRotate += txtRotate > 0 ? -90 : 90
return (
<text
x={txtX}
y={txtY}
fontSize={this.props.length * 0.6}
fontFamily="Sans-serif"
fill="lightgrey"
textAnchor="middle"
transform={`rotate(${txtRotate} ${txtX},${txtY})`}
style={{ alignmentBaseline: 'central' }}
>
{this.props.text}
</text>
)
}
})
const RadialLabels = React.createClass({
const: {
TEXT_HEIGHT_RADIUS_RATIO: 0.025
},
propTypes: {
layoutRadius: React.PropTypes.number.isRequired
},
render() {
const props = this.props
const textHeight =
this.const.TEXT_HEIGHT_RADIUS_RATIO * this.props.layoutRadius
const // Small-angle approx
textHeightInAngles =
Math.atan(textHeight / this.props.layoutRadius) * 180 / Math.PI
const anglesCarry = []
const opacities = []
this.props.labels.forEach(label => {
const closestAngle = anglesCarry.reduce(
(carry, angleCarry) =>
Math.min(carry, Math.abs(label.angle - angleCarry)),
Infinity
)
anglesCarry.push(label.angle)
opacities.push(
Math.pow(Math.min(1, closestAngle / textHeightInAngles), 6)
) // Exponential fade
})
return (
<g>
{this.props.labels.map((label, idx) => {
// Normalize angle
while (label.angle >= 180) {
label.angle -= 360
}
while (label.angle < -180) {
label.angle += 360
}
const txtX =
props.layoutRadius * Math.cos(label.angle * Math.PI / 180)
const txtY =
props.layoutRadius * Math.sin(label.angle * Math.PI / 180)
const txtRotate =
label.angle + (Math.abs(label.angle) < 90 ? 0 : 180)
return (
<text
x={txtX}
y={txtY}
fontSize={textHeight}
fontFamily="Sans-serif"
fill="lightgrey"
textAnchor={Math.abs(label.angle) < 90 ? 'start' : 'end'}
transform={`rotate(${txtRotate} ${txtX},${txtY})`}
style={{
alignmentBaseline: 'central',
fillOpacity: opacities[idx] * 0.8
}}
>
{label.text}
</text>
)
})}
</g>
)
}
})
const GraticuleGrid = React.createClass({
render() {
const props = this.props
return (
<g>
{Array(
...{
length: props.nRadialLines
}
).map((_, idx) => {
const angle = idx * 360 / props.nRadialLines * Math.PI / 180
return (
<line
x1={0}
y1={0}
x2={props.radius * Math.cos(angle)}
y2={props.radius * Math.sin(angle)}
stroke="lightgrey"
strokeWidth="1"
style={{
fillOpacity: 0,
vectorEffect: 'non-scaling-stroke'
}}
/>
)
})}
{Array(
...{
length: props.nConcentricLines
}
).map((_, idx) =>
<circle
r={props.radius * (idx + 1) / (props.nConcentricLines + 1)}
stroke="lightgrey"
strokeWidth="1"
style={{
fillOpacity: 0,
vectorEffect: 'non-scaling-stroke'
}}
/>
)}
</g>
)
}
})
return React.createClass({
propTypes: {
radius: React.PropTypes.number.isRequired,
margin: React.PropTypes.number.isRequired,
zoomCenter: React.PropTypes.arrayOf(React.PropTypes.number),
zoomRadius: React.PropTypes.number
},
getDefaultProps() {
return {
zoomCenter: [0, 0], // radial, angle
zoomRadius: 1,
bckgColor: 'white'
}
},
getInitialState() {
return {
cardinalPoints: {
S: 90,
SE: 45,
E: 0,
NE: -45,
N: -90,
NW: -135,
W: 180,
SW: 135
}
}
},
render() {
const rThis = this
const outerMargin = 70
const radius =
Math.min(this.props.width, this.props.height) / 2 -
this.props.margin -
outerMargin
return (
<svg
width={this.props.width}
height={this.props.height}
style={{ margin: 'auto', display: 'block' }}
>
<g
transform={`translate(${this.props.width / 2},${this.props
.height / 2})`}
>
<g
transform={`scale(${1 /
rThis.props.zoomRadius}) translate(${-radius *
rThis.props.zoomCenter[0] *
Math.cos(
-rThis.props.zoomCenter[1] * Math.PI / 180
)},${-radius *
rThis.props.zoomCenter[0] *
Math.sin(-rThis.props.zoomCenter[1] * Math.PI / 180)})`}
>
<GraticuleGrid
nConcentricLines={
Math.max(
2,
Math.pow(
2,
Math.round(Math.log(6 / this.props.zoomRadius))
)
) - 1
}
nRadialLines={Math.max(
4,
Math.pow(2, Math.round(Math.log(6 / this.props.zoomRadius)))
)}
radius={radius}
/>
</g>
{/* Semi-transparent window */}
<circle
r={radius + 500}
stroke={this.props.bckgColor}
strokeWidth="1000"
strokeOpacity="0.7"
fillOpacity="0"
/>
{/* Longitude ring (1/2 margin) */}
<circle
r={radius + this.props.margin / 4}
stroke="#3182bd"
strokeWidth={this.props.margin / 2}
strokeOpacity="0.6"
fillOpacity="0"
/>
<g>
{this._getLongitudeLabels().map(label =>
<DirectionMarker
length={rThis.props.margin / 2}
padding={radius}
angle={label.angle}
text={label.text}
/>
)}
</g>
<RadialLabels
layoutRadius={radius + rThis.props.margin * 0.65}
labels={locations.map(city => ({
text: city.text,
angle: rThis._getProjectedAngle(city.angle)
}))}
/>
</g>
</svg>
)
},
_getLongitudeLabels() {
const rThis = this
const longCoords = [0, 45, 90, 135, 180, -45, -90, -135]
const labels = []
labels.push(
...longCoords.map(longCoord => ({
text: `${longCoord}°`,
angle: rThis._getProjectedAngle(longCoord) * Math.PI / 180
}))
)
return labels
},
_getProjectedAngle(angle) {
if (!this.props.zoomCenter[0]) return -angle // Right in the center, direct projection
let knownAngle = this.props.zoomCenter[1] - angle
while (knownAngle > 180) knownAngle -= 360
while (knownAngle < -180) knownAngle += 360
const neg = knownAngle < 0
knownAngle = Math.abs(knownAngle)
if (knownAngle == 0 || knownAngle == 180) return angle // Zooming in the exact direction of the angle
// known angle A, side B (full radius=1), side C (zoom radius)
const res = solveTriangle(
null,
this.props.zoomCenter[0],
1,
knownAngle,
null,
null
)
return (180 - res[5]) * (neg ? -1 : 1) - this.props.zoomCenter[1] // Use angle C
}
})
}
)
require.config({
paths: {
babel: '//cdnjs.cloudflare.com/ajax/libs/babel-core/5.8.34/browser.min',
jsx: '//cdn.rawgit.com/podio/requirejs-react-jsx/v1.0.2/jsx',
text: '//cdnjs.cloudflare.com/ajax/libs/require-text/2.0.12/text.min',
react:
'//cdnjs.cloudflare.com/ajax/libs/react/0.14.6/react-with-addons.min',
'react-dom': '//cdnjs.cloudflare.com/ajax/libs/react/0.14.6/react-dom.min',
'react-bootstrap':
'//cdnjs.cloudflare.com/ajax/libs/react-bootstrap/0.28.1/react-bootstrap.min',
jquery: '//ajax.googleapis.com/ajax/libs/jquery/2.1.4/jquery.min',
bootstrap: '//maxcdn.bootstrapcdn.com/bootstrap/3.3.6/js/bootstrap.min',
underscore:
'//cdnjs.cloudflare.com/ajax/libs/underscore.js/1.8.3/underscore-min',
d3: '//cdnjs.cloudflare.com/ajax/libs/d3/3.5.12/d3.min',
cytoscape: '//cdnjs.cloudflare.com/ajax/libs/cytoscape/2.5.5/cytoscape.min',
'cytoscape-panzoom':
'//cdnjs.cloudflare.com/ajax/libs/cytoscape-panzoom/2.2.0/cytoscape-panzoom.min'
},
shim: {
bootstrap: ['jquery'],
'cytoscape-panzoom': ['jquery']
},
config: {
babel: {
fileExtension: '.jsx'
}
}
})
define([], () => {
// Given some sides and angles, this returns a tuple of 8 number/string values.
// angle A is opposite to side a, etc
// angles in degrees (0-360)
function solveTriangle(a, b, c, A, B, C) {
const sides = (a != null) + (b != null) + (c != null); // Boolean to integer conversion
const angles = (A != null) + (B != null) + (C != null); // Boolean to integer conversion
let area;
let status;
if (sides + angles != 3) throw 'Give exactly 3 pieces of information'
else if (sides == 0) throw 'Give at least one side length'
else if (sides == 3) {
status = 'Side side side (SSS) case'
if (lessEqual(a + b, c) || lessEqual(b + c, a) || lessEqual(c + a, b))
throw `${status} - No solution`
A = solveAngle(b, c, a)
B = solveAngle(c, a, b)
C = solveAngle(a, b, c)
// Heron's formula
const s = (a + b + c) / 2;
area = Math.sqrt(s * (s - a) * (s - b) * (s - c))
} else if (angles == 2) {
status = 'Angle side angle (ASA) case'
// Find missing angle
if (A == null) A = 180 - B - C
if (B == null) B = 180 - C - A
if (C == null) C = 180 - A - B
if (lessEqual(A, 0) || lessEqual(B, 0) || lessEqual(C, 0))
throw `${status} - No solution`
const sinA = Math.sin(degToRad(A));
const sinB = Math.sin(degToRad(B));
const sinC = Math.sin(degToRad(C));
// Use law of sines to find sides
var ratio // side / sin(angle)
if (a != null) {
ratio = a / sinA
area = a * ratio * sinB * sinC / 2
}
if (b != null) {
ratio = b / sinB
area = b * ratio * sinC * sinA / 2
}
if (c != null) {
ratio = c / sinC
area = c * ratio * sinA * sinB / 2
}
if (a == null) a = ratio * sinA
if (b == null) b = ratio * sinB
if (c == null) c = ratio * sinC
} else if (
and(A != null, a == null) ||
and(B != null, b == null) ||
and(C != null, c == null)
) {
status = 'Side angle side (SAS) case'
if (
and(A != null, A >= 180) ||
and(B != null, B >= 180) ||
and(C != null, C >= 180)
)
throw `${status} - No solution`
if (a == null) a = solveSide(b, c, A)
if (b == null) b = solveSide(c, a, B)
if (c == null) c = solveSide(a, b, C)
if (A == null) A = solveAngle(b, c, a)
if (B == null) B = solveAngle(c, a, b)
if (C == null) C = solveAngle(a, b, c)
if (A != null) area = b * c * Math.sin(degToRad(A)) / 2
if (B != null) area = c * a * Math.sin(degToRad(B)) / 2
if (C != null) area = a * b * Math.sin(degToRad(C)) / 2
} else {
status = 'Side side angle (SSA) case - '
let knownSide;
let knownAngle;
let partialSide;
if (and(a != null, A != null)) {
knownSide = a
knownAngle = A
}
if (and(b != null, B != null)) {
knownSide = b
knownAngle = B
}
if (and(c != null, C != null)) {
knownSide = c
knownAngle = C
}
if (and(a != null, A == null)) partialSide = a
if (and(b != null, B == null)) partialSide = b
if (and(c != null, C == null)) partialSide = c
if (knownAngle >= 180) throw `${status}No solution`
var ratio = knownSide / Math.sin(degToRad(knownAngle))
const temp = partialSide / ratio; // sin(partialAngle)
let partialAngle;
let unknownSide;
let unknownAngle;
if (temp > 1 || and(knownAngle >= 90, lessEqual(knownSide, partialSide)))
throw `${status}No solution`
else if (temp == 1 || knownSide >= partialSide) {
status += 'Unique solution'
partialAngle = radToDeg(Math.asin(temp))
unknownAngle = 180 - knownAngle - partialAngle
unknownSide = ratio * Math.sin(degToRad(unknownAngle)) // Law of sines
area = knownSide * partialSide * Math.sin(degToRad(unknownAngle)) / 2
} else {
status += 'Two solutions'
const partialAngle0 = radToDeg(Math.asin(temp));
const partialAngle1 = 180 - partialAngle0;
const unknownAngle0 = 180 - knownAngle - partialAngle0;
const unknownAngle1 = 180 - knownAngle - partialAngle1;
const unknownSide0 = ratio * Math.sin(degToRad(unknownAngle0)); // Law of sines
const unknownSide1 = ratio * Math.sin(degToRad(unknownAngle1)); // Law of sines
partialAngle = [partialAngle0, partialAngle1]
unknownAngle = [unknownAngle0, unknownAngle1]
unknownSide = [unknownSide0, unknownSide1]
area = [
knownSide * partialSide * Math.sin(degToRad(unknownAngle0)) / 2,
knownSide * partialSide * Math.sin(degToRad(unknownAngle1)) / 2
]
}
if (and(a != null, A == null)) A = partialAngle
if (and(b != null, B == null)) B = partialAngle
if (and(c != null, C == null)) C = partialAngle
if (and(a == null, A == null)) {
a = unknownSide
A = unknownAngle
}
if (and(b == null, B == null)) {
b = unknownSide
B = unknownAngle
}
if (and(c == null, C == null)) {
c = unknownSide
C = unknownAngle
}
}
return [a, b, c, A, B, C, area, status]
}
// Returns side c using law of cosines.
function solveSide(a, b, C) {
C = degToRad(C)
if (C > 0.001)
return Math.sqrt(a * a + b * b - 2 * a * b * Math.cos(C)) // Explained in http://www.nayuki.io/page/numerically-stable-law-of-cosines
else return Math.sqrt((a - b) * (a - b) + a * b * C * C * (1 - C * C / 12))
}
// Returns angle C using law of cosines.
function solveAngle(a, b, c) {
const temp = (a * a + b * b - c * c) / (2 * a * b);
if (and(lessEqual(-1, temp), lessEqual(temp, 0.9999999)))
return radToDeg(Math.acos(temp))
else if (
lessEqual(temp, 1) // Explained in http://www.nayuki.io/page/numerically-stable-law-of-cosines
)
return radToDeg(Math.sqrt((c * c - (a - b) * (a - b)) / (a * b)))
else throw 'No solution'
}
function degToRad(x) {
return x / 180 * Math.PI
}
function radToDeg(x) {
return x / Math.PI * 180
}
function lessEqual(x, y) {
return y >= x
}
function and(x, y) {
return x ? y : false
}
return solveTriangle
})