block by micahstubbs 1eddda946e40064a0f7eb1ccc89f8962

Internet Interactive Map Iteration

Full Screen

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

index.html

<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>

asgraph.jsx

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
  }
}

cities.js

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'
    }
  ]
})

colors.js

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
  }
})

intermap.jsx

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
        })
      }
    })
  }
)

lebab.js

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)
})

lebab.sh

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

locations.js

define(['cities'], locs => locs
  .map(loc => ({
  text: `${loc.city} (${loc.country})`,
  angle: loc.lon,
  weight: +loc.population
}))
  .sort((a, b) => b.weight - a.weight))

main.css

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;
}

polar-layout.jsx

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.js

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'
    }
  }
})

triangle-solver.js

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
})