├── .gitignore ├── README.md ├── components ├── default │ ├── action.js │ ├── inline.js │ ├── fixed.js │ ├── slide.js │ ├── svg.js │ ├── aside.js │ ├── float.js │ ├── button.js │ ├── panel.js │ ├── waypoint.js │ ├── full-screen.js │ ├── link.js │ ├── text-input.js │ ├── preload.js │ ├── code-highlight.js │ ├── boolean.js │ ├── range.js │ ├── header.js │ ├── display.js │ ├── analytics.js │ ├── select.js │ ├── slideshow.js │ ├── radio.js │ ├── dynamic.js │ ├── table.js │ ├── index.js │ ├── chart.js │ ├── utils │ │ ├── container.js │ │ └── screen.js │ ├── gist.js │ ├── equation.js │ └── feature.js ├── probe.js ├── vega-plot.js ├── theta.js ├── barnes-hut.js ├── data-network.js ├── time-plot.js ├── error-plot.js ├── data-performance.js └── quadtree.js ├── package.json ├── styles.css ├── index.idl └── docs ├── index.html └── styles.css /.gitignore: -------------------------------------------------------------------------------- 1 | .idyll 2 | .DS_Store 3 | build/ 4 | node_modules 5 | npm-debug.log -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Interactive explanation of the Barnes-Hut approximation. 2 | 3 | [View online at https://jheer.github.io/barnes-hut/](https://jheer.github.io/barnes-hut/). 4 | 5 | Created with [Idyll](http://idyll-lang.org/), [D3](https://d3js.org/), 6 | and [Vega](https://vega.github.io/vega/). 7 | -------------------------------------------------------------------------------- /components/default/action.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | class Action extends React.PureComponent { 4 | render() { 5 | return ( 6 | {this.props.children} 7 | ); 8 | } 9 | } 10 | 11 | export default Action; 12 | -------------------------------------------------------------------------------- /components/default/inline.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | class Inline extends React.PureComponent { 4 | render() { 5 | return ( 6 |
7 | {this.props.children} 8 |
9 | ); 10 | } 11 | } 12 | 13 | export default Inline; 14 | -------------------------------------------------------------------------------- /components/default/fixed.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | class Fixed extends React.PureComponent { 4 | render() { 5 | return ( 6 |
7 | {this.props.children} 8 |
9 | ); 10 | } 11 | } 12 | 13 | export default Fixed; 14 | -------------------------------------------------------------------------------- /components/default/slide.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | class Slide extends React.PureComponent { 4 | render() { 5 | return ( 6 |
7 | {this.props.children} 8 |
9 | ); 10 | } 11 | } 12 | 13 | export default Slide; 14 | -------------------------------------------------------------------------------- /components/default/svg.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | const InlineSVG = require('react-inlinesvg'); 3 | 4 | class SVG extends React.PureComponent { 5 | render() { 6 | return ( 7 | 8 | ); 9 | } 10 | } 11 | 12 | SVG.defaultProps = { 13 | src: '' 14 | } 15 | 16 | export default SVG; 17 | 18 | -------------------------------------------------------------------------------- /components/default/aside.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | class Aside extends React.PureComponent { 4 | render() { 5 | return ( 6 |
7 |
8 | {this.props.children} 9 |
10 |
11 | ); 12 | } 13 | } 14 | 15 | export default Aside; 16 | -------------------------------------------------------------------------------- /components/default/float.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | class Float extends React.PureComponent { 4 | render() { 5 | return ( 6 |
7 | {this.props.children} 8 |
9 | ); 10 | } 11 | } 12 | 13 | export default Float; 14 | -------------------------------------------------------------------------------- /components/default/button.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | class Button extends React.PureComponent { 4 | render() { 5 | return ( 6 | 9 | ); 10 | } 11 | } 12 | 13 | Button.defaultProps = { 14 | onClick: function() {} 15 | }; 16 | 17 | export default Button; 18 | -------------------------------------------------------------------------------- /components/default/panel.js: -------------------------------------------------------------------------------- 1 | 2 | import React from 'react'; 3 | const ReactDOM = require('react-dom'); 4 | 5 | class Panel extends React.PureComponent { 6 | constructor (props) { 7 | super(props); 8 | } 9 | 10 | render() { 11 | const { updateProps, hasError, ...props } = this.props; 12 | return
; 13 | } 14 | 15 | } 16 | 17 | export default Panel; 18 | -------------------------------------------------------------------------------- /components/default/waypoint.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import Screen from './utils/screen'; 4 | 5 | class Waypoint extends React.PureComponent { 6 | constructor (props) { 7 | super(props); 8 | } 9 | 10 | render() { 11 | return ; 12 | } 13 | 14 | } 15 | 16 | export default Waypoint; 17 | -------------------------------------------------------------------------------- /components/default/full-screen.js: -------------------------------------------------------------------------------- 1 | 2 | import React from 'react'; 3 | import ReactDOM from 'react-dom'; 4 | import Screen from './utils/screen'; 5 | 6 | class FullScreen extends React.PureComponent { 7 | constructor (props) { 8 | super(props); 9 | } 10 | 11 | render() { 12 | return ; 13 | } 14 | 15 | } 16 | 17 | export default FullScreen; 18 | -------------------------------------------------------------------------------- /components/default/link.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | class Link extends React.PureComponent { 4 | constructor(props) { 5 | super(props); 6 | } 7 | 8 | render() { 9 | let props = this.props; 10 | if (props.url) { 11 | props.href = props.url; 12 | } 13 | return ( 14 | 15 | {this.props.text || this.props.children} 16 | 17 | ); 18 | } 19 | } 20 | 21 | export default Link; 22 | -------------------------------------------------------------------------------- /components/probe.js: -------------------------------------------------------------------------------- 1 | const React = require('react'); 2 | 3 | // A custom component that simply renders a hand-coded SVG "probe" icon. 4 | class Probe extends React.Component { 5 | render() { 6 | return ( 7 | 8 | 9 | 10 | 11 | ); 12 | } 13 | } 14 | 15 | module.exports = Probe; 16 | -------------------------------------------------------------------------------- /components/default/text-input.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | const ReactDOM = require('react-dom'); 3 | 4 | class TextInput extends React.PureComponent { 5 | constructor(props) { 6 | super(props); 7 | this.onChange = this.onChange.bind(this); 8 | } 9 | 10 | onChange(e) { 11 | this.props.updateProps({ value: e.target.value }); 12 | } 13 | 14 | render() { 15 | return ( 16 | 17 | ); 18 | } 19 | } 20 | 21 | export default TextInput; 22 | -------------------------------------------------------------------------------- /components/default/preload.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | const ReactDOM = require('react-dom'); 3 | const imageCache = []; 4 | 5 | class Preloader extends React.PureComponent { 6 | componentDidMount() { 7 | const { images } = this.props; 8 | images.forEach((i) => { 9 | const img = new Image(); 10 | img.src = i; 11 | imageCache.push(img); 12 | }); 13 | } 14 | render () { 15 | return null; 16 | } 17 | } 18 | 19 | Preloader.defaultProps = { 20 | images: [] 21 | }; 22 | 23 | export default Preloader; 24 | -------------------------------------------------------------------------------- /components/default/code-highlight.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import SyntaxHighlighter from "react-syntax-highlighter/dist/light"; 3 | import style from 'react-syntax-highlighter/dist/styles/github'; 4 | 5 | class CodeHighlight extends React.PureComponent { 6 | render() { 7 | return {this.props.children.length ? this.props.children[0] : ''}; 8 | } 9 | } 10 | 11 | CodeHighlight.defaultProps = { 12 | children: [] 13 | } 14 | 15 | export default CodeHighlight; 16 | -------------------------------------------------------------------------------- /components/default/boolean.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | class Boolean extends React.PureComponent { 4 | constructor(props) { 5 | super(props); 6 | } 7 | 8 | toggleCheckbox() { 9 | this.props.updateProps({ 10 | value: !this.props.value 11 | }); 12 | } 13 | 14 | render() { 15 | const { value } = this.props; 16 | return ( 17 | 18 | ); 19 | } 20 | } 21 | 22 | Boolean.defaultProps = { 23 | value: false 24 | }; 25 | 26 | export default Boolean; 27 | -------------------------------------------------------------------------------- /components/default/range.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | class Range extends React.PureComponent { 4 | constructor(props) { 5 | super(props); 6 | } 7 | 8 | handleChange(event) { 9 | this.props.updateProps({ 10 | value: +event.target.value 11 | }); 12 | } 13 | 14 | render() { 15 | const { value, min, max, step } = this.props; 16 | return ( 17 | 18 | ); 19 | } 20 | } 21 | 22 | Range.defaultProps = { 23 | value: 0, 24 | min: 0, 25 | max: 1, 26 | step: 1 27 | }; 28 | 29 | export default Range; 30 | -------------------------------------------------------------------------------- /components/default/header.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | class Header extends React.PureComponent { 4 | render() { 5 | return ( 6 |
7 |

8 | {this.props.title} 9 |

10 | { 11 | this.props.subtitle && ( 12 |

13 | {this.props.subtitle} 14 |

15 | ) 16 | } 17 | { 18 | this.props.author && ( 19 |
20 | By: {this.props.author} 21 |
22 | ) 23 | } 24 | 25 |
26 | ); 27 | } 28 | } 29 | 30 | export default Header; 31 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "barnes-hut", 3 | "version": "1.0.0", 4 | "author": { 5 | "name": "Jeffrey Heer", 6 | "url": "http://idl.cs.washington.edu" 7 | }, 8 | "license": "BSD-3-Clause", 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/jheer/barnes-hut.git" 12 | }, 13 | "scripts": { 14 | "start": "idyll index.idl --css styles.css --theme github --watch", 15 | "build": "idyll index.idl --theme github --css styles.css; cp -r {images,fonts} build/;", 16 | "deploy": "npm run build && gh-pages -d ./build" 17 | }, 18 | "dependencies": { 19 | "d3": "^4.11.0", 20 | "idyll": "^2.0.3", 21 | "idyll-d3-component": "^2.0.3", 22 | "vega": "^3.0.7" 23 | }, 24 | "devDependencies": { 25 | "gh-pages": "^0.12.0" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /components/default/display.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | const Format = require('d3-format'); 3 | 4 | class Display extends React.PureComponent { 5 | constructor(props) { 6 | super(props); 7 | this.format = Format.format(props.format || '0.2f'); 8 | } 9 | 10 | formatValue(v) { 11 | const t = typeof v; 12 | switch(t) { 13 | case 'object': 14 | return JSON.stringify(v); 15 | case 'number': 16 | return this.format(v); 17 | case 'string': 18 | default: 19 | return v; 20 | } 21 | } 22 | 23 | render() { 24 | const { value } = this.props; 25 | const v = value !== undefined ? value : this.props.var; 26 | return ( 27 | 28 | {this.formatValue(v)} 29 | 30 | ); 31 | } 32 | } 33 | 34 | export default Display; 35 | -------------------------------------------------------------------------------- /components/default/analytics.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | class Analytics extends React.PureComponent { 4 | componentDidMount() { 5 | try { 6 | (function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){ 7 | (i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o), 8 | m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m) 9 | })(window,document,'script','https://www.google-analytics.com/analytics.js','ga'); 10 | 11 | ga('create', this.props.google, 'auto'); 12 | 13 | window.ga('send', 'pageview', { 14 | tag: this.props.tag 15 | }); 16 | } catch(e) { console.log('Could not mount Analytics.'); } 17 | } 18 | 19 | render() { 20 | return null; 21 | } 22 | } 23 | 24 | 25 | export default Analytics; 26 | -------------------------------------------------------------------------------- /components/default/select.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | const ReactDOM = require('react-dom'); 3 | 4 | class Select extends React.PureComponent { 5 | constructor(props) { 6 | super(props); 7 | this.onChange = this.onChange.bind(this); 8 | } 9 | 10 | onChange(e) { 11 | this.props.updateProps({ value: e.target.value }); 12 | } 13 | 14 | render() { 15 | return ( 16 | 24 | ); 25 | } 26 | } 27 | 28 | Select.defaultProps = { 29 | options: [] 30 | } 31 | 32 | export default Select; 33 | -------------------------------------------------------------------------------- /components/default/slideshow.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | const Slide = require('./slide'); 3 | 4 | class Slideshow extends React.PureComponent { 5 | 6 | getChildren(children) { 7 | let processedChildren = []; 8 | React.Children.forEach(children, (child) => { 9 | if (typeof child === 'string') { 10 | return; 11 | } 12 | if ((child.type.name && child.type.name.toLowerCase() === 'slide') || child.type.prototype instanceof Slide) { 13 | processedChildren.push(child); 14 | } else { 15 | processedChildren = processedChildren.concat(this.getChildren(child.props.children)); 16 | } 17 | }) 18 | return processedChildren; 19 | } 20 | 21 | render() { 22 | return ( 23 |
24 | {this.getChildren(this.props.children)[this.props.currentSlide-1]} 25 |
26 | ); 27 | } 28 | } 29 | 30 | Slideshow.defaultProps = { 31 | currentSlide: 1 32 | }; 33 | 34 | export default Slideshow; 35 | -------------------------------------------------------------------------------- /components/default/radio.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | const ReactDOM = require('react-dom'); 3 | let id = 0; 4 | 5 | class Radio extends React.PureComponent { 6 | constructor(props) { 7 | super(props); 8 | this.onChange = this.onChange.bind(this); 9 | this.id = id++; 10 | } 11 | 12 | onChange(e) { 13 | this.props.updateProps({ value: e.target.value }); 14 | } 15 | 16 | render() { 17 | return ( 18 |
19 | {this.props.options.map((d) => { 20 | if (typeof d === 'string') { 21 | return ; 22 | } 23 | return ; 24 | })} 25 |
26 | ); 27 | } 28 | } 29 | 30 | Radio.defaultProps = { 31 | options: [] 32 | }; 33 | 34 | export default Radio; 35 | -------------------------------------------------------------------------------- /components/vega-plot.js: -------------------------------------------------------------------------------- 1 | const D3Component = require('idyll-d3-component'); 2 | const d3 = require('d3'); 3 | const vega = require('vega'); 4 | const data = require('./data-performance'); 5 | 6 | function init(dom, props, spec) { 7 | const el = d3.select(dom) 8 | .append('div') 9 | .style('margin-left', '-35px') 10 | .style('padding', '1em 0'); 11 | 12 | const view = new vega.View(vega.parse(spec)) 13 | .renderer('svg') 14 | .initialize(el.node()) 15 | .insert('perf', data) 16 | .run(); 17 | 18 | view.addSignalListener('focus', (name, value) => { 19 | const obj = {focus: value !== -1}; 20 | if (obj.focus) obj.theta = value; 21 | setTimeout(props.updateProps(obj), 0); 22 | }); 23 | 24 | return view; 25 | }; 26 | 27 | class VegaPlot extends D3Component { 28 | 29 | initialize(dom, props) { 30 | this._view = init(dom, props, this.state.spec); 31 | } 32 | 33 | update(props) { 34 | const value = props.focus ? props.theta : -1; 35 | this._view.signal('focus', value).run(); 36 | } 37 | } 38 | 39 | module.exports = VegaPlot; 40 | -------------------------------------------------------------------------------- /styles.css: -------------------------------------------------------------------------------- 1 | /* Put your custom styles here */ 2 | 3 | /* Corrections for fixed component layout. */ 4 | .fixed { 5 | flex-direction: column; 6 | align-items: left; 7 | justify-content: center; 8 | } 9 | 10 | /* Range slider layout correction. */ 11 | input[type="range"] { 12 | margin: 0 10px; 13 | } 14 | 15 | /* Action link styling. */ 16 | .action { 17 | font-weight: 500; 18 | border-bottom: 1px dashed #888; 19 | cursor: pointer; 20 | } 21 | 22 | .action:hover { 23 | border-bottom: 1px dashed firebrick; 24 | color: firebrick; 25 | } 26 | 27 | /* Colors for Barnes-Hut theta values. */ 28 | .color0 { 29 | color: rgb(59, 15, 112); 30 | border-bottom: 1px dashed rgb(59, 15, 112); 31 | } 32 | .color1 { 33 | color: rgb(140, 41, 129); 34 | border-bottom: 1px dashed rgb(140, 41, 129); 35 | } 36 | .color2 { 37 | color: rgb(222, 73, 104); 38 | border-bottom: 1px dashed rgb(222, 73, 104); 39 | 40 | } 41 | .color3 { 42 | color: rgb(254, 159, 109); 43 | border-bottom: 1px dashed rgb(254, 159, 109); 44 | } 45 | 46 | /* Animation for Vega plot elements. */ 47 | svg.marks text, svg.marks path { 48 | transition: opacity 0.5s; 49 | } 50 | -------------------------------------------------------------------------------- /components/theta.js: -------------------------------------------------------------------------------- 1 | const React = require('react'); 2 | 3 | const colors = { 4 | '0': 'color0', 5 | '0.5': 'color1', 6 | '1': 'color2', 7 | '1.5': 'color3' 8 | }; 9 | 10 | function label(value) { 11 | return value ? 'theta=' + value : 'naïve approach'; 12 | } 13 | 14 | // A custom component that prints a colored theta parameter link 15 | // Hovering over the link updates theta and focus variables 16 | class Theta extends React.PureComponent { 17 | 18 | // On mouseover, push update to set theta value and enable focus 19 | over() { 20 | this.props.updateProps({ 21 | theta: this.props.value, 22 | focus: true 23 | }); 24 | } 25 | 26 | // On mouseout, push update to disable focus 27 | out() { 28 | this.props.updateProps({ 29 | focus: false 30 | }); 31 | } 32 | 33 | render() { 34 | return ( 35 | {label(this.props.value)} 41 | ); 42 | } 43 | } 44 | 45 | module.exports = Theta; 46 | -------------------------------------------------------------------------------- /components/default/dynamic.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | const ReactDOM = require('react-dom'); 3 | const Format = require('d3-format'); 4 | const Drag = require('d3-drag'); 5 | const Selection = require('d3-selection'); 6 | 7 | class Dynamic extends React.PureComponent { 8 | 9 | componentDidMount() { 10 | let node; 11 | try { 12 | node = ReactDOM.findDOMNode(this); 13 | } catch(e) {}; 14 | if (!node) { 15 | return; 16 | } 17 | this.drag = Drag.drag().on('drag', () => { 18 | const dx = Selection.event.dx; 19 | const { interval, value } = this.props; 20 | const newValue = Math.max(Math.min(value + interval * dx, this.props.max), this.props.min); 21 | this.props.updateProps({ value: newValue }); 22 | }); 23 | this.drag(Selection.select(node)); 24 | } 25 | 26 | render() { 27 | const { format, value } = this.props; 28 | const formatter = Format.format(format); 29 | return ( 30 | 31 | {formatter(value)} 32 | 33 | ); 34 | } 35 | } 36 | 37 | Dynamic.defaultProps = { 38 | format: '.2f', 39 | min: Number.NEGATIVE_INFINITY, 40 | max: Number.POSITIVE_INFINITY, 41 | interval: 1 42 | }; 43 | 44 | export default Dynamic; 45 | -------------------------------------------------------------------------------- /components/default/table.js: -------------------------------------------------------------------------------- 1 | const React = require('react'); 2 | const Table = require('react-table').default; 3 | 4 | class TableComponent extends React.PureComponent { 5 | getColumns() { 6 | if (this.props.columns) { 7 | if (this.props.columns.length && typeof this.props.columns[0] === 'string') { 8 | return this.props.columns.map((d) => { 9 | return { 10 | Header: d, 11 | accessor: d 12 | }; 13 | }) 14 | } 15 | 16 | return this.props.columns; 17 | } 18 | if ((this.props.data || []).length) { 19 | return Object.keys(this.props.data[0]).map((d) => { 20 | return { 21 | Header: d, 22 | accessor: d 23 | } 24 | }) 25 | } 26 | 27 | return []; 28 | } 29 | render() { 30 | return ( 31 | 38 | ); 39 | } 40 | } 41 | 42 | TableComponent.defaultProps = { 43 | showPagination: false, 44 | showPageSizeOptions: false, 45 | showPageJump: false 46 | } 47 | 48 | module.exports = TableComponent; 49 | -------------------------------------------------------------------------------- /components/default/index.js: -------------------------------------------------------------------------------- 1 | export { default as Action } from './action'; 2 | export { default as Analytics } from './analytics'; 3 | export { default as Aside } from './aside'; 4 | export { default as Boolean } from './boolean'; 5 | export { default as Button } from './button'; 6 | export { default as Chart } from './chart'; 7 | export { default as CodeHighlight } from './code-highlight'; 8 | export { default as Display } from './display'; 9 | export { default as Dynamic } from './dynamic'; 10 | export { default as Equation } from './equation'; 11 | export { default as Feature, Content as FeatureContent } from './feature'; 12 | export { default as Fixed } from './fixed'; 13 | export { default as Float } from './float'; 14 | export { default as FullScreen } from './full-screen'; 15 | export { default as Gist } from './gist'; 16 | export { default as Header } from './header'; 17 | export { default as Inline } from './inline'; 18 | export { default as Link } from './link'; 19 | export { default as Panel } from './panel'; 20 | export { default as Preload } from './preload'; 21 | export { default as Radio } from './radio'; 22 | export { default as Range } from './range'; 23 | export { default as Select } from './select'; 24 | export { default as Slide } from './slide'; 25 | export { default as Slideshow } from './slideshow'; 26 | export { default as SVG } from './svg'; 27 | export { default as Table } from './table'; 28 | export { default as TextInput } from './text-input'; 29 | export { default as Waypoint } from './waypoint'; 30 | -------------------------------------------------------------------------------- /components/barnes-hut.js: -------------------------------------------------------------------------------- 1 | const React = require('react'); 2 | const d3 = require('d3'); 3 | const D3Component = require('idyll-d3-component'); 4 | const quadtreeVis = require('./quadtree'); 5 | 6 | class BarnesHut extends D3Component { 7 | 8 | initialize(dom, props) { 9 | // initialize properties 10 | const state = this._state = { 11 | width: 514, 12 | height: 514, 13 | size: 0, 14 | theta: props.theta || 0, 15 | charge: props.charge || -30, 16 | radius: props.radius || 4, 17 | accumulate: 0, 18 | layout: true, 19 | estimate: false 20 | }; 21 | 22 | // initialize component 23 | const div = d3.select(dom), 24 | el = div.append('div').attr('class', 'quad'), 25 | quad = this._quad = quadtreeVis(el.node(), 26 | { 27 | width: state.width, 28 | height: state.height, 29 | theta: state.theta, 30 | radius: state.radius, 31 | } 32 | ); 33 | 34 | this.update(props); 35 | } 36 | 37 | updated(props, name) { 38 | return props[name] != null && props[name] !== this._state[name] 39 | ? (this._state[name] = props[name], 1) : 0; 40 | } 41 | 42 | update(props) { 43 | const quad = this._quad; 44 | 45 | if (this.updated(props, 'charge')) { 46 | quad.charge(props.charge); 47 | } 48 | 49 | if (this.updated(props, 'layout')) { 50 | quad.layout(props.layout); 51 | } 52 | 53 | if (this.updated(props, 'accumulate')) { 54 | quad.accumulate(); 55 | } 56 | 57 | if (this.updated(props, 'estimate')) { 58 | quad.estimate(props.estimate); 59 | } 60 | 61 | if (this.updated(props, 'theta')) { 62 | quad.theta(props.theta); 63 | } 64 | 65 | if (this.updated(props, 'size')) { 66 | quad.size(props.size); 67 | } 68 | } 69 | } 70 | 71 | module.exports = BarnesHut; 72 | -------------------------------------------------------------------------------- /components/default/chart.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | const V = require('victory'); 3 | const d3Arr = require('d3-array'); 4 | 5 | const types = { 6 | AREA: V.VictoryArea, 7 | TIME: V.VictoryLine, 8 | LINE: V.VictoryLine, 9 | BAR: V.VictoryBar, 10 | SCATTER: V.VictoryScatter, 11 | PIE: V.VictoryPie 12 | }; 13 | 14 | let chartCount = 0; 15 | 16 | class Chart extends React.PureComponent { 17 | 18 | constructor(props) { 19 | super(props); 20 | this.id = chartCount++; 21 | } 22 | 23 | render() { 24 | const { id, props } = this; 25 | const type = props.type.toUpperCase(); 26 | const INNER_CHART = types[type]; 27 | let { scale, data, domain, ...customProps } = props; 28 | 29 | if (props.equation) { 30 | const d = domain; 31 | data = d3Arr.range(d[0], d[1], (d[1] - d[0]) / props.samplePoints).map((x) => { 32 | return { 33 | x: x, 34 | y: props.equation(x) 35 | }; 36 | }); 37 | } 38 | 39 | if (type === types.TIME) { 40 | scale = {x: 'time', y: 'linear'}; 41 | data = data.map((d) => { 42 | return Object.assign({}, d, { 43 | x: new Date(d.x) 44 | }); 45 | }); 46 | } 47 | return ( 48 |
49 | {type !== 'PIE' ? ( 50 | 51 | 56 | 57 | 58 | ) : ( 59 | 60 | 61 | ) 62 | } 63 |
64 | ); 65 | } 66 | } 67 | 68 | Chart.defaultProps = { 69 | domain: [-1, 1], 70 | range: [-1, 1], 71 | domainPadding: 0, 72 | samplePoints: 100, 73 | type: 'line' 74 | }; 75 | 76 | export default Chart; 77 | -------------------------------------------------------------------------------- /components/default/utils/container.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | const ReactDOM = require('react-dom'); 3 | 4 | class Container extends React.Component { 5 | constructor (props) { 6 | super(props); 7 | 8 | this.state = { 9 | expandLeft: 0, 10 | expandRight: 0 11 | }; 12 | 13 | this.setPosition = this.setPosition.bind(this); 14 | } 15 | 16 | componentDidMount() { 17 | window.addEventListener('resize', this.setPosition); 18 | try { 19 | this.node = ReactDOM.findDOMNode(this) 20 | this.setPosition(); 21 | } catch(e) {} 22 | } 23 | 24 | //shouldComponentUpdate (nextProps, nextState) { 25 | //return Math.round(nextState.expandLeft) !== Math.round(this.state.expandLeft) || 26 | //Math.round(nextState.expandRight) !== Math.round(this.state.expandRight); 27 | //} 28 | 29 | setPosition () { 30 | var expandLeft, expandRight; 31 | var rect = this.node.getBoundingClientRect(); 32 | var pageWidth = window.innerWidth; 33 | 34 | if (this.props.fullBleed) { 35 | expandLeft = Infinity; 36 | expandRight = Infinity; 37 | } else { 38 | expandLeft = this.props.expandLeft === undefined ? this.props.expand : this.props.expandLeft; 39 | expandRight = this.props.expandRight === undefined ? this.props.expand : this.props.expandRight; 40 | } 41 | 42 | var left = Math.max(rect.left - expandLeft, this.props.padding); 43 | var right = Math.min(rect.right + expandRight, pageWidth - this.props.padding); 44 | 45 | this.setState({ 46 | expandLeft: left - rect.left, 47 | expandRight: rect.right - right 48 | }); 49 | } 50 | 51 | render () { 52 | var expandStyle = Object.assign({}, this.props.style || {}, { 53 | marginLeft: this.state.expandLeft, 54 | marginRight: this.state.expandRight 55 | }); 56 | 57 | return
61 |
62 | {this.props.children} 63 |
64 |
65 | } 66 | } 67 | 68 | Container.defaultProps = { 69 | padding: 15, 70 | expand: 0, 71 | fullBleed: false 72 | } 73 | 74 | export default Container; 75 | -------------------------------------------------------------------------------- /components/data-network.js: -------------------------------------------------------------------------------- 1 | const d3 = require('d3'); 2 | 3 | const links = [ 4 | [1,0],[2,0],[3,0],[3,2],[4,0],[5,0],[6,0],[7,0],[8,0],[9,0],[11,10], 5 | [11,3],[11,2],[11,0],[12,11],[13,11],[14,11],[15,11],[17,16],[18,16], 6 | [18,17],[19,16],[19,17],[19,18],[20,16],[20,17],[20,18],[20,19],[21,16], 7 | [21,17],[21,18],[21,19],[21,20],[22,16],[22,17],[22,18],[22,19],[22,20], 8 | [22,21],[23,16],[23,17],[23,18],[23,19],[23,20],[23,21],[23,22],[23,12], 9 | [23,11],[24,23],[24,11],[25,24],[25,23],[25,11],[26,24],[26,11],[26,16], 10 | [26,25],[27,11],[27,23],[27,25],[27,24],[27,26],[28,11],[28,27],[29,23], 11 | [29,27],[29,11],[30,23],[31,30],[31,11],[31,23],[31,27],[32,11],[33,11], 12 | [33,27],[34,11],[34,29],[35,11],[35,34],[35,29],[36,34],[36,35],[36,11], 13 | [36,29],[37,34],[37,35],[37,36],[37,11],[37,29],[38,34],[38,35],[38,36], 14 | [38,37],[38,11],[38,29],[39,25],[40,25],[41,24],[41,25],[42,41],[42,25], 15 | [42,24],[43,11],[43,26],[43,27],[44,28],[44,11],[45,28],[47,46],[48,47], 16 | [48,25],[48,27],[48,11],[49,26],[49,11],[50,49],[50,24],[51,49],[51,26], 17 | [51,11],[52,51],[52,39],[53,51],[54,51],[54,49],[54,26],[55,51],[55,49], 18 | [55,39],[55,54],[55,26],[55,11],[55,16],[55,25],[55,41],[55,48],[56,49], 19 | [56,55],[57,55],[57,41],[57,48],[58,55],[58,48],[58,27],[58,57],[58,11], 20 | [59,58],[59,55],[59,48],[59,57],[60,48],[60,58],[60,59],[61,48],[61,58], 21 | [61,60],[61,59],[61,57],[61,55],[62,55],[62,58],[62,59],[62,48],[62,57], 22 | [62,41],[62,61],[62,60],[63,59],[63,48],[63,62],[63,57],[63,58],[63,61], 23 | [63,60],[63,55],[64,55],[64,62],[64,48],[64,63],[64,58],[64,61],[64,60], 24 | [64,59],[64,57],[64,11],[65,63],[65,64],[65,48],[65,62],[65,58],[65,61], 25 | [65,60],[65,59],[65,57],[65,55],[66,64],[66,58],[66,59],[66,62],[66,65], 26 | [66,48],[66,63],[66,61],[66,60],[67,57],[68,25],[68,11],[68,24],[68,27], 27 | [68,48],[68,41],[69,25],[69,68],[69,11],[69,24],[69,27],[69,48],[69,41], 28 | [70,25],[70,69],[70,68],[70,11],[70,24],[70,27],[70,41],[70,58],[71,27], 29 | [71,69],[71,68],[71,70],[71,11],[71,48],[71,41],[71,25],[72,26],[72,27], 30 | [72,11],[73,48],[74,48],[74,73],[75,69],[75,68],[75,25],[75,48],[75,41], 31 | [75,70],[75,71],[76,64],[76,65],[76,66],[76,63],[76,62],[76,48],[76,58] 32 | ]; 33 | 34 | module.exports = { 35 | nodes: d3.range(77).map(i => { return {index: i}; }), 36 | links: links.map(d => { return {source: d[0], target: d[1]}; }) 37 | }; 38 | -------------------------------------------------------------------------------- /components/default/gist.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | const PropTypes = require('prop-types'); 3 | 4 | class EmbeddedGist extends React.PureComponent { 5 | 6 | constructor(props) { 7 | super(props); 8 | this.gist = props.gist; 9 | this.file = props.file; 10 | this.stylesheetAdded = false; 11 | this.state = { 12 | loading: true, 13 | src: "" 14 | }; 15 | } 16 | 17 | // The Gist JSON data includes a stylesheet to add to the page 18 | // to make it look correct. `addStylesheet` ensures we only add 19 | // the stylesheet one time. 20 | addStylesheet(href) { 21 | if (!this.stylesheetAdded) { 22 | this.stylesheetAdded = true; 23 | var link = document.createElement('link'); 24 | link.type = "text/css"; 25 | link.rel = "stylesheet"; 26 | link.href = href; 27 | 28 | document.head.appendChild(link); 29 | } 30 | } 31 | 32 | componentDidMount() { 33 | // Create a JSONP callback that will set our state 34 | // with the data that comes back from the Gist site 35 | var gistCallback = EmbeddedGist.nextGistCallback(); 36 | window[gistCallback] = function(gist) { 37 | this.setState({ 38 | loading: false, 39 | src: gist.div 40 | }); 41 | this.addStylesheet(gist.stylesheet); 42 | }.bind(this); 43 | 44 | var url = "https://gist.github.com/" + this.props.gist + ".json?callback=" + gistCallback; 45 | if (this.props.file) { 46 | url += "&file=" + this.props.file; 47 | } 48 | 49 | // Add the JSONP script tag to the document. 50 | var script = document.createElement('script'); 51 | script.type = 'text/javascript'; 52 | script.src = url; 53 | document.head.appendChild(script); 54 | } 55 | 56 | render() { 57 | if (this.state.loading) { 58 | return
loading...
; 59 | } else { 60 | return
; 61 | } 62 | } 63 | } 64 | 65 | EmbeddedGist.propTypes = { 66 | gist: PropTypes.string.isRequired, // e.g. "username/id" 67 | file: PropTypes.string // to embed a single specific file from the gist 68 | }; 69 | 70 | // Each time we request a Gist, we'll need to generate a new 71 | // global function name to serve as the JSONP callback. 72 | var gistCallbackId = 0; 73 | EmbeddedGist.nextGistCallback = () => { 74 | return "embed_gist_callback_" + gistCallbackId++; 75 | }; 76 | 77 | EmbeddedGist.defaultProps = { 78 | gist: 'mathisonian/689614257cb1af6b15de3344da6cdc7a' 79 | } 80 | 81 | export default EmbeddedGist; 82 | 83 | -------------------------------------------------------------------------------- /components/default/utils/screen.js: -------------------------------------------------------------------------------- 1 | 2 | import React from 'react'; 3 | import ReactDOM from 'react-dom'; 4 | import Container from './container'; 5 | 6 | class Screen extends React.PureComponent { 7 | constructor (props) { 8 | super(props); 9 | } 10 | 11 | 12 | render () { 13 | let overlayStyle = { 14 | position: this.props.display ? this.props.display : 'relative', 15 | zIndex: 1, 16 | width: this.props.fullBleed ? '100%' : undefined, 17 | left: this.props.display === 'fixed' ? 0 : undefined, 18 | pointerEvents: 'none', 19 | transition: 'background 0.5s' 20 | }; 21 | 22 | if (this.props.height) { 23 | overlayStyle.minHeight = this.props.height; 24 | } else { 25 | overlayStyle.height = '100vh'; 26 | } 27 | 28 | if (this.props.backgroundImage) { 29 | overlayStyle.backgroundImage = 'url(' + this.props.backgroundImage + ')'; 30 | overlayStyle.backgroundSize = 'cover'; 31 | overlayStyle.backgroundPosition = 'top center'; 32 | } 33 | 34 | let contentContainerStyle = Object.assign({ 35 | flexDirection: this.props.direction || 'column', 36 | display: 'flex', 37 | height: '100%', 38 | justifyContent: { 39 | center: 'center' 40 | }[this.props.justify] || undefined 41 | }, this.props.contentContainerStyle || {}); 42 | 43 | let contentStyle = { 44 | alignSelf: { 45 | left: 'flex-start', 46 | center: 'center', 47 | right: 'flex-end', 48 | stretch: 'stretch' 49 | }[this.props.align] || 'flex-end', 50 | pointerEvents: 'all' 51 | } 52 | 53 | if (this.props.fullBleed) { 54 | return ( 55 |
56 |
57 |
58 |
59 |
60 | {this.props.children} 61 |
62 |
63 |
64 |
65 |
66 |
67 | ); 68 | } 69 | 70 | return 78 |
79 |
80 |
81 | {this.props.children} 82 |
83 |
84 |
85 | 86 | } 87 | } 88 | 89 | Screen.defaultProps = { 90 | position: 0.5, 91 | padding: 0, 92 | fullBleed: false, 93 | align: 'left', 94 | }; 95 | 96 | export default Screen; 97 | -------------------------------------------------------------------------------- /components/time-plot.js: -------------------------------------------------------------------------------- 1 | const VegaPlot = require('./vega-plot'); 2 | 3 | const spec = { 4 | width: 635, 5 | height: 200, 6 | autosize: 'fit', 7 | config: { 8 | title: {fontSize: 12, anchor: 'start', offset: 0} 9 | }, 10 | 11 | signals: [ 12 | { 13 | name: "focus", 14 | value: -1, 15 | on: [ 16 | { 17 | events: 'text:mouseover, line:mouseover', 18 | update: 'datum.theta' 19 | }, 20 | { 21 | events: 'text:mouseout, line:mouseout', 22 | update: '-1' 23 | } 24 | ] 25 | } 26 | ], 27 | 28 | data: [ 29 | { 30 | name: 'perf' 31 | }, 32 | { 33 | name: 'labels', 34 | source: 'perf', 35 | transform: [ 36 | { 37 | type: 'filter', 38 | expr: 'datum.nodes === 1e4' 39 | }, 40 | { 41 | type: 'formula', as: 'label', 42 | expr: 'datum.theta ? "\u03F4 = " + datum.theta : "Naïve"' 43 | } 44 | ] 45 | } 46 | ], 47 | 48 | scales: [ 49 | { 50 | name: 'xs', 51 | type: 'linear', 52 | domain: {data: 'perf', field: 'nodes'}, 53 | range: 'width' 54 | }, 55 | { 56 | name: 'ys', 57 | type: 'linear', 58 | domain: {data: 'perf', field: 'time'}, 59 | range: 'height', nice: true 60 | }, 61 | { 62 | name: 'cs', 63 | type: 'ordinal', 64 | domain: {data: 'perf', field: 'theta', sort: true}, 65 | range: ['rgb(59, 15, 112)', 'rgb(140, 41, 129)', 'rgb(222, 73, 104)', 'rgb(254, 159, 109)'] 66 | } 67 | ], 68 | 69 | title: 'Average Running Time (milliseconds)', 70 | 71 | axes: [ 72 | { 73 | orient: 'left', scale: 'ys', 74 | grid: true, tickCount: 5, minExtent: 35 75 | }, 76 | { 77 | orient: 'bottom', scale: 'xs', 78 | title: 'Number of Points' 79 | } 80 | ], 81 | 82 | marks: [ 83 | { 84 | type: 'group', 85 | from: { 86 | facet: { 87 | data: 'perf', 88 | name: 'series', 89 | groupby: 'theta' 90 | } 91 | }, 92 | marks: [ 93 | { 94 | type: 'line', 95 | from: {data: 'series'}, 96 | encode: { 97 | enter: { 98 | interpolate: 'monotone', 99 | x: {scale: 'xs', field: 'nodes'}, 100 | y: {scale: 'ys', field: 'time'}, 101 | stroke: {scale: 'cs', field: 'theta'}, 102 | strokeWidth: {value: 3} 103 | }, 104 | update: { 105 | opacity: {signal: "focus < 0 || focus === datum.theta ? 1 : 0.25"} 106 | } 107 | } 108 | } 109 | ] 110 | }, 111 | { 112 | type: 'text', 113 | from: {data: 'labels'}, 114 | encode: { 115 | enter: { 116 | x: {scale: 'xs', field: 'nodes'}, 117 | dx: {value: 6}, 118 | y: {scale: 'ys', field: 'time'}, 119 | baseline: {value: 'middle'}, 120 | text: {field: 'label'}, 121 | fill: {scale: 'cs', field: 'theta'} 122 | }, 123 | update: { 124 | opacity: {signal: "focus < 0 || focus === datum.theta ? 1 : 0.25"} 125 | } 126 | } 127 | } 128 | ] 129 | }; 130 | 131 | class TimePlot extends VegaPlot { 132 | constructor(props) { 133 | super(props); 134 | this.state = {spec: spec}; 135 | } 136 | } 137 | 138 | module.exports = TimePlot; 139 | -------------------------------------------------------------------------------- /components/error-plot.js: -------------------------------------------------------------------------------- 1 | const VegaPlot = require('./vega-plot'); 2 | 3 | const spec = { 4 | width: 635, 5 | height: 200, 6 | autosize: 'fit', 7 | config: {title: {fontSize: 12, anchor: 'start', offset: 0}}, 8 | 9 | signals: [ 10 | { 11 | name: "focus", 12 | value: -1, 13 | on: [ 14 | { 15 | events: 'text:mouseover, line:mouseover', 16 | update: 'datum.theta' 17 | }, 18 | { 19 | events: 'text:mouseout, line:mouseout', 20 | update: '-1' 21 | } 22 | ] 23 | } 24 | ], 25 | 26 | data: [ 27 | { 28 | name: 'perf', 29 | transform: [ 30 | { 31 | type: 'filter', 32 | expr: 'datum.theta > 0' 33 | } 34 | ] 35 | }, 36 | { 37 | name: 'labels', 38 | source: 'perf', 39 | transform: [ 40 | { 41 | type: 'filter', 42 | expr: 'datum.nodes === 1e4' 43 | }, 44 | { 45 | type: 'formula', as: 'label', 46 | expr: 'datum.theta ? "\u03F4 = " + datum.theta : "Naïve"' 47 | } 48 | ] 49 | } 50 | ], 51 | 52 | scales: [ 53 | { 54 | name: 'xs', 55 | type: 'linear', 56 | domain: {data: 'perf', field: 'nodes'}, 57 | range: 'width' 58 | }, 59 | { 60 | name: 'ys', 61 | type: 'linear', 62 | domain: {data: 'perf', field: 'error'}, 63 | range: 'height', 64 | zero: false, 65 | nice: true 66 | }, 67 | { 68 | name: 'cs', 69 | type: 'ordinal', 70 | range: ['rgb(140, 41, 129)', 'rgb(222, 73, 104)', 'rgb(254, 159, 109)'] 71 | } 72 | ], 73 | 74 | title: 'Average Error, Relative to Naïve Calculation (pixels)', 75 | 76 | axes: [ 77 | { 78 | orient: 'left', scale: 'ys', 79 | grid: true, tickCount: 5, minExtent: 35 80 | }, 81 | { 82 | orient: 'bottom', scale: 'xs', 83 | title: 'Number of Points' 84 | } 85 | ], 86 | 87 | marks: [ 88 | { 89 | type: 'group', 90 | from: { 91 | facet: { 92 | data: 'perf', 93 | name: 'series', 94 | groupby: 'theta' 95 | } 96 | }, 97 | marks: [ 98 | { 99 | type: 'line', 100 | from: {data: 'series'}, 101 | encode: { 102 | enter: { 103 | interpolate: 'monotone', 104 | x: {scale: 'xs', field: 'nodes'}, 105 | y: {scale: 'ys', field: 'error'}, 106 | stroke: {scale: 'cs', field: 'theta'}, 107 | strokeWidth: {value: 3} 108 | }, 109 | update: { 110 | opacity: {signal: "focus < 0 || focus === datum.theta ? 1 : 0.25"} 111 | } 112 | } 113 | } 114 | ] 115 | }, 116 | { 117 | type: 'text', 118 | from: {data: 'labels'}, 119 | encode: { 120 | enter: { 121 | x: {scale: 'xs', field: 'nodes'}, 122 | dx: {value: 6}, 123 | y: {scale: 'ys', field: 'error'}, 124 | baseline: {value: 'middle'}, 125 | text: {field: 'label'}, 126 | fill: {scale: 'cs', field: 'theta'} 127 | }, 128 | update: { 129 | opacity: {signal: "focus < 0 || focus === datum.theta ? 1 : 0.25"} 130 | } 131 | } 132 | } 133 | ] 134 | }; 135 | 136 | class ErrorPlot extends VegaPlot { 137 | constructor(props) { 138 | super(props); 139 | this.state = {spec: spec}; 140 | } 141 | } 142 | 143 | module.exports = ErrorPlot; 144 | -------------------------------------------------------------------------------- /components/default/equation.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | const ReactDOM = require('react-dom'); 3 | const Latex = require('react-latex'); 4 | const select = require('d3-selection').select; 5 | const format = require('d3-format').format; 6 | 7 | if (typeof document !== 'undefined') { 8 | document.write(''); 9 | } 10 | 11 | const allowedProps = ['domain', 'step', 'children']; 12 | 13 | class Equation extends React.PureComponent { 14 | constructor(props) { 15 | super(props); 16 | this.state = { 17 | showRange: false 18 | }; 19 | } 20 | 21 | handleChange(event) { 22 | this.props.updateProps({ 23 | value: +event.target.value 24 | }); 25 | } 26 | 27 | componentDidMount() { 28 | let dom; 29 | try { 30 | dom = ReactDOM.findDOMNode(this); 31 | } catch(e) {}; 32 | if (!dom) { 33 | return; 34 | } 35 | 36 | this.propNodes = {}; 37 | const self = this; 38 | select(dom).selectAll('.mord').each(function (d) { 39 | const $this = select(this); 40 | Object.keys(self.props).filter((prop) => { 41 | return allowedProps.indexOf(prop) === -1 42 | }).forEach((prop) => { 43 | if ($this.text() === prop) { 44 | self.propNodes[prop] = $this; 45 | $this.style('cursor', 'pointer'); 46 | $this.on('mouseover', () => { 47 | $this.style('color', 'red'); 48 | }).on('mouseout', () => { 49 | if (!(self.state.showRange && self.state.var === prop)) { 50 | $this.style('color', 'black'); 51 | } 52 | }).on('click', () => { 53 | 54 | if (!(self.state.showRange && self.state.var === prop)) { 55 | self.setState({ 56 | showRange: true, 57 | var: prop 58 | }); 59 | $this.text(self.props[prop]) 60 | $this.style('color', 'red'); 61 | Object.keys(self.propNodes).filter(d => d !== prop).forEach((d) => { 62 | self.propNodes[d].text(d); 63 | self.propNodes[d].style('color', 'black'); 64 | }) 65 | } else { 66 | self.setState({ 67 | showRange: false, 68 | var: prop 69 | }); 70 | $this.style('color', 'black'); 71 | $this.text(prop) 72 | } 73 | }) 74 | } 75 | }) 76 | }); 77 | 78 | } 79 | 80 | handleRangeUpdate(event) { 81 | const newProps = {}; 82 | const val = +event.target.value; 83 | newProps[this.state.var] = val; 84 | this.props.updateProps(newProps); 85 | this.propNodes[this.state.var].text(val); 86 | } 87 | 88 | renderEditing() { 89 | if (!this.state.showRange) { 90 | return null; 91 | } 92 | 93 | const d = (this.props.domain || {})[this.state.var] || [-10, 10]; 94 | const step = (this.props.step || {})[this.state.var] || 0.1; 95 | return ( 96 |
97 | 98 |
99 | ); 100 | } 101 | 102 | getLatex() { 103 | if (this.props.latex) { 104 | return this.props.latex; 105 | } 106 | return (this.props.children && this.props.children[0]) ? this.props.children[0] : ''; 107 | } 108 | 109 | render() { 110 | const latexChar = '$'; 111 | const latexString = latexChar + this.getLatex() + latexChar; 112 | 113 | let style; 114 | if (this.state.showRange) { 115 | style = this.props.style; 116 | } else { 117 | style = Object.assign({ 118 | display: this.props.display ? "block" : "inline-block" 119 | }, this.props.style); 120 | } 121 | 122 | return ( 123 | 124 | {latexString} 125 | {this.renderEditing()} 126 | 127 | ); 128 | } 129 | } 130 | 131 | export default Equation; 132 | -------------------------------------------------------------------------------- /components/default/feature.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | 4 | const stateClasses = [ 5 | 'is-top', 6 | 'is-fixed', 7 | 'is-bottom' 8 | ]; 9 | 10 | class Content extends React.PureComponent { 11 | render () { 12 | return
13 | {this.props.children} 14 |
15 | } 16 | } 17 | 18 | class Feature extends React.PureComponent { 19 | constructor (props) { 20 | super(props) 21 | this.setFeature = this.setFeature.bind(this); 22 | this.setRoot = this.setRoot.bind(this); 23 | 24 | this.state = { 25 | scrollState: 0, 26 | featureMarginLeft: 0, 27 | }; 28 | } 29 | 30 | setRoot (c) { 31 | this.rootEl = c; 32 | this.initialize(); 33 | } 34 | 35 | setFeature (c) { 36 | this.featureEl = c; 37 | this.initialize(); 38 | } 39 | 40 | handleResize () { 41 | let rootRect = this.rootEl.getBoundingClientRect() 42 | this.setState({ 43 | featureMarginLeft: -rootRect.left 44 | }); 45 | } 46 | 47 | handleScroll () { 48 | if (!this.rootEl) return; 49 | let rootRect = this.rootEl.getBoundingClientRect(); 50 | let position = rootRect.top / (window.innerHeight - rootRect.height) 51 | // Update this whenever it changes so that the state is correctly adjusted: 52 | this.setState({scrollState: position < 0 ? 0 : (position <= 1 ? 1 : 2)}) 53 | // Only update the value when onscreen: 54 | if (rootRect.top < window.innerHeight && rootRect.bottom > 0) { 55 | this.props.updateProps({value: position}) 56 | } 57 | } 58 | 59 | 60 | 61 | initialize () { 62 | if (!this.rootEl || !this.featureEl) return; 63 | 64 | this.handleResize(); 65 | window.addEventListener('resize', this.handleResize.bind(this)); 66 | window.addEventListener('scroll', this.handleScroll.bind(this)); 67 | } 68 | 69 | unwrapChild(c) { 70 | if (c => c.type.name && c.type.name.toLowerCase() === 'wrapper') { 71 | return c.props.children[0]; 72 | } 73 | return c; 74 | } 75 | 76 | unwrapChildren() { 77 | return this.props.children.map((c) => this.unwrapChild(c)); 78 | } 79 | 80 | splitFeatureChildren() { 81 | const unwrapped = this.unwrapChildren(); 82 | return React.Children.toArray(this.props.children).reduce((memo, child, i) => { 83 | const c = unwrapped[i]; 84 | if (!c.type) { 85 | memo[1] = memo[1].concat([child]); 86 | return memo; 87 | } 88 | if ((c.type.name && c.type.name.toLowerCase() === 'content') || c.type.prototype instanceof Content) { 89 | memo[0] = child; 90 | } else { 91 | memo[1] = memo[1].concat([child]); 92 | } 93 | return memo; 94 | }, [undefined, []]); 95 | } 96 | 97 | render () { 98 | let feature; 99 | let ps = this.state.scrollState; 100 | let featureStyles = { 101 | width: 'calc(100vw - 15px)', 102 | overflowX: 'hidden', 103 | height: '100vh', 104 | marginLeft: ps === 1 ? 0 : (this.state.featureMarginLeft + 'px'), 105 | position: ps >= 1 ? 'fixed' : 'absolute', 106 | bottom: ps === 2 ? 0 : 'auto', 107 | zIndex: -1 108 | }; 109 | 110 | if (ps === 1) { 111 | featureStyles.top = 0; 112 | featureStyles.right = 0; 113 | featureStyles.bottom = 0; 114 | featureStyles.left = 0; 115 | } 116 | 117 | let rootStyles = { 118 | position: 'relative', 119 | marginLeft: 0, 120 | marginRight: 0, 121 | maxWidth: 'none' 122 | }; 123 | 124 | const [ featureChild, nonFeatureChildren ] = this.splitFeatureChildren(); 125 | 126 | if (featureChild) { 127 | const unwrapped = this.unwrapChild(featureChild); 128 | if (featureChild !== unwrapped) { 129 | // React.Children.only(featureChild.props.children); 130 | feature = React.cloneElement(featureChild, { 131 | children: React.cloneElement(React.Children.toArray(featureChild.props.children)[0], { 132 | style: featureStyles, 133 | ref: (ref) => this.setFeature(ref) 134 | }) 135 | }); 136 | } else { 137 | feature = React.cloneElement(featureChild, { 138 | style: featureStyles, 139 | ref: (ref) => this.setFeature(ref) 140 | }); 141 | } 142 | } 143 | 144 | return
{ return this.setRoot(ref) }} 148 | > 149 | {feature} 150 | {nonFeatureChildren} 151 |
152 | } 153 | } 154 | 155 | Feature.defaultProps = { 156 | children: [] 157 | }; 158 | 159 | export { Content, Feature as default }; 160 | -------------------------------------------------------------------------------- /components/data-performance.js: -------------------------------------------------------------------------------- 1 | module.exports = [ 2 | { 3 | "nodes": 500, 4 | "theta": 0, 5 | "time": 0.56, 6 | "error": 0 7 | }, 8 | { 9 | "nodes": 500, 10 | "theta": 0.5, 11 | "time": 3.72, 12 | "error": 0.05200202597209933 13 | }, 14 | { 15 | "nodes": 500, 16 | "theta": 1, 17 | "time": 1.62, 18 | "error": 0.05235635117626017 19 | }, 20 | { 21 | "nodes": 500, 22 | "theta": 1.5, 23 | "time": 0.9, 24 | "error": 0.05364959811502322 25 | }, 26 | { 27 | "nodes": 1000, 28 | "theta": 0, 29 | "time": 1.54, 30 | "error": 0 31 | }, 32 | { 33 | "nodes": 1000, 34 | "theta": 0.5, 35 | "time": 6.42, 36 | "error": 0.051100704681022374 37 | }, 38 | { 39 | "nodes": 1000, 40 | "theta": 1, 41 | "time": 2.68, 42 | "error": 0.05145824401863238 43 | }, 44 | { 45 | "nodes": 1000, 46 | "theta": 1.5, 47 | "time": 1.58, 48 | "error": 0.0527730904049427 49 | }, 50 | { 51 | "nodes": 1500, 52 | "theta": 0, 53 | "time": 3.66, 54 | "error": 0 55 | }, 56 | { 57 | "nodes": 1500, 58 | "theta": 0.5, 59 | "time": 11.3, 60 | "error": 0.05065250167285926 61 | }, 62 | { 63 | "nodes": 1500, 64 | "theta": 1, 65 | "time": 4.36, 66 | "error": 0.05100210193824075 67 | }, 68 | { 69 | "nodes": 1500, 70 | "theta": 1.5, 71 | "time": 2.64, 72 | "error": 0.052263724165450914 73 | }, 74 | { 75 | "nodes": 2000, 76 | "theta": 0, 77 | "time": 6.32, 78 | "error": 0 79 | }, 80 | { 81 | "nodes": 2000, 82 | "theta": 0.5, 83 | "time": 16.84, 84 | "error": 0.05040655452845216 85 | }, 86 | { 87 | "nodes": 2000, 88 | "theta": 1, 89 | "time": 6.32, 90 | "error": 0.050773438684784816 91 | }, 92 | { 93 | "nodes": 2000, 94 | "theta": 1.5, 95 | "time": 3.76, 96 | "error": 0.05208289581090376 97 | }, 98 | { 99 | "nodes": 2500, 100 | "theta": 0, 101 | "time": 10, 102 | "error": 0 103 | }, 104 | { 105 | "nodes": 2500, 106 | "theta": 0.5, 107 | "time": 22.48, 108 | "error": 0.05018702377740935 109 | }, 110 | { 111 | "nodes": 2500, 112 | "theta": 1, 113 | "time": 8.04, 114 | "error": 0.05054754976730526 115 | }, 116 | { 117 | "nodes": 2500, 118 | "theta": 1.5, 119 | "time": 5.02, 120 | "error": 0.05183226903289724 121 | }, 122 | { 123 | "nodes": 3000, 124 | "theta": 0, 125 | "time": 14.74, 126 | "error": 0 127 | }, 128 | { 129 | "nodes": 3000, 130 | "theta": 0.5, 131 | "time": 27.68, 132 | "error": 0.050308818616019844 133 | }, 134 | { 135 | "nodes": 3000, 136 | "theta": 1, 137 | "time": 10.06, 138 | "error": 0.05067487078391384 139 | }, 140 | { 141 | "nodes": 3000, 142 | "theta": 1.5, 143 | "time": 6.22, 144 | "error": 0.05199211876791672 145 | }, 146 | { 147 | "nodes": 3500, 148 | "theta": 0, 149 | "time": 20.64, 150 | "error": 0 151 | }, 152 | { 153 | "nodes": 3500, 154 | "theta": 0.5, 155 | "time": 33.62, 156 | "error": 0.05004853487486027 157 | }, 158 | { 159 | "nodes": 3500, 160 | "theta": 1, 161 | "time": 12.2, 162 | "error": 0.05041703116269491 163 | }, 164 | { 165 | "nodes": 3500, 166 | "theta": 1.5, 167 | "time": 7.7, 168 | "error": 0.05170552704388485 169 | }, 170 | { 171 | "nodes": 4000, 172 | "theta": 0, 173 | "time": 27.32, 174 | "error": 0 175 | }, 176 | { 177 | "nodes": 4000, 178 | "theta": 0.5, 179 | "time": 38.88, 180 | "error": 0.05019060517654669 181 | }, 182 | { 183 | "nodes": 4000, 184 | "theta": 1, 185 | "time": 14.58, 186 | "error": 0.05056004954031823 187 | }, 188 | { 189 | "nodes": 4000, 190 | "theta": 1.5, 191 | "time": 8.9, 192 | "error": 0.051869004205317155 193 | }, 194 | { 195 | "nodes": 4500, 196 | "theta": 0, 197 | "time": 34.54, 198 | "error": 0 199 | }, 200 | { 201 | "nodes": 4500, 202 | "theta": 0.5, 203 | "time": 44.4, 204 | "error": 0.050120388342931955 205 | }, 206 | { 207 | "nodes": 4500, 208 | "theta": 1, 209 | "time": 16.88, 210 | "error": 0.0504893948312208 211 | }, 212 | { 213 | "nodes": 4500, 214 | "theta": 1.5, 215 | "time": 10.38, 216 | "error": 0.05178804015573142 217 | }, 218 | { 219 | "nodes": 5000, 220 | "theta": 0, 221 | "time": 43.5, 222 | "error": 0 223 | }, 224 | { 225 | "nodes": 5000, 226 | "theta": 0.5, 227 | "time": 49.82, 228 | "error": 0.0502078881851173 229 | }, 230 | { 231 | "nodes": 5000, 232 | "theta": 1, 233 | "time": 19.42, 234 | "error": 0.050556513572848315 235 | }, 236 | { 237 | "nodes": 5000, 238 | "theta": 1.5, 239 | "time": 12.36, 240 | "error": 0.05182141623831191 241 | }, 242 | { 243 | "nodes": 5500, 244 | "theta": 0, 245 | "time": 52.86, 246 | "error": 0 247 | }, 248 | { 249 | "nodes": 5500, 250 | "theta": 0.5, 251 | "time": 56.04, 252 | "error": 0.05000671534740984 253 | }, 254 | { 255 | "nodes": 5500, 256 | "theta": 1, 257 | "time": 21.84, 258 | "error": 0.05036747490974292 259 | }, 260 | { 261 | "nodes": 5500, 262 | "theta": 1.5, 263 | "time": 13.16, 264 | "error": 0.051636964809069846 265 | }, 266 | { 267 | "nodes": 6000, 268 | "theta": 0, 269 | "time": 63.12, 270 | "error": 0 271 | }, 272 | { 273 | "nodes": 6000, 274 | "theta": 0.5, 275 | "time": 62.34, 276 | "error": 0.050108840001680185 277 | }, 278 | { 279 | "nodes": 6000, 280 | "theta": 1, 281 | "time": 25.44, 282 | "error": 0.0504845789284521 283 | }, 284 | { 285 | "nodes": 6000, 286 | "theta": 1.5, 287 | "time": 15.1, 288 | "error": 0.05178990719815004 289 | }, 290 | { 291 | "nodes": 6500, 292 | "theta": 0, 293 | "time": 74.02, 294 | "error": 0 295 | }, 296 | { 297 | "nodes": 6500, 298 | "theta": 0.5, 299 | "time": 68.32, 300 | "error": 0.05015978570893315 301 | }, 302 | { 303 | "nodes": 6500, 304 | "theta": 1, 305 | "time": 27.22, 306 | "error": 0.050524547776730995 307 | }, 308 | { 309 | "nodes": 6500, 310 | "theta": 1.5, 311 | "time": 16.58, 312 | "error": 0.051807786978559624 313 | }, 314 | { 315 | "nodes": 7000, 316 | "theta": 0, 317 | "time": 86.86, 318 | "error": 0 319 | }, 320 | { 321 | "nodes": 7000, 322 | "theta": 0.5, 323 | "time": 75.84, 324 | "error": 0.05009715454588301 325 | }, 326 | { 327 | "nodes": 7000, 328 | "theta": 1, 329 | "time": 28.96, 330 | "error": 0.050464252838042886 331 | }, 332 | { 333 | "nodes": 7000, 334 | "theta": 1.5, 335 | "time": 18.34, 336 | "error": 0.05175272301004472 337 | }, 338 | { 339 | "nodes": 7500, 340 | "theta": 0, 341 | "time": 97.92, 342 | "error": 0 343 | }, 344 | { 345 | "nodes": 7500, 346 | "theta": 0.5, 347 | "time": 80.12, 348 | "error": 0.050103625186719125 349 | }, 350 | { 351 | "nodes": 7500, 352 | "theta": 1, 353 | "time": 31.7, 354 | "error": 0.05046902952584346 355 | }, 356 | { 357 | "nodes": 7500, 358 | "theta": 1.5, 359 | "time": 20.72, 360 | "error": 0.051759162426569125 361 | }, 362 | { 363 | "nodes": 8000, 364 | "theta": 0, 365 | "time": 114.32, 366 | "error": 0 367 | }, 368 | { 369 | "nodes": 8000, 370 | "theta": 0.5, 371 | "time": 88.52, 372 | "error": 0.05010771874184223 373 | }, 374 | { 375 | "nodes": 8000, 376 | "theta": 1, 377 | "time": 34.52, 378 | "error": 0.05046208406423784 379 | }, 380 | { 381 | "nodes": 8000, 382 | "theta": 1.5, 383 | "time": 22.96, 384 | "error": 0.051739305168694634 385 | }, 386 | { 387 | "nodes": 8500, 388 | "theta": 0, 389 | "time": 129.82, 390 | "error": 0 391 | }, 392 | { 393 | "nodes": 8500, 394 | "theta": 0.5, 395 | "time": 97.96, 396 | "error": 0.04991075721026138 397 | }, 398 | { 399 | "nodes": 8500, 400 | "theta": 1, 401 | "time": 35.4, 402 | "error": 0.050271992053986664 403 | }, 404 | { 405 | "nodes": 8500, 406 | "theta": 1.5, 407 | "time": 24.1, 408 | "error": 0.051558114104811856 409 | }, 410 | { 411 | "nodes": 9000, 412 | "theta": 0, 413 | "time": 148.58, 414 | "error": 0 415 | }, 416 | { 417 | "nodes": 9000, 418 | "theta": 0.5, 419 | "time": 105.14, 420 | "error": 0.049955850454765756 421 | }, 422 | { 423 | "nodes": 9000, 424 | "theta": 1, 425 | "time": 38.02, 426 | "error": 0.050320401669984936 427 | }, 428 | { 429 | "nodes": 9000, 430 | "theta": 1.5, 431 | "time": 25.9, 432 | "error": 0.051606533672847184 433 | }, 434 | { 435 | "nodes": 9500, 436 | "theta": 0, 437 | "time": 163.66, 438 | "error": 0 439 | }, 440 | { 441 | "nodes": 9500, 442 | "theta": 0.5, 443 | "time": 110.06, 444 | "error": 0.050049450254373 445 | }, 446 | { 447 | "nodes": 9500, 448 | "theta": 1, 449 | "time": 40.44, 450 | "error": 0.05040925481053241 451 | }, 452 | { 453 | "nodes": 9500, 454 | "theta": 1.5, 455 | "time": 27.48, 456 | "error": 0.0516827503789717 457 | }, 458 | { 459 | "nodes": 10000, 460 | "theta": 0, 461 | "time": 181.56, 462 | "error": 0 463 | }, 464 | { 465 | "nodes": 10000, 466 | "theta": 0.5, 467 | "time": 112.24, 468 | "error": 0.050078126512543175 469 | }, 470 | { 471 | "nodes": 10000, 472 | "theta": 1, 473 | "time": 43.94, 474 | "error": 0.05043578694613407 475 | }, 476 | { 477 | "nodes": 10000, 478 | "theta": 1.5, 479 | "time": 30.7, 480 | "error": 0.05171306545026989 481 | } 482 | ]; -------------------------------------------------------------------------------- /index.idl: -------------------------------------------------------------------------------- 1 | [meta 2 | title:"The Barnes-Hut Approximation" 3 | description:"Efficient computation of N-body forces" /] 4 | 5 | [header 6 | title:"The Barnes-Hut Approximation" 7 | subtitle:"Efficient computation of N-body forces" 8 | author:"Jeffrey Heer" 9 | authorLink:"http://jheer.org" /] 10 | 11 | [var name:"charge" value:-30 /] 12 | [var name:"step" value:0 /] 13 | [var name:"accum" value:0 /] 14 | [var name:"theta" value:0 /] 15 | [var name:"layout" value:true /] 16 | [var name:"estimate" value:false /] 17 | [var name:"focus" value:false /] 18 | 19 | [fixed] 20 | [BarnesHut 21 | size:step 22 | theta:theta 23 | charge:charge 24 | layout:`layout && !(estimate || focus)` 25 | estimate:`estimate || focus;` 26 | accumulate:accum 27 | /] 28 | [/fixed] 29 | 30 | Computers can serve as exciting tools for discovery, with which we can 31 | model and explore complex phenomena. For example, to test theories about 32 | the formation of the universe, we can perform _simulations_ to predict 33 | how galaxies evolve. To do this, we could gather the estimated mass and 34 | location of stars and then model their gravitational interactions over time. 35 | 36 | Another avenue for discovery is to _visualize_ complex information to 37 | reveal structure and patterns. Consider this network diagram, showing 38 | connections between people in a social network. We can use such diagrams 39 | to examine community groups and identify people who bridge between them. 40 | 41 | Though they may seem quite different at first, these two examples share 42 | a common need: they require computing forces that arise from pairwise 43 | interactions among a set of points, often referred to as an _N-body problem_. 44 | In the case of astronomical simulation, we seek to model the 45 | gravitational forces among stars. In the case of network visualization, 46 | we compute the layout using a similar physical simulation: 47 | nodes in the network act as charged particles that repel each other, while 48 | links act as springs that pull related nodes together. 49 | 50 | _To get a sense of how this force-directed layout works, drag the nodes or 51 | use the slider to adjust the force strength._ Negative values indicate 52 | repulsive forces, while positive values indicate attractive forces. 53 | 54 | [p] 55 | [em]Force Strength [/em] 56 | [Range min:-30 max:10 step:1 value:charge /] 57 | [Display value:charge /] 58 | [/p] 59 | 60 | A straightforward approach to computing N-body forces is to consider all 61 | pairs of individual points and add up the contributions of each 62 | interaction. This naïve scheme has _quadratic complexity_: as the number 63 | of points [i]n[/i] increases, the running time grows proportionally 64 | to [i]n[/i][sup]2[/sup], quickly leading to intractably long calculations. 65 | How might we do better? 66 | 67 | # The Barnes-Hut Approximation 68 | 69 | To accelerate computation and make large-scale simulations possible, the 70 | astronomers Josh Barnes and Piet Hut devised a clever scheme. 71 | The key idea is to approximate long-range 72 | forces by replacing a group of distant points with their center 73 | of mass. In exchange for a small amount of error, 74 | this scheme significantly speeds up calculation, with 75 | complexity [i]n log n[/i] rather than [i]n[/i][sup]2[/sup]. 76 | 77 | Central to this approximation is a _spatial index_: a "map" of space 78 | that helps us model groups of points as a single center of mass. In 79 | two dimensions, we can use a [quadtree](https://en.wikipedia.org/wiki/Quadtree) 80 | data structure, which recursively 81 | subdivides square regions of space into four equal-sized quadrants. (In three dimensions, one can use an analogous [octree](https://en.wikipedia.org/wiki/Octree) that divides a cubic volume into eight sub-cubes.) 82 | 83 | The Barnes-Hut approximation involves three steps: 84 | 85 | 1. Construct the spatial index (e.g., quadtree) 86 | 2. Calculate centers of mass 87 | 3. Estimate forces 88 | 89 | Let's explore each step in turn. We will assume we are computing _repulsive_ 90 | forces for the purposes of network layout. This setup is akin to modeling 91 | anti-gravity or electric forces with similarly-charged particles. While we 92 | will use the term "center of mass", this could readily 93 | be replaced with "center of charge". 94 | 95 | [em]As you read through, click the [/em] 96 | [action onClick:`alert('👍 🎉');`]action links[/action] 97 | [em] to update the diagram![/em] 98 | 99 | 100 | // Quadtree Construction 101 | 102 | ## Step 1: Construct the Quadtree 103 | 104 | We begin with [action onClick:`layout=false; step=0;`]a set of 105 | two-dimensional points as input[/action]. 106 | When we [action onClick:`layout=false; step=1;`]insert the first point into 107 | the quadtree[/action], it is added to the top-level root cell of the tree. 108 | 109 | [action onClick:`layout=false; step=2;`]Next we insert another point[/action], 110 | which requires expanding the tree by subdiving the space. 111 | Upon [action onClick:`layout=false; step=Math.min(step+1, 77);`]each subsequent 112 | insertion[/action], more fine-grained cells may be added until all points 113 | reside in their own cell. 114 | 115 | _Advance the slider to add each point and produce the full quadtree_. 116 | 117 | [p] 118 | [em]Inserted Points [/em] 119 | [Range min:0 max:77 step:1 value:step /] 120 | [Display value:`step;` format:"d" /] 121 | [/p] 122 | 123 | 124 | // Accumulate 125 | 126 | ## Step 2: Calculate Centers of Mass 127 | 128 | After quadtree construction, [action 129 | onClick:`layout=false; accum=(accum+1)%2;` 130 | ] we calculate centers of mass for each cell of the tree[/action]. 131 | The center of mass of a quadtree cell is simply the weighted 132 | average of the centers of its four child cells. 133 | 134 | We visit the leaf node cells first and then visit subsequent parent 135 | cells, merging data as we pass upwards through the tree. 136 | Once the traversal completes, each cell has been updated with the 137 | position and strength of its center of mass. 138 | 139 | 140 | // Force Estimation 141 | 142 | ## Step 3: Estimate N-Body Forces 143 | 144 | Now we are ready to estimate forces! 145 | 146 | [action onClick:`theta=0; estimate=true;`]To measure forces at a given 147 | point, let's add a "probe" to our diagram[/action] ([probe/]). The purple 148 | line extending from the probe indicates the direction and magnitude of 149 | the total force at that location. (To promote visibility, 150 | the purple line is three times longer than the actual 151 | pixel distance the probe would be moved in a single timestep of 152 | the force simulation.) The dotted lines extending to the probe 153 | represent the force components exerted by individual points. 154 | 155 | _Move the probe (click or 156 | drag in the visualization) to explore the force field_. 157 | 158 | Ignoring the quadtree, we can naïvely calculate forces by summing the 159 | contributions of _all_ individual points. Of course, we would instead like 160 | to use the quadtree to accelerate calculation and approximate long-range 161 | forces. Rather than compute interactions among individual 162 | points, [action onClick:`theta=1; estimate=true;`]we can compute interactions 163 | with centers of mass, using smaller quadtree cells for nearer points and 164 | larger cells for more distant points.[/action] 165 | 166 | At this point we've skipped a critical detail: what constitutes 167 | "long-range" versus "short-range" forces? We consider both 168 | the _distance_ to the center of a quadtree cell and that cell's _width_. 169 | If the ratio _width / distance_ falls below a chosen threshold 170 | (a parameter named _theta_), we treat the quadtree cell as a 171 | source of long-range forces and use its center of mass. 172 | Otherwise, we will recursively visit the child cells in the quadtree. 173 | 174 | When theta = 1, a quadtree cell's center of mass will be used — and its 175 | internal points ignored — if the distance from the sample point to the 176 | cell's center is greater than or equal to the cell's width. 177 | 178 | _Adjust the theta parameter to view its effect on force estimation_. How does 179 | the number of considered points change based on the probe location and theta? 180 | How does the direction and magnitude of the total force vary with theta? 181 | 182 | [p] 183 | [em]Theta [/em] 184 | [Range min:0 max:2 step:0.1 value:theta /] 185 | [Display value:theta /] 186 | [/p] 187 | 188 | We can now perform force estimation for each individual point, using the 189 | Barnes-Hut approximation to limit the total number of comparisons! 190 | 191 | # Performance Analysis 192 | 193 | To assess the performance of the Barnes-Hut approximation, let's look at 194 | both the running time and accuracy of force estimation. We will compare 195 | naïve ([i]n[/i][sup]2[/sup]) calculation to different settings of the theta 196 | parameter. 197 | 198 | We will take measurements using different point sets, 199 | ranging from 500 to 10,000 points. For each point count, 200 | we average the results from 50 separate runs of force estimation, 201 | each time using a different set of points placed at uniformly 202 | random coordinates within a 900 x 500 pixel rectangle. 203 | 204 | [TimePlot focus:focus theta:theta /] 205 | 206 | The running time results confirm that the Barnes-Hut 207 | approximation can significantly speed-up computation. 208 | As expected, the 209 | [Theta value:0 theta:theta focus:focus /] 210 | exhibits a quadratic relationship, whereas increasing the theta parameter 211 | leads to faster calculations. A low setting of 212 | [Theta value:0.5 theta:theta focus:focus /] 213 | does not fare better than the naïve 214 | approach until processing about 6,000 points. Until that point, 215 | the overhead of quadtree construction and center of mass 216 | calculation outstrips any gains in force 217 | estimation. For 218 | [Theta value:1 theta:theta focus:focus /] and 219 | [Theta value:1.5 theta:theta focus:focus /], 220 | however, we see a 221 | significant improvement in running time, with similar 222 | performance for each. 223 | 224 | To evaluate approximation error, we measure the average vector 225 | distance between the results of the naïve scheme and Barnes-Hut. 226 | In the context of a force-directed graph layout, this error 227 | represents the difference (in pixels) between node positions after 228 | applying the naïve and approximate methods. 229 | 230 | [ErrorPlot focus:focus theta:theta /] 231 | 232 | Looking at the error results, we first see that the average 233 | error is relatively small: only ~5% of a single pixel in 234 | difference! However, we should take care in interpreting these 235 | results, as we use the _average_ error per point and 236 | the _maximum_ error may be substantially higher. While 237 | [Theta value:1 theta:theta focus:focus /] and 238 | [Theta value:1.5 theta:theta focus:focus /] exhibit similar _running times_, 239 | here we see notably higher _error rates_ for 240 | [Theta value:1.5 theta:theta focus:focus /] 241 | versus 242 | [Theta value:1 theta:theta focus:focus /] and 243 | [Theta value:0.5 theta:theta focus:focus /]. 244 | 245 | These results suggest that a good default value for theta 246 | — with low running time _and_ low approximation error 247 | — is around 1.0. Indeed, in practice it is common to see default 248 | settings slightly below 1. In 249 | visualization applications, where errors on the order of a few 250 | pixels are not a problem, even higher theta values may be used 251 | without issue. 252 | 253 | # Conclusion 254 | 255 | The Barnes-Hut approximation has had a major impact on both physical 256 | simulation and network visualization, enabling n-body calculations 257 | to scale to much larger data sets than naïve force calculation permits. 258 | 259 | [action onClick:`step=77; estimate=false, layout=true;`]Returning to our initial 260 | network diagram[/action], we can use Barnes-Hut to efficiently compute 261 | repulsive forces at each timestep. For each animation frame, we perform the 262 | approximation anew, creating a new quadtree, accumulating centers of mass, 263 | and (approximately) estimating forces. 264 | 265 | Want to learn more or apply Barnes-Hut in your own work? 266 | 267 | * The popular [D3.js](https://d3js.org) library by Mike Bostock includes Barnes-Hut for performing force-directed layout as part of the [d3-force module](https://github.com/d3/d3-force). The examples in this article are based on the D3 implementation. 268 | * For more details, including pseudo-code and performance analysis, Prof. James Demmel of UC Berkeley has [online lecture notes on the Barnes-Hut approximation](https://people.eecs.berkeley.edu/~demmel/cs267/lecture26/lecture26.html). These notes provided my first introduction to the technique! 269 | * While the error of the Barnes-Hut approximation is acceptable in a variety of applications, sometimes greater precision is needed. In such cases, more complicated schemes such as the [Fast Multipole Method](https://en.wikipedia.org/wiki/Fast_multipole_method) can be used in lieu of Barnes-Hut. 270 | 271 | This article was created using [Idyll](http://idyll-lang.org/), with 272 | visualizations powered by [D3](https://d3js.org/) 273 | and [Vega](https://vega.github.io/vega/). 274 | The [source code](https://github.com/jheer/barnes-hut) is available on GitHub. -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | The Barnes-Hut Approximation 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 |

The Barnes-Hut Approximation

Efficient computation of N-body forces

Computers can serve as exciting tools for discovery, with which we can 18 | model and explore complex phenomena. For example, to test theories about 19 | the formation of the universe, we can perform simulations to predict 20 | how galaxies evolve. To do this, we could gather the estimated mass and 21 | location of stars and then model their gravitational interactions over time.

Another avenue for discovery is to visualize complex information to 22 | reveal structure and patterns. Consider this network diagram, showing 23 | connections between people in a social network. We can use such diagrams 24 | to examine community groups and identify people who bridge between them.

Though they may seem quite different at first, these two examples share 25 | a common need: they require computing forces that arise from pairwise 26 | interactions among a set of points, often referred to as an N-body problem. 27 | In the case of astronomical simulation, we seek to model the 28 | gravitational forces among stars. In the case of network visualization, 29 | we compute the layout using a similar physical simulation: 30 | nodes in the network act as charged particles that repel each other, while 31 | links act as springs that pull related nodes together.

To get a sense of how this force-directed layout works, drag the nodes or 32 | use the slider to adjust the force strength. Negative values indicate 33 | repulsive forces, while positive values indicate attractive forces.

Force Strength -30.00

A straightforward approach to computing N-body forces is to consider all 34 | pairs of individual points and add up the contributions of each 35 | interaction. This naïve scheme has quadratic complexity: as the number 36 | of points n increases, the running time grows proportionally 37 | to n2, quickly leading to intractably long calculations. 38 | How might we do better?

The Barnes-Hut Approximation

To accelerate computation and make large-scale simulations possible, the 39 | astronomers Josh Barnes and Piet Hut devised a clever scheme. 40 | The key idea is to approximate long-range 41 | forces by replacing a group of distant points with their center 42 | of mass. In exchange for a small amount of error, 43 | this scheme significantly speeds up calculation, with 44 | complexity n log n rather than n2.

Central to this approximation is a spatial index: a “map” of space 45 | that helps us model groups of points as a single center of mass. In 46 | two dimensions, we can use a quadtree 47 | data structure, which recursively 48 | subdivides square regions of space into four equal-sized quadrants. (In three dimensions, one can use an analogous octree that divides a cubic volume into eight sub-cubes.)

The Barnes-Hut approximation involves three steps:

  1. Construct the spatial index (e.g., quadtree)
  2. Calculate centers of mass
  3. Estimate forces

49 | Let’s explore each step in turn. We will assume we are computing repulsive 50 | forces for the purposes of network layout. This setup is akin to modeling 51 | anti-gravity or electric forces with similarly-charged particles. While we 52 | will use the term “center of mass”, this could readily 53 | be replaced with “center of charge”.

As you read through, click the action links to update the diagram!

Step 1: Construct the Quadtree

We begin with a set of 54 | two-dimensional points as input. 55 | When we insert the first point into 56 | the quadtree, it is added to the top-level root cell of the tree.

Next we insert another point, 57 | which requires expanding the tree by subdiving the space. 58 | Upon each subsequent 59 | insertion, more fine-grained cells may be added until all points 60 | reside in their own cell.

Advance the slider to add each point and produce the full quadtree.

Inserted Points 0

Step 2: Calculate Centers of Mass

After quadtree construction, we calculate centers of mass for each cell of the tree. 61 | The center of mass of a quadtree cell is simply the weighted 62 | average of the centers of its four child cells.

We visit the leaf node cells first and then visit subsequent parent 63 | cells, merging data as we pass upwards through the tree. 64 | Once the traversal completes, each cell has been updated with the 65 | position and strength of its center of mass.

Step 3: Estimate N-Body Forces

Now we are ready to estimate forces!

To measure forces at a given 66 | point, let's add a "probe" to our diagram (). The purple 67 | line extending from the probe indicates the direction and magnitude of 68 | the total force at that location. (To promote visibility, 69 | the purple line is three times longer than the actual 70 | pixel distance the probe would be moved in a single timestep of 71 | the force simulation.) The dotted lines extending to the probe 72 | represent the force components exerted by individual points.

Move the probe (click or 73 | drag in the visualization) to explore the force field.

Ignoring the quadtree, we can naïvely calculate forces by summing the 74 | contributions of all individual points. Of course, we would instead like 75 | to use the quadtree to accelerate calculation and approximate long-range 76 | forces. Rather than compute interactions among individual 77 | points, we can compute interactions 78 | with centers of mass, using smaller quadtree cells for nearer points and 79 | larger cells for more distant points.

At this point we’ve skipped a critical detail: what constitutes 80 | “long-range” versus “short-range” forces? We consider both 81 | the distance to the center of a quadtree cell and that cell’s width. 82 | If the ratio width / distance falls below a chosen threshold 83 | (a parameter named theta), we treat the quadtree cell as a 84 | source of long-range forces and use its center of mass. 85 | Otherwise, we will recursively visit the child cells in the quadtree.

When theta = 1, a quadtree cell’s center of mass will be used — and its 86 | internal points ignored — if the distance from the sample point to the 87 | cell’s center is greater than or equal to the cell’s width.

Adjust the theta parameter to view its effect on force estimation. How does 88 | the number of considered points change based on the probe location and theta? 89 | How does the direction and magnitude of the total force vary with theta?

Theta 0.00

We can now perform force estimation for each individual point, using the 90 | Barnes-Hut approximation to limit the total number of comparisons!

Performance Analysis

To assess the performance of the Barnes-Hut approximation, let’s look at 91 | both the running time and accuracy of force estimation. We will compare 92 | naïve (n2) calculation to different settings of the theta 93 | parameter.

We will take measurements using different point sets, 94 | ranging from 500 to 10,000 points. For each point count, 95 | we average the results from 50 separate runs of force estimation, 96 | each time using a different set of points placed at uniformly 97 | random coordinates within a 900 x 500 pixel rectangle.

The running time results confirm that the Barnes-Hut 98 | approximation can significantly speed-up computation. 99 | As expected, the naïve approach 100 | exhibits a quadratic relationship, whereas increasing the theta parameter 101 | leads to faster calculations. A low setting of theta=0.5 102 | does not fare better than the naïve 103 | approach until processing about 6,000 points. Until that point, 104 | the overhead of quadtree construction and center of mass 105 | calculation outstrips any gains in force 106 | estimation. For theta=1 and theta=1.5, 107 | however, we see a 108 | significant improvement in running time, with similar 109 | performance for each.

To evaluate approximation error, we measure the average vector 110 | distance between the results of the naïve scheme and Barnes-Hut. 111 | In the context of a force-directed graph layout, this error 112 | represents the difference (in pixels) between node positions after 113 | applying the naïve and approximate methods.

Looking at the error results, we first see that the average 114 | error is relatively small: only ~5% of a single pixel in 115 | difference! However, we should take care in interpreting these 116 | results, as we use the average error per point and 117 | the maximum error may be substantially higher. While theta=1 and theta=1.5 exhibit similar running times, 118 | here we see notably higher error rates for theta=1.5 119 | versus theta=1 and theta=0.5.

These results suggest that a good default value for theta 120 | — with low running time and low approximation error 121 | — is around 1.0. Indeed, in practice it is common to see default 122 | settings slightly below 1. In 123 | visualization applications, where errors on the order of a few 124 | pixels are not a problem, even higher theta values may be used 125 | without issue.

Conclusion

The Barnes-Hut approximation has had a major impact on both physical 126 | simulation and network visualization, enabling n-body calculations 127 | to scale to much larger data sets than naïve force calculation permits.

Returning to our initial 128 | network diagram, we can use Barnes-Hut to efficiently compute 129 | repulsive forces at each timestep. For each animation frame, we perform the 130 | approximation anew, creating a new quadtree, accumulating centers of mass, 131 | and (approximately) estimating forces.

Want to learn more or apply Barnes-Hut in your own work?

  • The popular D3.js library by Mike Bostock includes Barnes-Hut for performing force-directed layout as part of the d3-force module. The examples in this article are based on the D3 implementation.
  • For more details, including pseudo-code and performance analysis, Prof. James Demmel of UC Berkeley has online lecture notes on the Barnes-Hut approximation. These notes provided my first introduction to the technique!
  • While the error of the Barnes-Hut approximation is acceptable in a variety of applications, sometimes greater precision is needed. In such cases, more complicated schemes such as the Fast Multipole Method can be used in lieu of Barnes-Hut.

132 | This article was created using Idyll, with 133 | visualizations powered by D3 134 | and Vega. 135 | The source code is available on GitHub.

136 | 137 | 138 | 139 | -------------------------------------------------------------------------------- /components/quadtree.js: -------------------------------------------------------------------------------- 1 | const d3 = require('d3'); 2 | 3 | const graph = require('./data-network'), 4 | nodes = graph.nodes, 5 | links = graph.links; 6 | 7 | const quadColor = '#d5aaaa', 8 | pointColor = 'firebrick'; 9 | 10 | // helper function to map from d3.mouse array to x/y object 11 | function toPoint(xy) { 12 | return {x: xy[0], y: xy[1]}; 13 | } 14 | 15 | module.exports = function(dom, opt) { 16 | // init svg dom 17 | const w = opt.width, 18 | h = opt.height, 19 | el = d3.select(dom), 20 | svg = el.append('svg').attr('width', w).attr('height', h), 21 | gg = svg.append('g'), 22 | eg = gg.append('g'), 23 | qg = gg.append('g'), 24 | fg = gg.append('g'), 25 | lg = gg.append('g'), 26 | ng = gg.append('g'), 27 | cg = gg.append('g'); 28 | 29 | // constants 30 | const baseRadius = opt.radius || 4, 31 | defaultExtent = [[1, 1], [513, 513]], 32 | defaultProbe = {x: w / 2 + 64, y: h / 2 + 64}; 33 | 34 | // force simulation 35 | const nbodyForce = d3.forceManyBody(), 36 | linkForce = d3.forceLink(), 37 | xyForce = d3.forceCenter().x(w / 2).y(h / 2), 38 | simulation = d3.forceSimulation() 39 | .force('link', linkForce) 40 | .force('charge', nbodyForce) 41 | .force('center', xyForce); 42 | 43 | // state variables 44 | let theta2 = Math.sqrt(opt.theta) || 0, 45 | quad = null, 46 | size = 0, 47 | layout = true, 48 | active = false, 49 | probePoint = defaultProbe, 50 | probeDown = false, 51 | es, ns, obj; 52 | 53 | 54 | // -- INITIALIZATION -- 55 | 56 | // initialize diagram 57 | function init() { 58 | es = eg.selectAll('line') 59 | .data(links) 60 | .enter().append('line') 61 | .style('stroke', '#ccc') 62 | .style('stroke-width', 1) 63 | .style('stroke-opacity', 1) 64 | .style('pointer-events', 'none'); 65 | 66 | ns = ng.selectAll('circle') 67 | .data(nodes) 68 | .enter().append('circle') 69 | .attr('cx', d => d.x) 70 | .attr('cy', d => d.y) 71 | .attr('r', baseRadius) 72 | .style('fill', '#666') 73 | .style('fill-opacity', 1) 74 | .style('cursor', 'pointer'); 75 | 76 | simulation 77 | .nodes(nodes) 78 | .on('tick', onTick); 79 | 80 | simulation.force('link') 81 | .links(links); 82 | 83 | reinit(); 84 | size = 0; 85 | qg.selectAll('rect').style('opacity', 0); 86 | ns.style('fill-opacity', 1); 87 | 88 | // add probe interaction to visualization 89 | svg.on('mousemove', onProbeMove) 90 | .on('mousedown', onProbeDown); 91 | 92 | // add drag interaction to layout 93 | ns.call(d3.drag() 94 | .on('start', onDragStart) 95 | .on('drag', onDrag) 96 | .on('end', onDragEnd)); 97 | 98 | return obj; 99 | } 100 | 101 | // reinitialize upon state change 102 | function reinit() { 103 | if (size != nodes.length) { 104 | initQuadTree(nodes); 105 | } 106 | quad.visitAfter(accumulate); 107 | quads().clear(); 108 | ns.style('fill-opacity', 0.25); 109 | } 110 | 111 | // quadtree initialization 112 | function initQuadTree(nodes) { 113 | quad = d3.quadtree() 114 | .extent(defaultExtent) 115 | .x(d => d.x) 116 | .y(d => d.y); 117 | if (nodes) quad.addAll(nodes); 118 | size = nodes ? nodes.length : 0; 119 | } 120 | 121 | 122 | // -- EVENT LISTENERS -- 123 | 124 | function onTick() { 125 | if (size > 0) { 126 | initQuadTree(nodes); 127 | quads(); 128 | } 129 | es.attr('x1', d => d.source.x) 130 | .attr('y1', d => d.source.y) 131 | .attr('x2', d => d.target.x) 132 | .attr('y2', d => d.target.y); 133 | ns.attr('cx', d => d.x) 134 | .attr('cy', d => d.y); 135 | } 136 | 137 | function onDragStart(d) { 138 | if (!layout) return; 139 | if (!d3.event.active) simulation.alphaTarget(0.3).restart(); 140 | d.fx = d.x; 141 | d.fy = d.y; 142 | } 143 | 144 | function onDrag(d) { 145 | if (!layout) return; 146 | d.fx = d3.event.x; 147 | d.fy = d3.event.y; 148 | } 149 | 150 | function onDragEnd(d) { 151 | if (!layout) return; 152 | if (!d3.event.active) simulation.alphaTarget(0); 153 | d.fx = null; 154 | d.fy = null; 155 | } 156 | 157 | function onProbeMove() { 158 | if (active && probeDown) { 159 | probe(toPoint(d3.mouse(this))); 160 | } 161 | } 162 | 163 | function onProbeUp() { 164 | probeDown = false; 165 | window.removeEventListener('mouseup', onProbeUp); 166 | } 167 | 168 | function onProbeDown() { 169 | if (!active) return; 170 | 171 | probeDown = true; 172 | window.addEventListener('mouseup', onProbeUp); 173 | 174 | let t = d3.event.target; 175 | probe((t && t.localName == 'circle') 176 | ? t.__data__ 177 | : toPoint(d3.mouse(this))); 178 | } 179 | 180 | 181 | // -- QUADTREE METHODS -- 182 | 183 | // computer centers of mass 184 | function accumulate(quad, x1, y1, x2, y2) { 185 | let strength = 0, q, c, x, y, i; 186 | 187 | // For internal nodes, accumulate forces from child quadrants. 188 | if (quad.length) { 189 | let pid = [x1,y1,x2,y2].join(','); 190 | for (x = y = i = 0; i < 4; ++i) { 191 | if ((q = quad[i]) && (c = q.value)) { 192 | strength += c, x += c * q.x, y += c * q.y; 193 | q.parent = quad; 194 | q.pid = pid; 195 | } 196 | } 197 | quad.x = x / strength; 198 | quad.y = y / strength; 199 | quad.x1 = x1; 200 | quad.y1 = y1; 201 | quad.w = x2 - x1; 202 | quad.h = y2 - y1; 203 | } 204 | 205 | // For leaf nodes, accumulate forces from coincident quadrants. 206 | else { 207 | q = quad; 208 | q.x = q.data.x; 209 | q.y = q.data.y; 210 | do strength += 1; 211 | while (q = q.next); 212 | } 213 | 214 | quad.value = strength; 215 | } 216 | 217 | // return the quadtree path for a point as an array of extents 218 | function getPath(p) { 219 | let path = []; 220 | 221 | quad.visit(function(node, x1, y1, x2, y2) { 222 | // if point is not contained in node, abandon branch 223 | if (p.x < x1 || p.x >= x2 || p.y < y1 || p.y >= y2) { 224 | return true; 225 | } 226 | path.push({x1: x1, y1: y1, w: x2 - x1, h: y2 - y1}); 227 | }); 228 | 229 | return path; 230 | } 231 | 232 | 233 | // -- DIAGRAM UPDATE METHODS -- 234 | 235 | // clear annotations 236 | function clear() { 237 | fg.html(''); 238 | lg.html(''); 239 | cg.html(''); 240 | ns.style('fill-opacity', 1); 241 | return obj; 242 | } 243 | 244 | // collect and draw quadtree rectangles 245 | function quads() { 246 | let boxes = []; 247 | 248 | function processNode(node, extent, depth) { 249 | if (Array.isArray(node)) { 250 | processSplit(node, extent, depth); 251 | } 252 | } 253 | 254 | function processSplit(node, extent, depth) { 255 | let lo = extent[0], 256 | hi = extent[1], 257 | mp = [(lo[0] + hi[0]) >> 1, (lo[1] + hi[1]) >> 1]; 258 | 259 | let e = [ 260 | [lo, mp], 261 | [[mp[0], lo[1]], [hi[0], mp[1]]], 262 | [[lo[0], mp[1]], [mp[0], hi[1]]], 263 | [mp, hi] 264 | ]; 265 | for (let i=0; i<4; ++i) { 266 | boxes.push(e[i]); 267 | e[i].depth = depth; 268 | if (node[i]) processNode(node[i], e[i], depth + 1); 269 | } 270 | } 271 | 272 | if (quad.root()) { 273 | // add quadtree root extents 274 | boxes.push(quad.extent().slice()); 275 | // recurse to process quadtree content 276 | processNode(quad.root(), quad.extent(), 1); 277 | } 278 | 279 | qg.html('') 280 | .selectAll('rect').data(boxes) 281 | .enter().append('rect') 282 | .attr('x', q => q[0][0] + 0.5) 283 | .attr('y', q => q[0][1] + 0.5) 284 | .attr('width', q => q[1][0] - q[0][0]) 285 | .attr('height', q => q[1][1] - q[0][1]) 286 | .style('fill', 'none') 287 | .style('stroke', '#ddd') 288 | .style('line-width', 0.5); 289 | 290 | return obj; 291 | } 292 | 293 | // set the number of inserted points in the quadtree 294 | function treeSize(index) { 295 | let duration = 500; 296 | 297 | initQuadTree(); 298 | 299 | if (index < 1) { 300 | size = 0; 301 | quads(); 302 | fg.html(''); 303 | cg.html(''); 304 | ns.style('fill-opacity', 0.25); 305 | return; 306 | } 307 | size = index; 308 | 309 | let p = nodes[--index]; 310 | 311 | // initialize quadtree 312 | quad.addAll(nodes.slice(0, index)); 313 | 314 | // get initial tree path for point 315 | let path0 = getPath(p); 316 | 317 | // add point to quadtree 318 | quad.add(p).visitAfter(accumulate); 319 | 320 | // get updated tree path for point 321 | let path1 = getPath(p).slice(path0.length); 322 | 323 | if (path1.length) { 324 | fg.html('') 325 | .selectAll('rect.foo') 326 | .data(path1) 327 | .enter().append('rect') 328 | .attr('x', d => d.x1) 329 | .attr('y', d => d.y1) 330 | .attr('width', d => d.w) 331 | .attr('height', d => d.h) 332 | .style('pointer-events', 'none') 333 | .style('fill', 'none') 334 | .style('stroke', quadColor) 335 | .style('stroke-width', 2) 336 | .style('stroke-opacity', 1) 337 | .transition() 338 | .delay(0.5 * duration) 339 | .duration(duration) 340 | .style('stroke-opacity', 0) 341 | .remove(); 342 | } 343 | 344 | quads(); 345 | ns.style('fill-opacity', d => d.index <= index ? 1 : 0.25); 346 | 347 | cg.html('') 348 | .append('circle') 349 | .datum(p) 350 | .attr('cx', d => d.x) 351 | .attr('cy', d => d.y) 352 | .attr('r', 0) 353 | .style('pointer-events', 'none') 354 | .style('fill', pointColor) 355 | .style('fill-opacity', 1) 356 | .transition() 357 | .duration(duration) 358 | .ease(d3.easeBackOut.overshoot(2)) 359 | .attr('r', d => baseRadius) 360 | .transition() 361 | .delay(duration) 362 | .duration(duration) 363 | .style('fill-opacity', 0) 364 | .remove(); 365 | } 366 | 367 | // play animation of center or mass accumuluation 368 | function animateAccumulation() { 369 | let duration = 400, 370 | quads = [], 371 | queue = [], 372 | map = {}; 373 | 374 | reinit(); 375 | 376 | // collect non-leaf nodes 377 | quad.visitAfter(function(quad) { 378 | if (quad.length) quads.push(quad); 379 | }); 380 | // group nodes by depth (using width as proxy) 381 | quads.forEach(function(q) { 382 | let id = q.w, 383 | l = map[id]; 384 | if (!l) queue.push(map[id] = l = []); 385 | l.push(q); 386 | }); 387 | // sort groups by ascending width (descending depth) 388 | queue.sort((a, b) => a[0].w - b[0].w); 389 | 390 | // advance the animation one step 391 | function advance(index) { 392 | if (index < 1) { 393 | cg.html(''); 394 | return; 395 | } 396 | index -= 1; 397 | 398 | let qlist = queue[index]; 399 | 400 | qlist.forEach(function(q) { 401 | let points = q.filter(function(_) { return _; }); 402 | 403 | fg.append('rect') 404 | .datum(q) 405 | .attr('x', (d) => d.x1) 406 | .attr('y', (d) => d.y1) 407 | .attr('width', (d) => d.w) 408 | .attr('height', (d) => d.h) 409 | .style('pointer-events', 'none') 410 | .style('fill', 'none') 411 | .style('stroke', quadColor) 412 | .style('stroke-width', 2) 413 | .style('stroke-opacity', 1) 414 | .transition() 415 | .duration(2 * duration) 416 | .delay(3 * duration) 417 | .style('stroke-opacity', 0) 418 | .remove(); 419 | 420 | cg.selectAll('circle.foo') 421 | .data(points) 422 | .enter().append('circle') 423 | .attr('cx', (d) => d.x) 424 | .attr('cy', (d) => d.y) 425 | .attr('r', (d) => baseRadius * (Math.sqrt(d.value) || 1)) 426 | .style('pointer-events', 'none') 427 | .style('fill', pointColor) 428 | .transition() 429 | .delay(duration) 430 | .duration(duration) 431 | .ease(d3.easeCubicIn) 432 | .attr('cx', q.x) 433 | .attr('cy', q.y) 434 | .remove(); 435 | 436 | cg.append('circle') 437 | .datum(q) 438 | .attr('cx', (d) => d.x) 439 | .attr('cy', (d) => d.y) 440 | .attr('r', 0) 441 | .style('pointer-events', 'none') 442 | .style('fill', pointColor) 443 | .transition() 444 | .delay(duration * 1.8) 445 | .duration(duration) 446 | .ease(d3.easeBackOut) 447 | .attr('r', (d) => baseRadius * (Math.sqrt(d.value) || 1)) 448 | .transition() 449 | .delay(duration) 450 | .duration(duration) 451 | .style('fill', '#666') 452 | .style('fill-opacity', 0.25); 453 | }); 454 | } 455 | 456 | const stepDuration = 3.8 * duration; 457 | 458 | setTimeout(() => { 459 | // schedule each animation step 460 | queue.forEach(function(q, i) { 461 | setTimeout(() => advance(i+1), i * stepDuration); 462 | }); 463 | // upon animation end, reset view 464 | setTimeout(() => { 465 | cg.selectAll('circle') 466 | .transition() 467 | .duration(500) 468 | .style('fill-opacity', 0) 469 | .remove(); 470 | }, (1 + queue.length) * stepDuration); 471 | }, 1000); 472 | } 473 | 474 | // toggle interactive force-directed layout 475 | function performLayout(state) { 476 | if (layout === state) return; 477 | layout = state; 478 | if (layout) { 479 | simulation.alpha(0.5).alphaTarget(0).restart(); 480 | } else { 481 | simulation.stop(); 482 | } 483 | ns.transition(500).style('fill-opacity', layout ? 1 : 0.25); 484 | es.transition(500).style('stroke-opacity', +layout); 485 | } 486 | 487 | // toggle interactive force estimation probe 488 | function performEstimation(state) { 489 | if (active === state) return; 490 | active = state; 491 | if (active) { 492 | reinit(); 493 | probe(probePoint); 494 | } else { 495 | probePoint = defaultProbe; 496 | quads().clear(); 497 | } 498 | } 499 | 500 | // perform force estimation relative to probe point 501 | function estimate() { 502 | clear(); 503 | 504 | let p = probePoint; 505 | if (!p) return; 506 | 507 | let charges = [], 508 | boxes = [], 509 | fx = 0, 510 | fy = 0; 511 | 512 | quad.visit(function(quad, x1, y1, x2, y2) { 513 | if (!quad.value) return true; 514 | 515 | let x = quad.x - p.x, 516 | y = quad.y - p.y, 517 | w = x2 - x1, 518 | l = x * x + y * y; 519 | 520 | // Apply the Barnes-Hut approximation. 521 | if (quad.length && w * w / theta2 < l) { 522 | let c = { 523 | x: quad.x, 524 | y: quad.y, 525 | v: quad.value, 526 | s: 5e3 * quad.value / l 527 | }; 528 | charges.push(c); 529 | boxes.push({x: x1, y: y1, w: w, h: y2 - y1}); 530 | 531 | fx += x * quad.value / l; 532 | fy += y * quad.value / l; 533 | 534 | return true; 535 | } 536 | 537 | // Otherwise, process points directly. 538 | else if (quad.length || !l) return; 539 | 540 | do if (quad.data !== p) { 541 | charges.push({ 542 | x: quad.data.x, 543 | y: quad.data.y, 544 | v: 1, 545 | s: 5e3 / l 546 | }); 547 | fx += x / l; 548 | fy += y / l; 549 | } while (quad = quad.next); 550 | }); 551 | 552 | fg.selectAll('rect').data(boxes) 553 | .enter().append('rect') 554 | .attr('x', d => d.x) 555 | .attr('y', d => d.y) 556 | .attr('width', d => d.w) 557 | .attr('height', d => d.h) 558 | .style('pointer-events', 'none') 559 | .style('fill', 'none') 560 | .style('stroke', quadColor) 561 | .style('stroke-width', 2); 562 | 563 | lg.selectAll('path').data(charges) 564 | .enter().append('path') 565 | .style('pointer-events', 'none') 566 | .style('stroke', '#991151') 567 | .style('stroke-opacity', 0.3) 568 | .style('stroke-dasharray', [5, 5]) 569 | .style('stroke-linecap', 'round') 570 | .style('stroke-width', d => Math.max(1, Math.min(5, d.s || 1))) 571 | .attr('d', d => 'M' + d.x + ',' + d.y + 'L' + p.x + ',' + p.y); 572 | 573 | cg.selectAll('circle').data(charges) 574 | .enter().append('circle') 575 | .attr('cx', d => d.x) 576 | .attr('cy', d => d.y) 577 | .attr('r', d => baseRadius * (Math.sqrt(d.v) || 1)) 578 | .style('pointer-events', 'none') 579 | .style('fill', pointColor); 580 | 581 | ns.style('fill-opacity', 0.25); 582 | 583 | cg.append('circle') 584 | .attr('cx', p.x) 585 | .attr('cy', p.y) 586 | .attr('r', baseRadius) 587 | .style('pointer-events', 'none') 588 | .style('fill', 'white') 589 | .style('stroke-width', 1.5) 590 | .style('stroke-linecap', 'round') 591 | .style('stroke', '#800080'); 592 | 593 | fx = p.x - fx * 90, 594 | fy = p.y - fy * 90; 595 | 596 | cg.append('path') 597 | .attr('d', 'M' + p.x + ',' + p.y + 'L' + fx + ',' + fy) 598 | .style('pointer-events', 'none') 599 | .style('fill', 'none') 600 | .style('stroke-width', 1.5) 601 | .style('stroke-linecap', 'round') 602 | .style('stroke', 'purple'); 603 | 604 | return obj; 605 | } 606 | 607 | // set the probe point 608 | function probe(point) { 609 | probePoint = point; 610 | return estimate(); 611 | } 612 | 613 | // set the Barnes-Hut theta parameter 614 | function theta(_) { 615 | theta2 = _ * _; 616 | return estimate(); 617 | } 618 | 619 | // set the default node charge / mass 620 | function charge(_) { 621 | let c = +_; 622 | if (c === c) { 623 | nbodyForce.strength(c); 624 | } 625 | if (layout) { 626 | simulation.alpha(0.5).alphaTarget(0).restart(); 627 | } 628 | } 629 | 630 | // define returned API object 631 | obj = { 632 | svg: svg, 633 | quad: quad, 634 | init: init, 635 | size: treeSize, 636 | clear: clear, 637 | theta: theta, 638 | charge: charge, 639 | layout: performLayout, 640 | estimate: performEstimation, 641 | accumulate: animateAccumulation, 642 | }; 643 | 644 | return init(); 645 | }; 646 | -------------------------------------------------------------------------------- /docs/styles.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | } 4 | 5 | .idyll-root { 6 | box-sizing: border-box; 7 | max-width: 90vw; 8 | margin: 0 auto; 9 | padding: 60px 0; 10 | margin-bottom: 60px; 11 | width: 600px; 12 | margin-left: 10vw; 13 | } 14 | 15 | .section { 16 | padding: 0 10px; 17 | margin: 0 auto; 18 | } 19 | 20 | .article-header { 21 | width: 600px; 22 | max-width: 90vw; 23 | margin-bottom: 45px; 24 | } 25 | 26 | .inset { 27 | max-width: 400px; 28 | margin: 0 auto; 29 | } 30 | 31 | input { 32 | cursor: pointer; 33 | } 34 | 35 | .relative { 36 | position: relative; 37 | } 38 | .aside-container { 39 | position: relative; 40 | } 41 | .aside { 42 | position: absolute; 43 | width: 300px; 44 | right: calc((10vw + 600px + 150px) / -2); 45 | } 46 | 47 | .fixed { 48 | position: fixed; 49 | display: flex; 50 | align-self: center; 51 | /*flex-direction: column;*/ 52 | align-items: center; 53 | right: 25px; 54 | top: 0; 55 | bottom: 0; 56 | width: calc((80vw - 600px) - 50px); 57 | } 58 | 59 | .fixed div { 60 | width: 100%; 61 | } 62 | 63 | @media all and (max-width: 1600px) { 64 | .idyll-root { 65 | margin-left: 100px; 66 | } 67 | 68 | .fixed { 69 | width: calc((85vw - 600px) - 50px); 70 | } 71 | } 72 | 73 | @media all and (max-width: 1000px) { 74 | /* put your css styles in here */ 75 | .desktop { 76 | display: none; 77 | } 78 | .relative { 79 | position: static; 80 | } 81 | .aside { 82 | position: static; 83 | width: 100%; 84 | right: 0; 85 | } 86 | 87 | .hed { 88 | width: 100%; 89 | } 90 | 91 | .idyll-root { 92 | padding: 15px 0; 93 | } 94 | 95 | .idyll-root { 96 | width: 90vw; 97 | max-width: 600px; 98 | margin: 0 auto; 99 | padding-bottom: 80vh; 100 | } 101 | .fixed { 102 | position: fixed; 103 | left: 0; 104 | right: 0; 105 | bottom: 0; 106 | width: 100vw; 107 | top: initial; 108 | } 109 | } 110 | 111 | 112 | @font-face { 113 | font-family: octicons-link; 114 | src: url(data:font/woff;charset=utf-8;base64,d09GRgABAAAAAAZwABAAAAAACFQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABEU0lHAAAGaAAAAAgAAAAIAAAAAUdTVUIAAAZcAAAACgAAAAoAAQAAT1MvMgAAAyQAAABJAAAAYFYEU3RjbWFwAAADcAAAAEUAAACAAJThvmN2dCAAAATkAAAABAAAAAQAAAAAZnBnbQAAA7gAAACyAAABCUM+8IhnYXNwAAAGTAAAABAAAAAQABoAI2dseWYAAAFsAAABPAAAAZwcEq9taGVhZAAAAsgAAAA0AAAANgh4a91oaGVhAAADCAAAABoAAAAkCA8DRGhtdHgAAAL8AAAADAAAAAwGAACfbG9jYQAAAsAAAAAIAAAACABiATBtYXhwAAACqAAAABgAAAAgAA8ASm5hbWUAAAToAAABQgAAAlXu73sOcG9zdAAABiwAAAAeAAAAME3QpOBwcmVwAAAEbAAAAHYAAAB/aFGpk3jaTY6xa8JAGMW/O62BDi0tJLYQincXEypYIiGJjSgHniQ6umTsUEyLm5BV6NDBP8Tpts6F0v+k/0an2i+itHDw3v2+9+DBKTzsJNnWJNTgHEy4BgG3EMI9DCEDOGEXzDADU5hBKMIgNPZqoD3SilVaXZCER3/I7AtxEJLtzzuZfI+VVkprxTlXShWKb3TBecG11rwoNlmmn1P2WYcJczl32etSpKnziC7lQyWe1smVPy/Lt7Kc+0vWY/gAgIIEqAN9we0pwKXreiMasxvabDQMM4riO+qxM2ogwDGOZTXxwxDiycQIcoYFBLj5K3EIaSctAq2kTYiw+ymhce7vwM9jSqO8JyVd5RH9gyTt2+J/yUmYlIR0s04n6+7Vm1ozezUeLEaUjhaDSuXHwVRgvLJn1tQ7xiuVv/ocTRF42mNgZGBgYGbwZOBiAAFGJBIMAAizAFoAAABiAGIAznjaY2BkYGAA4in8zwXi+W2+MjCzMIDApSwvXzC97Z4Ig8N/BxYGZgcgl52BCSQKAA3jCV8CAABfAAAAAAQAAEB42mNgZGBg4f3vACQZQABIMjKgAmYAKEgBXgAAeNpjYGY6wTiBgZWBg2kmUxoDA4MPhGZMYzBi1AHygVLYQUCaawqDA4PChxhmh/8ODDEsvAwHgMKMIDnGL0x7gJQCAwMAJd4MFwAAAHjaY2BgYGaA4DAGRgYQkAHyGMF8NgYrIM3JIAGVYYDT+AEjAwuDFpBmA9KMDEwMCh9i/v8H8sH0/4dQc1iAmAkALaUKLgAAAHjaTY9LDsIgEIbtgqHUPpDi3gPoBVyRTmTddOmqTXThEXqrob2gQ1FjwpDvfwCBdmdXC5AVKFu3e5MfNFJ29KTQT48Ob9/lqYwOGZxeUelN2U2R6+cArgtCJpauW7UQBqnFkUsjAY/kOU1cP+DAgvxwn1chZDwUbd6CFimGXwzwF6tPbFIcjEl+vvmM/byA48e6tWrKArm4ZJlCbdsrxksL1AwWn/yBSJKpYbq8AXaaTb8AAHja28jAwOC00ZrBeQNDQOWO//sdBBgYGRiYWYAEELEwMTE4uzo5Zzo5b2BxdnFOcALxNjA6b2ByTswC8jYwg0VlNuoCTWAMqNzMzsoK1rEhNqByEyerg5PMJlYuVueETKcd/89uBpnpvIEVomeHLoMsAAe1Id4AAAAAAAB42oWQT07CQBTGv0JBhagk7HQzKxca2sJCE1hDt4QF+9JOS0nbaaYDCQfwCJ7Au3AHj+LO13FMmm6cl7785vven0kBjHCBhfpYuNa5Ph1c0e2Xu3jEvWG7UdPDLZ4N92nOm+EBXuAbHmIMSRMs+4aUEd4Nd3CHD8NdvOLTsA2GL8M9PODbcL+hD7C1xoaHeLJSEao0FEW14ckxC+TU8TxvsY6X0eLPmRhry2WVioLpkrbp84LLQPGI7c6sOiUzpWIWS5GzlSgUzzLBSikOPFTOXqly7rqx0Z1Q5BAIoZBSFihQYQOOBEdkCOgXTOHA07HAGjGWiIjaPZNW13/+lm6S9FT7rLHFJ6fQbkATOG1j2OFMucKJJsxIVfQORl+9Jyda6Sl1dUYhSCm1dyClfoeDve4qMYdLEbfqHf3O/AdDumsjAAB42mNgYoAAZQYjBmyAGYQZmdhL8zLdDEydARfoAqIAAAABAAMABwAKABMAB///AA8AAQAAAAAAAAAAAAAAAAABAAAAAA==) format('woff'); 115 | } 116 | 117 | .ReactTable{position:relative;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column;border:1px solid rgba(0,0,0,0.1);}.ReactTable *{box-sizing:border-box}.ReactTable .rt-table{-webkit-box-flex:1;-ms-flex:1;flex:1;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column;-webkit-box-align:stretch;-ms-flex-align:stretch;align-items:stretch;width:100%;border-collapse:collapse;overflow:auto}.ReactTable .rt-thead{-webkit-box-flex:1;-ms-flex:1 0 auto;flex:1 0 auto;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;}.ReactTable .rt-thead.-headerGroups{background:rgba(0,0,0,0.03);border-bottom:1px solid rgba(0,0,0,0.05)}.ReactTable .rt-thead.-filters{border-bottom:1px solid rgba(0,0,0,0.05);}.ReactTable .rt-thead.-filters .rt-th{border-right:1px solid rgba(0,0,0,0.02)}.ReactTable .rt-thead.-header{box-shadow:0 2px 15px 0 rgba(0,0,0,0.15)}.ReactTable .rt-thead .rt-tr{text-align:center}.ReactTable .rt-thead .rt-th,.ReactTable .rt-thead .rt-td{padding:5px 5px;line-height:normal;position:relative;border-right:1px solid rgba(0,0,0,0.05);-webkit-transition:box-shadow .3s cubic-bezier(.175,.885,.32,1.275);transition:box-shadow .3s cubic-bezier(.175,.885,.32,1.275);box-shadow:inset 0 0 0 0 transparent;}.ReactTable .rt-thead .rt-th.-sort-asc,.ReactTable .rt-thead .rt-td.-sort-asc{box-shadow:inset 0 3px 0 0 rgba(0,0,0,0.6)}.ReactTable .rt-thead .rt-th.-sort-desc,.ReactTable .rt-thead .rt-td.-sort-desc{box-shadow:inset 0 -3px 0 0 rgba(0,0,0,0.6)}.ReactTable .rt-thead .rt-th.-cursor-pointer,.ReactTable .rt-thead .rt-td.-cursor-pointer{cursor:pointer}.ReactTable .rt-thead .rt-th:last-child,.ReactTable .rt-thead .rt-td:last-child{border-right:0}.ReactTable .rt-thead .rt-resizable-header{overflow:visible;}.ReactTable .rt-thead .rt-resizable-header:last-child{overflow:hidden}.ReactTable .rt-thead .rt-resizable-header-content{overflow:hidden;text-overflow:ellipsis}.ReactTable .rt-thead .rt-header-pivot{border-right-color:#f7f7f7}.ReactTable .rt-thead .rt-header-pivot:after,.ReactTable .rt-thead .rt-header-pivot:before{left:100%;top:50%;border:solid transparent;content:" ";height:0;width:0;position:absolute;pointer-events:none}.ReactTable .rt-thead .rt-header-pivot:after{border-color:rgba(255,255,255,0);border-left-color:#fff;border-width:8px;margin-top:-8px}.ReactTable .rt-thead .rt-header-pivot:before{border-color:rgba(102,102,102,0);border-left-color:#f7f7f7;border-width:10px;margin-top:-10px}.ReactTable .rt-tbody{-webkit-box-flex:99999;-ms-flex:99999 1 auto;flex:99999 1 auto;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column;overflow:auto;}.ReactTable .rt-tbody .rt-tr-group{border-bottom:solid 1px rgba(0,0,0,0.05);}.ReactTable .rt-tbody .rt-tr-group:last-child{border-bottom:0}.ReactTable .rt-tbody .rt-td{border-right:1px solid rgba(0,0,0,0.02);}.ReactTable .rt-tbody .rt-td:last-child{border-right:0}.ReactTable .rt-tbody .rt-expandable{cursor:pointer}.ReactTable .rt-tr-group{-webkit-box-flex:1;-ms-flex:1 0 auto;flex:1 0 auto;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column;-webkit-box-align:stretch;-ms-flex-align:stretch;align-items:stretch}.ReactTable .rt-tr{-webkit-box-flex:1;-ms-flex:1 0 auto;flex:1 0 auto;display:-webkit-inline-box;display:-ms-inline-flexbox;display:inline-flex}.ReactTable .rt-th,.ReactTable .rt-td{-webkit-box-flex:1;-ms-flex:1 0 0px;flex:1 0 0;white-space:nowrap;text-overflow:ellipsis;padding:7px 5px;overflow:hidden;-webkit-transition:.3s ease;transition:.3s ease;-webkit-transition-property:width,min-width,padding,opacity;transition-property:width,min-width,padding,opacity;}.ReactTable .rt-th.-hidden,.ReactTable .rt-td.-hidden{width:0 !important;min-width:0 !important;padding:0 !important;border:0 !important;opacity:0 !important}.ReactTable .rt-expander{display:inline-block;position:relative;margin:0;color:transparent;margin:0 10px;}.ReactTable .rt-expander:after{content:'';position:absolute;width:0;height:0;top:50%;left:50%;-webkit-transform:translate(-50%,-50%) rotate(-90deg);transform:translate(-50%,-50%) rotate(-90deg);border-left:5.04px solid transparent;border-right:5.04px solid transparent;border-top:7px solid rgba(0,0,0,0.8);-webkit-transition:all .3s cubic-bezier(.175,.885,.32,1.275);transition:all .3s cubic-bezier(.175,.885,.32,1.275);cursor:pointer}.ReactTable .rt-expander.-open:after{-webkit-transform:translate(-50%,-50%) rotate(0);transform:translate(-50%,-50%) rotate(0)}.ReactTable .rt-resizer{display:inline-block;position:absolute;width:36px;top:0;bottom:0;right:-18px;cursor:col-resize;z-index:10}.ReactTable .rt-tfoot{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column;box-shadow:0 0 15px 0 rgba(0,0,0,0.15);}.ReactTable .rt-tfoot .rt-td{border-right:1px solid rgba(0,0,0,0.05);}.ReactTable .rt-tfoot .rt-td:last-child{border-right:0}.ReactTable.-striped .rt-tr.-odd{background:rgba(0,0,0,0.03)}.ReactTable.-highlight .rt-tbody .rt-tr:not(.-padRow):hover{background:rgba(0,0,0,0.05)}.ReactTable .-pagination{z-index:1;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between;-webkit-box-align:stretch;-ms-flex-align:stretch;align-items:stretch;-ms-flex-wrap:wrap;flex-wrap:wrap;padding:3px;box-shadow:0 0 15px 0 rgba(0,0,0,0.1);border-top:2px solid rgba(0,0,0,0.1);}.ReactTable .-pagination .-btn{-webkit-appearance:none;-moz-appearance:none;appearance:none;display:block;width:100%;height:100%;border:0;border-radius:3px;padding:6px;font-size:1em;color:rgba(0,0,0,0.6);background:rgba(0,0,0,0.1);-webkit-transition:all .1s ease;transition:all .1s ease;cursor:pointer;outline:none;}.ReactTable .-pagination .-btn[disabled]{opacity:.5;cursor:default}.ReactTable .-pagination .-btn:not([disabled]):hover{background:rgba(0,0,0,0.3);color:#fff}.ReactTable .-pagination .-previous,.ReactTable .-pagination .-next{-webkit-box-flex:1;-ms-flex:1;flex:1;text-align:center}.ReactTable .-pagination .-center{-webkit-box-flex:1.5;-ms-flex:1.5;flex:1.5;text-align:center;margin-bottom:0;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:horizontal;-webkit-box-direction:normal;-ms-flex-direction:row;flex-direction:row;-ms-flex-wrap:wrap;flex-wrap:wrap;-webkit-box-align:center;-ms-flex-align:center;align-items:center;-ms-flex-pack:distribute;justify-content:space-around}.ReactTable .-pagination .-pageInfo{display:inline-block;margin:3px 10px;white-space:nowrap}.ReactTable .-pagination .-pageJump{display:inline-block;}.ReactTable .-pagination .-pageJump input{width:70px;text-align:center}.ReactTable .-pagination .-pageSizeOptions{margin:3px 10px}.ReactTable .rt-noData{display:block;position:absolute;left:50%;top:50%;-webkit-transform:translate(-50%,-50%);transform:translate(-50%,-50%);background:rgba(255,255,255,0.8);-webkit-transition:all .3s ease;transition:all .3s ease;z-index:1;pointer-events:none;padding:20px;color:rgba(0,0,0,0.5)}.ReactTable .-loading{display:block;position:absolute;left:0;right:0;top:0;bottom:0;background:rgba(255,255,255,0.8);-webkit-transition:all .3s ease;transition:all .3s ease;z-index:-1;opacity:0;pointer-events:none;}.ReactTable .-loading > div{position:absolute;display:block;text-align:center;width:100%;top:50%;left:0;font-size:15px;color:rgba(0,0,0,0.6);-webkit-transform:translateY(-52%);transform:translateY(-52%);-webkit-transition:all .3s cubic-bezier(.25,.46,.45,.94);transition:all .3s cubic-bezier(.25,.46,.45,.94)}.ReactTable .-loading.-active{opacity:1;z-index:2;pointer-events:all;}.ReactTable .-loading.-active > div{-webkit-transform:translateY(50%);transform:translateY(50%)}.ReactTable input,.ReactTable select{border:1px solid rgba(0,0,0,0.1);background:#fff;padding:5px 7px;font-size:inherit;border-radius:3px;font-weight:normal;outline:none}.ReactTable .rt-resizing .rt-th,.ReactTable .rt-resizing .rt-td{-webkit-transition:none !important;transition:none !important;cursor:col-resize;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none} 118 | 119 | * { 120 | box-sizing: border-box; 121 | } 122 | body { 123 | -ms-text-size-adjust: 100%; 124 | -webkit-text-size-adjust: 100%; 125 | line-height: 1.5; 126 | color: #24292e; 127 | font-family: -apple-system, system-ui, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"; 128 | font-size: 16px; 129 | line-height: 1.5; 130 | word-wrap: break-word; 131 | } 132 | 133 | .pl-c { 134 | color: #969896; 135 | } 136 | 137 | .pl-c1, 138 | .pl-s .pl-v { 139 | color: #0086b3; 140 | } 141 | 142 | .pl-e, 143 | .pl-en { 144 | color: #795da3; 145 | } 146 | 147 | .pl-smi, 148 | .pl-s .pl-s1 { 149 | color: #333; 150 | } 151 | 152 | .pl-ent { 153 | color: #63a35c; 154 | } 155 | 156 | .pl-k { 157 | color: #a71d5d; 158 | } 159 | 160 | .pl-s, 161 | .pl-pds, 162 | .pl-s .pl-pse .pl-s1, 163 | .pl-sr, 164 | .pl-sr .pl-cce, 165 | .pl-sr .pl-sre, 166 | .pl-sr .pl-sra { 167 | color: #183691; 168 | } 169 | 170 | .pl-v, 171 | .pl-smw { 172 | color: #ed6a43; 173 | } 174 | 175 | .pl-bu { 176 | color: #b52a1d; 177 | } 178 | 179 | .pl-ii { 180 | color: #f8f8f8; 181 | background-color: #b52a1d; 182 | } 183 | 184 | .pl-c2 { 185 | color: #f8f8f8; 186 | background-color: #b52a1d; 187 | } 188 | 189 | .pl-c2::before { 190 | content: "^M"; 191 | } 192 | 193 | .pl-sr .pl-cce { 194 | font-weight: bold; 195 | color: #63a35c; 196 | } 197 | 198 | .pl-ml { 199 | color: #693a17; 200 | } 201 | 202 | .pl-mh, 203 | .pl-mh .pl-en, 204 | .pl-ms { 205 | font-weight: bold; 206 | color: #1d3e81; 207 | } 208 | 209 | .pl-mq { 210 | color: #008080; 211 | } 212 | 213 | .pl-mi { 214 | font-style: italic; 215 | color: #333; 216 | } 217 | 218 | .pl-mb { 219 | font-weight: bold; 220 | color: #333; 221 | } 222 | 223 | .pl-md { 224 | color: #bd2c00; 225 | background-color: #ffecec; 226 | } 227 | 228 | .pl-mi1 { 229 | color: #55a532; 230 | background-color: #eaffea; 231 | } 232 | 233 | .pl-mc { 234 | color: #ef9700; 235 | background-color: #ffe3b4; 236 | } 237 | 238 | .pl-mi2 { 239 | color: #d8d8d8; 240 | background-color: #808080; 241 | } 242 | 243 | .pl-mdr { 244 | font-weight: bold; 245 | color: #795da3; 246 | } 247 | 248 | .pl-mo { 249 | color: #1d3e81; 250 | } 251 | 252 | .pl-ba { 253 | color: #595e62; 254 | } 255 | 256 | .pl-sg { 257 | color: #c0c0c0; 258 | } 259 | 260 | .pl-corl { 261 | text-decoration: underline; 262 | color: #183691; 263 | } 264 | 265 | .octicon { 266 | display: inline-block; 267 | vertical-align: text-top; 268 | fill: currentColor; 269 | } 270 | 271 | a { 272 | background-color: transparent; 273 | -webkit-text-decoration-skip: objects; 274 | } 275 | 276 | a:active, 277 | a:hover { 278 | outline-width: 0; 279 | } 280 | 281 | strong { 282 | font-weight: inherit; 283 | } 284 | 285 | strong { 286 | font-weight: bolder; 287 | } 288 | 289 | h1 { 290 | font-size: 2em; 291 | margin: 0.67em 0; 292 | } 293 | 294 | img { 295 | border-style: none; 296 | } 297 | 298 | svg:not(:root) { 299 | overflow: hidden; 300 | } 301 | 302 | code, 303 | kbd, 304 | pre { 305 | font-family: monospace, monospace; 306 | font-size: 1em; 307 | } 308 | 309 | hr { 310 | box-sizing: content-box; 311 | height: 0; 312 | overflow: visible; 313 | } 314 | 315 | input { 316 | font: inherit; 317 | margin: 10px 10px 20px 0; 318 | } 319 | 320 | input { 321 | overflow: visible; 322 | } 323 | 324 | [type="checkbox"] { 325 | box-sizing: border-box; 326 | padding: 0; 327 | } 328 | 329 | 330 | input { 331 | font-family: inherit; 332 | font-size: inherit; 333 | line-height: inherit; 334 | } 335 | 336 | a { 337 | color: #0366d6; 338 | text-decoration: none; 339 | } 340 | 341 | a:hover { 342 | text-decoration: underline; 343 | } 344 | 345 | strong { 346 | font-weight: 600; 347 | } 348 | 349 | hr { 350 | height: 0; 351 | margin: 15px 0; 352 | overflow: hidden; 353 | background: transparent; 354 | border: 0; 355 | border-bottom: 1px solid #dfe2e5; 356 | } 357 | 358 | hr::before { 359 | display: table; 360 | content: ""; 361 | } 362 | 363 | hr::after { 364 | display: table; 365 | clear: both; 366 | content: ""; 367 | } 368 | 369 | table { 370 | border-spacing: 0; 371 | border-collapse: collapse; 372 | } 373 | 374 | td, 375 | th { 376 | padding: 0; 377 | } 378 | 379 | h1, 380 | h2, 381 | h3, 382 | h4, 383 | h5, 384 | h6 { 385 | margin-top: 0; 386 | margin-bottom: 0; 387 | } 388 | 389 | h1 { 390 | font-size: 32px; 391 | font-weight: 600; 392 | } 393 | 394 | h2 { 395 | font-size: 24px; 396 | font-weight: 600; 397 | } 398 | 399 | h3 { 400 | font-size: 20px; 401 | font-weight: 600; 402 | } 403 | 404 | h4 { 405 | font-size: 16px; 406 | font-weight: 600; 407 | } 408 | 409 | h5 { 410 | font-size: 14px; 411 | font-weight: 600; 412 | } 413 | 414 | h6 { 415 | font-size: 12px; 416 | font-weight: 600; 417 | } 418 | 419 | p { 420 | margin-top: 0; 421 | margin-bottom: 10px; 422 | } 423 | 424 | blockquote { 425 | margin: 0; 426 | } 427 | 428 | ul, 429 | ol { 430 | padding-left: 0; 431 | margin-top: 0; 432 | margin-bottom: 0; 433 | } 434 | 435 | ol ol, 436 | ul ol { 437 | list-style-type: lower-roman; 438 | } 439 | 440 | ul ul ol, 441 | ul ol ol, 442 | ol ul ol, 443 | ol ol ol { 444 | list-style-type: lower-alpha; 445 | } 446 | 447 | dd { 448 | margin-left: 0; 449 | } 450 | 451 | code { 452 | font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, Courier, monospace; 453 | font-size: 12px; 454 | } 455 | 456 | pre { 457 | margin-top: 0; 458 | margin-bottom: 0; 459 | font: 12px "SFMono-Regular", Consolas, "Liberation Mono", Menlo, Courier, monospace; 460 | } 461 | 462 | .octicon { 463 | vertical-align: text-bottom; 464 | } 465 | 466 | .pl-0 { 467 | padding-left: 0 !important; 468 | } 469 | 470 | .pl-1 { 471 | padding-left: 4px !important; 472 | } 473 | 474 | .pl-2 { 475 | padding-left: 8px !important; 476 | } 477 | 478 | .pl-3 { 479 | padding-left: 16px !important; 480 | } 481 | 482 | .pl-4 { 483 | padding-left: 24px !important; 484 | } 485 | 486 | .pl-5 { 487 | padding-left: 32px !important; 488 | } 489 | 490 | .pl-6 { 491 | padding-left: 40px !important; 492 | } 493 | 494 | .idyll-root::before { 495 | display: table; 496 | content: ""; 497 | } 498 | 499 | .idyll-root::after { 500 | display: table; 501 | clear: both; 502 | content: ""; 503 | } 504 | 505 | .idyll-root>*:first-child { 506 | margin-top: 0 !important; 507 | } 508 | 509 | .idyll-root>*:last-child { 510 | margin-bottom: 0 !important; 511 | } 512 | 513 | a:not([href]) { 514 | color: inherit; 515 | text-decoration: none; 516 | } 517 | 518 | .anchor { 519 | float: left; 520 | padding-right: 4px; 521 | margin-left: -20px; 522 | line-height: 1; 523 | } 524 | 525 | .anchor:focus { 526 | outline: none; 527 | } 528 | 529 | p, 530 | blockquote, 531 | ul, 532 | ol, 533 | dl, 534 | table, 535 | pre { 536 | margin-top: 0; 537 | margin-bottom: 16px; 538 | } 539 | 540 | hr { 541 | height: 0.25em; 542 | padding: 0; 543 | margin: 24px 0; 544 | background-color: #e1e4e8; 545 | border: 0; 546 | } 547 | 548 | blockquote { 549 | padding: 0 1em; 550 | color: #6a737d; 551 | border-left: 0.25em solid #dfe2e5; 552 | } 553 | 554 | blockquote>:first-child { 555 | margin-top: 0; 556 | } 557 | 558 | blockquote>:last-child { 559 | margin-bottom: 0; 560 | } 561 | 562 | kbd { 563 | display: inline-block; 564 | padding: 3px 5px; 565 | font-size: 11px; 566 | line-height: 10px; 567 | color: #444d56; 568 | vertical-align: middle; 569 | background-color: #fafbfc; 570 | border: solid 1px #c6cbd1; 571 | border-bottom-color: #959da5; 572 | border-radius: 3px; 573 | box-shadow: inset 0 -1px 0 #959da5; 574 | } 575 | 576 | h1, 577 | h2, 578 | h3, 579 | h4, 580 | h5, 581 | h6 { 582 | margin-top: 24px; 583 | margin-bottom: 16px; 584 | font-weight: 600; 585 | line-height: 1.25; 586 | } 587 | 588 | h1 .octicon-link, 589 | h2 .octicon-link, 590 | h3 .octicon-link, 591 | h4 .octicon-link, 592 | h5 .octicon-link, 593 | h6 .octicon-link { 594 | color: #1b1f23; 595 | vertical-align: middle; 596 | visibility: hidden; 597 | } 598 | 599 | h1:hover .anchor, 600 | h2:hover .anchor, 601 | h3:hover .anchor, 602 | h4:hover .anchor, 603 | h5:hover .anchor, 604 | h6:hover .anchor { 605 | text-decoration: none; 606 | } 607 | 608 | h1:hover .anchor .octicon-link, 609 | h2:hover .anchor .octicon-link, 610 | h3:hover .anchor .octicon-link, 611 | h4:hover .anchor .octicon-link, 612 | h5:hover .anchor .octicon-link, 613 | h6:hover .anchor .octicon-link { 614 | visibility: visible; 615 | } 616 | 617 | h1 { 618 | padding-bottom: 0.3em; 619 | font-size: 2em; 620 | border-bottom: 1px solid #eaecef; 621 | } 622 | 623 | h2 { 624 | padding-bottom: 0.3em; 625 | font-size: 1.5em; 626 | border-bottom: 1px solid #eaecef; 627 | } 628 | 629 | h3 { 630 | font-size: 1.25em; 631 | } 632 | 633 | h4 { 634 | font-size: 1em; 635 | } 636 | 637 | h5 { 638 | font-size: 0.875em; 639 | } 640 | 641 | h6 { 642 | font-size: 0.85em; 643 | color: #6a737d; 644 | } 645 | 646 | h1.hed, 647 | h2.dek { 648 | border-bottom: none; 649 | padding-bottom: 0; 650 | margin-top: 12px; 651 | } 652 | 653 | ul, 654 | ol { 655 | padding-left: 2em; 656 | } 657 | 658 | ul ul, 659 | ul ol, 660 | ol ol, 661 | ol ul { 662 | margin-top: 0; 663 | margin-bottom: 0; 664 | } 665 | 666 | li>p { 667 | margin-top: 16px; 668 | } 669 | 670 | li+li { 671 | margin-top: 0.25em; 672 | } 673 | 674 | dl { 675 | padding: 0; 676 | } 677 | 678 | dl dt { 679 | padding: 0; 680 | margin-top: 16px; 681 | font-size: 1em; 682 | font-style: italic; 683 | font-weight: 600; 684 | } 685 | 686 | dl dd { 687 | padding: 0 16px; 688 | margin-bottom: 16px; 689 | } 690 | 691 | table { 692 | display: block; 693 | width: 100%; 694 | overflow: auto; 695 | } 696 | 697 | table th { 698 | font-weight: 600; 699 | } 700 | 701 | table th, 702 | table td { 703 | padding: 6px 13px; 704 | border: 1px solid #dfe2e5; 705 | } 706 | 707 | table tr { 708 | background-color: #fff; 709 | border-top: 1px solid #c6cbd1; 710 | } 711 | 712 | table tr:nth-child(2n) { 713 | background-color: #f6f8fa; 714 | } 715 | 716 | img { 717 | max-width: 100%; 718 | box-sizing: content-box; 719 | background-color: #fff; 720 | } 721 | 722 | code { 723 | padding: 0; 724 | padding-top: 0.2em; 725 | padding-bottom: 0.2em; 726 | margin: 0; 727 | font-size: 85%; 728 | background-color: rgba(27,31,35,0.05); 729 | border-radius: 3px; 730 | } 731 | 732 | code::before, 733 | code::after { 734 | letter-spacing: -0.2em; 735 | content: "\00a0"; 736 | } 737 | 738 | pre { 739 | word-wrap: normal; 740 | } 741 | 742 | pre>code { 743 | padding: 0; 744 | margin: 0; 745 | font-size: 100%; 746 | word-break: normal; 747 | white-space: pre; 748 | background: transparent; 749 | border: 0; 750 | } 751 | 752 | .highlight { 753 | margin-bottom: 16px; 754 | } 755 | 756 | .highlight pre { 757 | margin-bottom: 0; 758 | word-break: normal; 759 | } 760 | 761 | .highlight pre, 762 | pre { 763 | padding: 16px; 764 | overflow: auto; 765 | font-size: 85%; 766 | line-height: 1.45; 767 | background-color: #f6f8fa; 768 | border-radius: 3px; 769 | } 770 | 771 | pre code { 772 | display: inline; 773 | max-width: auto; 774 | padding: 0; 775 | margin: 0; 776 | overflow: visible; 777 | line-height: inherit; 778 | word-wrap: normal; 779 | background-color: transparent; 780 | border: 0; 781 | } 782 | 783 | pre code::before, 784 | pre code::after { 785 | content: normal; 786 | } 787 | 788 | .full-commit .btn-outline:not(:disabled):hover { 789 | color: #005cc5; 790 | border-color: #005cc5; 791 | } 792 | 793 | kbd { 794 | display: inline-block; 795 | padding: 3px 5px; 796 | font: 11px "SFMono-Regular", Consolas, "Liberation Mono", Menlo, Courier, monospace; 797 | line-height: 10px; 798 | color: #444d56; 799 | vertical-align: middle; 800 | background-color: #fcfcfc; 801 | border: solid 1px #c6cbd1; 802 | border-bottom-color: #959da5; 803 | border-radius: 3px; 804 | box-shadow: inset 0 -1px 0 #959da5; 805 | } 806 | 807 | :checked+.radio-label { 808 | position: relative; 809 | z-index: 1; 810 | border-color: #0366d6; 811 | } 812 | 813 | .task-list-item { 814 | list-style-type: none; 815 | } 816 | 817 | .task-list-item+.task-list-item { 818 | margin-top: 3px; 819 | } 820 | 821 | .task-list-item input { 822 | margin: 0 0.2em 0.25em -1.6em; 823 | vertical-align: middle; 824 | } 825 | 826 | hr { 827 | border-bottom-color: #eee; 828 | } 829 | 830 | 831 | /* Put your custom styles here */ 832 | 833 | /* Corrections for fixed component layout. */ 834 | .fixed { 835 | flex-direction: column; 836 | align-items: left; 837 | justify-content: center; 838 | } 839 | 840 | /* Range slider layout correction. */ 841 | input[type="range"] { 842 | margin: 0 10px; 843 | } 844 | 845 | /* Action link styling. */ 846 | .action { 847 | font-weight: 500; 848 | border-bottom: 1px dashed #888; 849 | cursor: pointer; 850 | } 851 | 852 | .action:hover { 853 | border-bottom: 1px dashed firebrick; 854 | color: firebrick; 855 | } 856 | 857 | /* Colors for Barnes-Hut theta values. */ 858 | .color0 { 859 | color: rgb(59, 15, 112); 860 | border-bottom: 1px dashed rgb(59, 15, 112); 861 | } 862 | .color1 { 863 | color: rgb(140, 41, 129); 864 | border-bottom: 1px dashed rgb(140, 41, 129); 865 | } 866 | .color2 { 867 | color: rgb(222, 73, 104); 868 | border-bottom: 1px dashed rgb(222, 73, 104); 869 | 870 | } 871 | .color3 { 872 | color: rgb(254, 159, 109); 873 | border-bottom: 1px dashed rgb(254, 159, 109); 874 | } 875 | 876 | /* Animation for Vega plot elements. */ 877 | svg.marks text, svg.marks path { 878 | transition: opacity 0.5s; 879 | } 880 | --------------------------------------------------------------------------------