Skip to content

Instantly share code, notes, and snippets.

@lorenzopub
Created August 10, 2017 05:51
Show Gist options
  • Save lorenzopub/b69484f53fc637d3c8a89d3b1335dff6 to your computer and use it in GitHub Desktop.
Save lorenzopub/b69484f53fc637d3c8a89d3b1335dff6 to your computer and use it in GitHub Desktop.
Makeover Monday - marimekko with labella.js
license: mit

Updated entry for Makeover Monday week 14, which was based on the data from a Guardian article and PwC report on the affects of automation on UK jobs.

The chart is vertical marimekko chart, just using rects and manually calculating the offset of each row, rather than using the d3 treemap function, like this example.

Updated using labella.js to create the labels, following Micah Stubbs' recommendation.

Built with blockbuilder.org, layitout.com, and Bootstrap.

forked from tomshanley's block: Makeover Monday - marimekko with step

forked from tomshanley's block: Makeover Monday - marimekko with labella.js

<!DOCTYPE html>
<head>
<meta charset="utf-8">
<script src="https://d3js.org/d3.v4.min.js"></script>
<script src="labella.js"></script>
<!-- Latest compiled and minified CSS -->
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous">
<!-- Optional theme -->
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap-theme.min.css" integrity="sha384-rHyoN1iRsVXV4nD0JutlnGaslCJuC7uwjduW9SVrLvRYooPp2bWYgmgJQIXwl/Sp" crossorigin="anonymous">
<style>
body { text-align: center;}
p, button { text-align: center; }
path { stroke: #8FBC8F; stroke-width: 1px; fill: none; opacity: 0.6 }
line { stroke: lightGrey }
#next-button { background-color: #2E8B57; background-image: linear-gradient(to bottom,#8FBC8F 0,#2E8B57 100%) }
text { fill: Black; font-size: 12px }
rect { shape-rendering: crispEdges; stroke: white; stroke-width: 1px; }
.industry-label { font-size: 14px }
</style>
</head>
<body>
<div class="container-fluid">
<div class="row">
<div class="col-xs-12">
<p id="headline" class="text-center lead">By 2030, over 30% of jobs could be replced by automation. Half of the potential job losses are in four key industry sectors: wholesale and retail trade, manufacturing, administrative and support services, and transport and storage:
</p>
<div id="chart"></div>
<p id="source-link">More information and analysis is available in the PwC report: <a href="http://www.pwc.co.uk/services/economics-policy/insights/uk-economic-outlook.html">UK Economic Outlook March 2017</a></p>
</div>
</div>
</div>
<script>
var data = [
{
"industry": "Wholesale and retail trade",
"employmentShare": 0.15,
"jobAutomation": 0.44
},
{
"industry": "Manufacturing",
"employmentShare": 0.08,
"jobAutomation": 0.46
},
{
"industry": "Administrative and support services",
"employmentShare": 0.08,
"jobAutomation": 0.37
},
{
"industry": "Transportation and storage",
"employmentShare": 0.05,
"jobAutomation": 0.56
},
{
"industry": "Professional, scientific and technical",
"employmentShare": 0.09,
"jobAutomation": 0.26
},
{
"industry": "Human health and social work",
"employmentShare": 0.12,
"jobAutomation": 0.17
},
{
"industry": "Accommodation and food services",
"employmentShare": 0.07,
"jobAutomation": 0.26
},
{
"industry": "Construction",
"employmentShare": 0.064,
"jobAutomation": 0.24
},
{
"industry": "Public administration and defence",
"employmentShare": 0.043,
"jobAutomation": 0.32
},
{
"industry": "Information and communication",
"employmentShare": 0.041,
"jobAutomation": 0.27
},
{
"industry": "Financial and insurance",
"employmentShare": 0.032,
"jobAutomation": 0.32
},
{
"industry": "Education",
"employmentShare": 0.087,
"jobAutomation": 0.09
},
{
"industry": "Arts and entertainment",
"employmentShare": 0.029,
"jobAutomation": 0.22
},
{
"industry": "Other services",
"employmentShare": 0.027,
"jobAutomation": 0.19
},
{
"industry": "Real estate",
"employmentShare": 0.017,
"jobAutomation": 0.28
},
{
"industry": "Water, sewage and waste management",
"employmentShare": 0.006,
"jobAutomation": 0.63
},
{
"industry": "Agriculture, forestry and fishing",
"employmentShare": 0.011,
"jobAutomation": 0.19
},
{
"industry": "Electricity and gas supply",
"employmentShare": 0.004,
"jobAutomation": 0.32
},
{
"industry": "Mining and quarrying",
"employmentShare": 0.002,
"jobAutomation": 0.23
},
{
"industry": "Domestic personnel and self-subsistence",
"employmentShare": 0.003,
"jobAutomation": 0.08
}
]
const width = 290;
const height = 750;
const margin = {"top": 25, "left": 300, "right": 300, "bottom": 75};
var xScale = d3.scaleLinear().range([0,width]).domain([0,1]); //jobAutomation
var yScale = d3.scaleLinear().range([0,height]); //employmentShare
var totalOffset = 0;
const rowGap = 20;
const dataLength = data.length;
const expandedHeight = height + (dataLength * rowGap);
const jobs = 34766667;
const overallPercent = 0.3;
const mainColour = "SeaGreen";
const backgroundColour = "PaleGoldenRod";
data.forEach(function(d){
d.offset = totalOffset;
totalOffset = d.offset + d.employmentShare;
});
yScale.domain([0,totalOffset]);
var xAxis = d3.axisTop(xScale).tickFormat(formatPercentage);
/////////////////////////////////////////////////////////////////////
var svg = d3.select("#chart").append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
var chart = svg.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
var iLabels = svg.append("g").attr("class", "labels").attr("transform", "translate(" + margin.left + "," + margin.top + ")");
var iLinks = svg.append("g").attr("class", "links")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
var vLabels = svg.append("g").attr("class", "labels").attr("transform", "translate(" + (width + margin.left) + "," + margin.top + ")");
var vLinks = svg.append("g").attr("class", "links")
.attr("transform", "translate(" + (width + margin.left) + "," + margin.top + ")");
var gAxis = svg.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")")
.call(xAxis);
gAxis.selectAll("text")
.style("fill", mainColour);
gAxis.selectAll("path").remove();
/////////////////////////////////////////////////////////////////////
var iNodes = data.map(function(industry){
var y = yScale(industry.offset) + yScale(industry.employmentShare)/2;
return new labella.Node(y, 15, industry);
});
var iRenderer = new labella.Renderer({
layerGap: 40,
nodeHeight: 5,
direction: 'left'
});
var iForce = new labella.Force({ minPos: 0, maxPos: (height + margin.bottom), nodeSpacing: 0, lineSpacing: 0 })
.nodes(iNodes)
.compute();
drawLabels(iForce.nodes());
function drawLabels(nodes){
// Add x,y,dx,dy to node
iRenderer.layout(nodes);
// Draw label rectangles
var iLabel = iLabels.selectAll('text')
.data(nodes)
.enter().append('g')
.attr('transform', function(d){return 'translate('+(d.x)+','+(d.y + 5)+')';});
iLabel.append('text')
.attr('x', 0)
.attr('y', 0)
.style('fill', mainColour)
.text(function(d){return (d.data.industry);})
.style("text-anchor", "end");
// Draw path from point on the timeline to the label rectangle
iLinks.selectAll('path')
.data(nodes)
.enter().append('path')
.attr('d', function(d){return iRenderer.generatePath(d);});
};
/////////////////////////////////////////////////////////////////////
var vNodes = data.map(function(industry){
var y = yScale(industry.offset) + yScale(industry.employmentShare)/2;
return new labella.Node(y, 15, industry);
});
var vRenderer = new labella.Renderer({
layerGap: 40,
nodeHeight: 5,
direction: 'right'
});
var vForce = new labella.Force({ minPos: 0, maxPos: (height + margin.bottom), nodeSpacing: 0, lineSpacing: 0 })
.nodes(vNodes)
.compute();
drawValues(vForce.nodes());
function drawValues(nodes){
// Add x,y,dx,dy to node
vRenderer.layout(nodes);
// Draw label rectangles
var vLabel = vLabels.selectAll('text')
.data(nodes)
.enter().append('g')
.attr('transform', function(d){return 'translate('+(d.x)+','+(d.y + 5)+')';});
vLabel.append('text')
.attr('x', 0)
.attr('y', 0)
.style('fill', mainColour)
.text(function(d){ return formatMillions((d.data.employmentShare * jobs) * d.data.jobAutomation) + " (" + formatPercentage(d.data.jobAutomation) + ")" })
.style("text-anchor", "start");
// Draw path from point on the timeline to the label rectangle
vLinks.selectAll('path')
.data(nodes)
.enter().append('path')
.attr('d', function(d){return vRenderer.generatePath(d);});
};
/////////////////////////////////////////////////////////////////////
var industries = chart.selectAll(".industry")
.data(data)
.enter()
.append("g")
.attr("class", "industry")
.attr("transform", function(d) { return "translate(0," + yScale(d.offset) + ")"; });
/*industries.append("text")
.text(function(d) { return formatPercentage(d.employmentShare * d.jobAutomation); })
.attr("x", width + 5)
.attr("y", function(d) { return (yScale(d.employmentShare)/2) + 5; })
.style("opacity", 0);*/
var all = industries.append("rect")
.attr("class", "all")
.attr("x", 0)
.attr("y", 0)
.attr("width", width)
.attr("height", function(d) { return yScale(d.employmentShare) < 3 ? 3 : yScale(d.employmentShare); })
.style("fill", backgroundColour);
/*industries.append("text")
.attr("class", "all-text")
.text(function(d){ return formatMillions(d.employmentShare * jobs) + " (" + formatPercentage(d.employmentShare) + ")" })
.attr("x", width/2)
.attr("y", function(d) { return 5 + yScale(d.employmentShare)/2; })
.style("text-anchor", "middle");*/
var automation = industries.append("rect")
.attr("class", "automation")
.attr("x", 0)
.attr("y", 0)
.attr("width", function(d) { return xScale(d.jobAutomation); })
.attr("height", function(d) { return yScale(d.employmentShare) < 3 ? 3 : yScale(d.employmentShare); })
.style("fill", mainColour);
/*industries.append("text")
.attr("class", "automation-text")
.text(function(d){ return formatMillions((d.employmentShare * jobs) * d.jobAutomation) + " (" + formatPercentage(d.jobAutomation) + ")" })
.attr("x", width + 5 )
.attr("y", function(d) { return 5 + yScale(d.employmentShare)/2; })
.style("fill", mainColour);*/
/*var industryLabel = industries.append("text")
.attr("class", "industry-label")
.text(function(d) { return d.industry })
.attr("x", -5)
.attr("y", function(d) { return (yScale(d.employmentShare)/2) + 5; })
.style("text-anchor", "end");*/
function formatPercentage(n) {
var roundedN = Math.round(n * 1000);
return roundedN/10 + "%";
};
function roundMillions(n) {
var roundedN = Math.round(n/10000)/100;
return roundedN;
};
function formatMillions(n) {
return roundMillions(n) + "M";
};
</script>
</body>
(function webpackUniversalModuleDefinition(root, factory) {
if(typeof exports === 'object' && typeof module === 'object')
module.exports = factory();
else if(typeof define === 'function' && define.amd)
define([], factory);
else if(typeof exports === 'object')
exports["labella"] = factory();
else
root["labella"] = factory();
})(this, function() {
return /******/ (function(modules) { // webpackBootstrap
/******/ // The module cache
/******/ var installedModules = {};
/******/ // The require function
/******/ function __webpack_require__(moduleId) {
/******/ // Check if module is in cache
/******/ if(installedModules[moduleId])
/******/ return installedModules[moduleId].exports;
/******/ // Create a new module (and put it into the cache)
/******/ var module = installedModules[moduleId] = {
/******/ exports: {},
/******/ id: moduleId,
/******/ loaded: false
/******/ };
/******/ // Execute the module function
/******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
/******/ // Flag the module as loaded
/******/ module.loaded = true;
/******/ // Return the exports of the module
/******/ return module.exports;
/******/ }
/******/ // expose the modules object (__webpack_modules__)
/******/ __webpack_require__.m = modules;
/******/ // expose the module cache
/******/ __webpack_require__.c = installedModules;
/******/ // __webpack_public_path__
/******/ __webpack_require__.p = "";
/******/ // Load entry module and return exports
/******/ return __webpack_require__(0);
/******/ })
/************************************************************************/
/******/ ([
/* 0 */
/***/ function(module, exports, __webpack_require__) {
'use strict';
/*
Copyright 2015 Twitter, Inc.
Licensed under the Apache License, Version 2.0
http://www.apache.org/licenses/LICENSE-2.0
*/
module.exports = {
Node: __webpack_require__(1),
Force: __webpack_require__(2),
Distributor: __webpack_require__(3),
Renderer: __webpack_require__(10)
};
/***/ },
/* 1 */
/***/ function(module, exports) {
'use strict';
var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }();
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
var Node = function () {
function Node(idealPos, width, data) {
_classCallCheck(this, Node);
this.idealPos = idealPos;
this.currentPos = idealPos;
this.width = width;
this.data = data;
this.layerIndex = 0;
}
// return negative if overlap
_createClass(Node, [{
key: 'distanceFrom',
value: function distanceFrom(node) {
var halfWidth = this.width / 2;
var nodeHalfWidth = node.width / 2;
// max(a[0], b[0]) - min(a[1], b[1])
return Math.max(this.currentPos - halfWidth, node.currentPos - nodeHalfWidth) - Math.min(this.currentPos + halfWidth, node.currentPos + nodeHalfWidth);
}
}, {
key: 'moveToIdealPosition',
value: function moveToIdealPosition() {
this.currentPos = this.idealPos;
return this;
}
}, {
key: 'displacement',
value: function displacement() {
return this.idealPos - this.currentPos;
}
}, {
key: 'overlapWithNode',
value: function overlapWithNode(node) {
var buffer = arguments.length <= 1 || arguments[1] === undefined ? 0 : arguments[1];
return this.distanceFrom(node) - buffer < 0;
}
}, {
key: 'overlapWithPoint',
value: function overlapWithPoint(pos) {
var halfWidth = this.width / 2;
return pos >= this.currentPos - halfWidth && pos <= this.currentPos + halfWidth;
}
}, {
key: 'positionBefore',
value: function positionBefore(node) {
var buffer = arguments.length <= 1 || arguments[1] === undefined ? 0 : arguments[1];
return node.currentLeft() - this.width / 2 - buffer;
}
}, {
key: 'positionAfter',
value: function positionAfter(node) {
var buffer = arguments.length <= 1 || arguments[1] === undefined ? 0 : arguments[1];
return node.currentRight() + this.width / 2 + buffer;
}
}, {
key: 'currentRight',
value: function currentRight() {
return this.currentPos + this.width / 2;
}
}, {
key: 'currentLeft',
value: function currentLeft() {
return this.currentPos - this.width / 2;
}
}, {
key: 'idealRight',
value: function idealRight() {
return this.idealPos + this.width / 2;
}
}, {
key: 'idealLeft',
value: function idealLeft() {
return this.idealPos - this.width / 2;
}
}, {
key: 'createStub',
value: function createStub(width) {
var stub = new Node(this.idealPos, width, this.data);
stub.currentPos = this.currentPos;
stub.child = this;
this.parent = stub;
return stub;
}
}, {
key: 'removeStub',
value: function removeStub() {
if (this.parent) {
this.parent.child = null;
this.parent = null;
}
return this;
}
}, {
key: 'isStub',
value: function isStub() {
return !!this.child;
}
}, {
key: 'getPathToRoot',
value: function getPathToRoot() {
var path = [];
var current = this;
while (current) {
path.push(current);
current = current.parent;
}
return path;
}
}, {
key: 'getPathFromRoot',
value: function getPathFromRoot() {
return this.getPathToRoot().reverse();
}
}, {
key: 'getPathToRootLength',
value: function getPathToRootLength() {
var length = 0;
var current = this;
while (current) {
var targetPos = current.parent ? current.parent.currentPos : current.idealPos;
length += Math.abs(current.currentPos - targetPos);
current = current.parent;
}
return length;
}
// Trace back to the node without parent
}, {
key: 'getRoot',
value: function getRoot() {
var previous = this;
var current = this;
while (current) {
previous = current;
current = current.parent;
}
return previous;
}
}, {
key: 'getLayerIndex',
value: function getLayerIndex() {
return this.layerIndex;
}
}, {
key: 'clone',
value: function clone() {
var node = new Node(this.idealPos, this.width, this.data);
node.currentPos = this.currentPos;
node.layerIndex = this.layerIndex;
return node;
}
}]);
return Node;
}();
module.exports = Node;
/***/ },
/* 2 */
/***/ function(module, exports, __webpack_require__) {
'use strict';
var Distributor = __webpack_require__(3);
var helper = __webpack_require__(4);
var removeOverlap = __webpack_require__(8);
var DEFAULT_OPTIONS = {
nodeSpacing: 3,
minPos: 0,
maxPos: null,
algorithm: 'overlap',
removeOverlap: true,
density: 0.85,
stubWidth: 1
};
var Force = function Force(_options) {
var force = {};
var options = helper.extend({}, DEFAULT_OPTIONS);
var distributor = new Distributor();
var nodes = [];
var layers = null;
force.nodes = function (x) {
if (!arguments.length) return nodes;
nodes = x;
layers = [x];
return force;
};
force.getLayers = function () {
return layers;
};
force.options = function (x) {
if (!arguments.length) return options;
options = helper.extend(options, x);
var disOptions = helper.pick(options, Object.keys(Distributor.DEFAULT_OPTIONS));
if (helper.isDefined(options.minPos) && helper.isDefined(options.maxPos)) {
disOptions.layerWidth = options.maxPos - options.minPos;
} else {
disOptions.layerWidth = null;
}
distributor.options(disOptions);
return force;
};
force.options(_options);
force.compute = function () {
var overlapOptions = helper.pick(options, Object.keys(removeOverlap.DEFAULT_OPTIONS));
nodes.forEach(function (node) {
node.removeStub();
});
layers = distributor.distribute(nodes);
layers.map(function (nodes, layerIndex) {
nodes.forEach(function (node) {
node.layerIndex = layerIndex;
});
if (options.removeOverlap) {
removeOverlap(nodes, overlapOptions);
}
});
return force;
};
force.start = function () {
console.log('[warning] force.start() is deprecated. Please use force.compute() instead.');
};
return force;
};
Force.DEFAULT_OPTIONS = DEFAULT_OPTIONS;
module.exports = Force;
/***/ },
/* 3 */
/***/ function(module, exports, __webpack_require__) {
'use strict';
var helper = __webpack_require__(4);
var IntervalTree = __webpack_require__(6);
var DEFAULT_OPTIONS = {
algorithm: 'overlap',
layerWidth: 1000,
density: 0.75,
nodeSpacing: 3,
stubWidth: 1
};
var Distributor = function Distributor(options) {
var distributor = {};
options = helper.extend({}, DEFAULT_OPTIONS, options);
distributor.options = function (x) {
if (!arguments.length) return options;
options = helper.extend(options, x);
return distributor;
};
distributor.computeRequiredWidth = function (nodes) {
return helper.sum(nodes, function (d) {
return d.width + options.nodeSpacing;
}) - options.nodeSpacing;
};
distributor.maxWidthPerLayer = function () {
return options.density * options.layerWidth;
};
distributor.needToSplit = function (nodes) {
return distributor.estimateRequiredLayers(nodes) > 1;
};
distributor.estimateRequiredLayers = function (nodes) {
return options.layerWidth ? Math.ceil(distributor.computeRequiredWidth(nodes) / distributor.maxWidthPerLayer()) : 1;
};
var algorithms = {
simple: function simple(nodes) {
var numLayers = distributor.estimateRequiredLayers(nodes);
var layers = [];
for (var i = 0; i < numLayers; i++) {
layers.push([]);
}
nodes.forEach(function (node, i) {
var mod = i % numLayers;
layers[mod].push(node);
var stub = node;
for (var j = mod - 1; j >= 0; j--) {
stub = stub.createStub(options.stubWidth);
layers[j].push(stub);
}
});
return layers;
},
roundRobin: function roundRobin(nodes) {
var layers = [];
return layers;
},
overlap: function overlap(nodes) {
var layers = [];
var maxWidth = distributor.maxWidthPerLayer();
var puntedNodes = nodes.concat();
var puntedWidth = distributor.computeRequiredWidth(puntedNodes);
while (puntedWidth > maxWidth) {
distributor.countIdealOverlaps(puntedNodes);
var nodesInCurrentLayer = puntedNodes.concat();
var currentlayerWidth = puntedWidth;
puntedNodes = [];
while (nodesInCurrentLayer.length > 2 && currentlayerWidth > maxWidth) {
// Sort by overlaps
nodesInCurrentLayer.sort(function (a, b) {
return b.overlapCount - a.overlapCount;
});
// Remove the node with most overlap
var first = nodesInCurrentLayer.shift();
// Update width
currentlayerWidth -= first.width;
currentlayerWidth += options.stubWidth;
// Update overlap count for the remaining nodes
first.overlaps.forEach(function (node) {
node.overlapCount--;
});
// Add removed node to the next layer
puntedNodes.push(first);
}
layers.push(nodesInCurrentLayer);
puntedWidth = distributor.computeRequiredWidth(puntedNodes);
}
if (puntedNodes.length > 0) {
layers.push(puntedNodes);
}
// Create stubs
// From last layer
for (var i = layers.length - 1; i >= 1; i--) {
var layer = layers[i];
// For each node in the layer
for (var k = 0; k < layer.length; k++) {
var node = layer[k];
// If it is not a stub
if (node.isStub()) continue;
// Create one stub for each layer above it
var stub = node;
for (var j = i - 1; j >= 0; j--) {
stub = stub.createStub(options.stubWidth);
layers[j].push(stub);
}
}
}
return layers;
}
};
distributor.countIdealOverlaps = function (nodes) {
var iTree = new IntervalTree(options.layerWidth / 2);
nodes.forEach(function (node) {
iTree.add([node.idealLeft(), node.idealRight(), node]);
});
nodes.forEach(function (node) {
var overlaps = iTree.search(node.idealLeft(), node.idealRight());
node.overlaps = overlaps.map(function (x) {
return x.data[2];
});
node.overlapCount = overlaps.length;
});
return nodes;
};
distributor.distribute = function (nodes) {
if (!nodes || nodes.length === 0) return [];
if (options.algorithm == 'none' || !helper.isDefined(options.algorithm)) {
return [nodes];
}
if (!distributor.needToSplit(nodes)) {
return [nodes];
}
nodes = nodes.concat().sort(function (a, b) {
return a.idealPos - b.idealPos;
});
if (typeof options.algorithm == 'function') {
return options.algorithm(nodes, options);
} else if (algorithms.hasOwnProperty(options.algorithm)) {
return algorithms[options.algorithm](nodes);
} else {
throw 'Unknown algorithm: ' + options.algorithm;
}
};
return distributor;
};
Distributor.DEFAULT_OPTIONS = DEFAULT_OPTIONS;
// return module
module.exports = Distributor;
/***/ },
/* 4 */
/***/ function(module, exports, __webpack_require__) {
'use strict';
var helper = {
isDefined: function isDefined(x) {
return x !== null && x !== undefined;
},
last: function last(array) {
return array.length > 0 ? array[array.length - 1] : null;
},
pick: function pick(object, keys) {
return keys.reduce(function (prev, key) {
prev[key] = object[key];
return prev;
}, {});
},
sum: function sum(array, accessor) {
return array.map(accessor).reduce(function (prev, current) {
return prev + current;
}, 0);
}
};
helper.extend = __webpack_require__(5);
module.exports = helper;
/***/ },
/* 5 */
/***/ function(module, exports) {
'use strict';
var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol ? "symbol" : typeof obj; };
/*
This file is modified from https://github.com/justmoon/node-extend
The MIT License (MIT)
Copyright (c) 2014 Stefan Thomas
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
var hasOwn = Object.prototype.hasOwnProperty;
var toStr = Object.prototype.toString;
var isArray = function isArray(arr) {
if (typeof Array.isArray === 'function') {
return Array.isArray(arr);
}
return toStr.call(arr) === '[object Array]';
};
var isPlainObject = function isPlainObject(obj) {
'use strict';
if (!obj || toStr.call(obj) !== '[object Object]') {
return false;
}
var has_own_constructor = hasOwn.call(obj, 'constructor');
var has_is_property_of_method = obj.constructor && obj.constructor.prototype && hasOwn.call(obj.constructor.prototype, 'isPrototypeOf');
// Not own constructor property must be Object
if (obj.constructor && !has_own_constructor && !has_is_property_of_method) {
return false;
}
// Own properties are enumerated firstly, so to speed up,
// if last one is own, then all properties are own.
var key;
for (key in obj) {}
return key === undefined || hasOwn.call(obj, key);
};
module.exports = function extend() {
'use strict';
var options,
name,
src,
copy,
copyIsArray,
clone,
target = arguments[0],
i = 1,
length = arguments.length,
deep = false;
// Handle a deep copy situation
if (typeof target === 'boolean') {
deep = target;
target = arguments[1] || {};
// skip the boolean and the target
i = 2;
} else if ((typeof target === 'undefined' ? 'undefined' : _typeof(target)) !== 'object' && typeof target !== 'function' || target == null) {
target = {};
}
for (; i < length; ++i) {
options = arguments[i];
// Only deal with non-null/undefined values
if (options != null) {
// Extend the base object
for (name in options) {
src = target[name];
copy = options[name];
// Prevent never-ending loop
if (target === copy) {
continue;
}
// Recurse if we're merging plain objects or arrays
if (deep && copy && (isPlainObject(copy) || (copyIsArray = isArray(copy)))) {
if (copyIsArray) {
copyIsArray = false;
clone = src && isArray(src) ? src : [];
} else {
clone = src && isPlainObject(src) ? src : {};
}
// Never move original objects, clone them
target[name] = extend(deep, clone, copy);
// Don't bring in undefined values
} else if (copy !== undefined) {
target[name] = copy;
}
}
}
}
// Return the modified object
return target;
};
/***/ },
/* 6 */
/***/ function(module, exports, __webpack_require__) {
'use strict';
/*
This file is modified from https://github.com/shinout/interval-tree
(The MIT License)
Copyright (c) 2012 SHIN Suzuki <shinout310@gmail.com>
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
'Software'), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
var SortedList = __webpack_require__(7);
/**
* IntervalTree
*
* @param (object) data:
* @param (number) center:
* @param (object) options:
* center:
*
**/
function IntervalTree(center, options) {
options || (options = {});
this.startKey = options.startKey || 0; // start key
this.endKey = options.endKey || 1; // end key
this.intervalHash = {}; // id => interval object
this.pointTree = new SortedList({ // b-tree of start, end points
compare: function compare(a, b) {
if (a == null) return -1;
if (b == null) return 1;
var c = a[0] - b[0];
return c > 0 ? 1 : c == 0 ? 0 : -1;
}
});
this._autoIncrement = 0;
// index of the root node
if (!center || typeof center != 'number') {
throw new Error('you must specify center index as the 2nd argument.');
}
this.root = new Node(center, this);
}
/**
* publid methods
**/
/**
* add new range
**/
IntervalTree.prototype.add = function (data, id) {
if (this.intervalHash[id]) {
throw new Error('id ' + id + ' is already registered.');
}
if (id == undefined) {
while (this.intervalHash[this._autoIncrement]) {
this._autoIncrement++;
}
id = this._autoIncrement;
}
var itvl = new Interval(data, id, this.startKey, this.endKey);
this.pointTree.insert([itvl.start, id]);
this.pointTree.insert([itvl.end, id]);
this.intervalHash[id] = itvl;
this._autoIncrement++;
_insert.call(this, this.root, itvl);
};
/**
* search
*
* @param (integer) val:
* @return (array)
**/
IntervalTree.prototype.search = function (val1, val2) {
var ret = [];
if (typeof val1 != 'number') {
throw new Error(val1 + ': invalid input');
}
if (val2 == undefined) {
_pointSearch.call(this, this.root, val1, ret);
} else if (typeof val2 == 'number') {
_rangeSearch.call(this, val1, val2, ret);
} else {
throw new Error(val1 + ',' + val2 + ': invalid input');
}
return ret;
};
/**
* remove:
**/
IntervalTree.prototype.remove = function (interval_id) {};
/**
* private methods
**/
/**
* _insert
**/
function _insert(node, itvl) {
if (itvl.end < node.idx) {
if (!node.left) {
node.left = new Node(itvl.start + itvl.end >> 1, this);
}
return _insert.call(this, node.left, itvl);
}
if (node.idx < itvl.start) {
if (!node.right) {
node.right = new Node(itvl.start + itvl.end >> 1, this);
}
return _insert.call(this, node.right, itvl);
}
return node.insert(itvl);
}
/**
* _pointSearch
* @param (Node) node
* @param (integer) idx
* @param (Array) arr
**/
function _pointSearch(node, idx, arr) {
if (!node) return;
if (idx < node.idx) {
node.starts.every(function (itvl) {
var bool = itvl.start <= idx;
if (bool) arr.push(itvl.result());
return bool;
});
return _pointSearch.call(this, node.left, idx, arr);
} else if (idx > node.idx) {
node.ends.every(function (itvl) {
var bool = itvl.end >= idx;
if (bool) arr.push(itvl.result());
return bool;
});
return _pointSearch.call(this, node.right, idx, arr);
}
// exact equal
else {
node.starts.map(function (itvl) {
arr.push(itvl.result());
});
}
}
/**
* _rangeSearch
* @param (integer) start
* @param (integer) end
* @param (Array) arr
**/
function _rangeSearch(start, end, arr) {
if (end - start <= 0) {
throw new Error('end must be greater than start. start: ' + start + ', end: ' + end);
}
var resultHash = {};
var wholeWraps = [];
_pointSearch.call(this, this.root, start + end >> 1, wholeWraps, true);
wholeWraps.forEach(function (result) {
resultHash[result.id] = true;
});
var idx1 = this.pointTree.bsearch([start, null]);
var pointTreeArray = this.pointTree;
while (idx1 >= 0 && pointTreeArray[idx1][0] == start) {
idx1--;
}
var idx2 = this.pointTree.bsearch([end, null]);
if (idx2 >= 0) {
var len = pointTreeArray.length - 1;
while (idx2 <= len && pointTreeArray[idx2][0] <= end) {
idx2++;
}
pointTreeArray.slice(idx1 + 1, idx2).forEach(function (point) {
var id = point[1];
resultHash[id] = true;
}, this);
Object.keys(resultHash).forEach(function (id) {
var itvl = this.intervalHash[id];
arr.push(itvl.result(start, end));
}, this);
}
}
/**
* subclasses
*
**/
/**
* Node : prototype of each node in a interval tree
*
**/
function Node(idx) {
this.idx = idx;
this.starts = new SortedList({
compare: function compare(a, b) {
if (a == null) return -1;
if (b == null) return 1;
var c = a.start - b.start;
return c > 0 ? 1 : c == 0 ? 0 : -1;
}
});
this.ends = new SortedList({
compare: function compare(a, b) {
if (a == null) return -1;
if (b == null) return 1;
var c = a.end - b.end;
return c < 0 ? 1 : c == 0 ? 0 : -1;
}
});
}
/**
* insert an Interval object to this node
**/
Node.prototype.insert = function (interval) {
this.starts.insert(interval);
this.ends.insert(interval);
};
/**
* Interval : prototype of interval info
**/
function Interval(data, id, s, e) {
this.id = id;
this.start = data[s];
this.end = data[e];
this.data = data;
if (typeof this.start != 'number' || typeof this.end != 'number') {
throw new Error('start, end must be number. start: ' + this.start + ', end: ' + this.end);
}
if (this.start >= this.end) {
throw new Error('start must be smaller than end. start: ' + this.start + ', end: ' + this.end);
}
}
/**
* get result object
**/
Interval.prototype.result = function (start, end) {
var ret = {
id: this.id,
data: this.data
};
if (typeof start == 'number' && typeof end == 'number') {
/**
* calc overlapping rate
**/
var left = Math.max(this.start, start);
var right = Math.min(this.end, end);
var lapLn = right - left;
ret.rate1 = lapLn / (end - start);
ret.rate2 = lapLn / (this.end - this.start);
}
return ret;
};
module.exports = IntervalTree;
/***/ },
/* 7 */
/***/ function(module, exports) {
"use strict";
var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol ? "symbol" : typeof obj; };
/*
This file is modified from https://github.com/shinout/SortedList
(The MIT License)
Copyright (c) 2012 SHIN Suzuki <shinout310@gmail.com>
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
'Software'), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
var SortedList = function SortedList() {
var arr = null,
options = {},
args = arguments;
["0", "1"].forEach(function (n) {
var val = args[n];
if (Array.isArray(val)) {
arr = val;
} else if (val && (typeof val === "undefined" ? "undefined" : _typeof(val)) == "object") {
options = val;
}
});
if (typeof options.filter == 'function') {
this._filter = options.filter;
}
if (typeof options.compare == 'function') {
this._compare = options.compare;
} else if (typeof options.compare == 'string' && SortedList.compares[options.compare]) {
this._compare = SortedList.compares[options.compare];
}
this._unique = !!options.unique;
if (options.resume && arr) {
arr.forEach(function (v, i) {
this.push(v);
}, this);
} else if (arr) this.insert.apply(this, arr);
};
/**
* SortedList.create(val1, val2)
* creates an instance
**/
SortedList.create = function (val1, val2) {
return new SortedList(val1, val2);
};
SortedList.prototype = new Array();
SortedList.prototype.constructor = Array.prototype.constructor;
/**
* sorted.insertOne(val)
* insert one value
* returns false if failed, inserted position if succeed
**/
SortedList.prototype.insertOne = function (val) {
var pos = this.bsearch(val);
if (this._unique && this.key(val, pos) != null) return false;
if (!this._filter(val, pos)) return false;
this.splice(pos + 1, 0, val);
return pos + 1;
};
/**
* sorted.insert(val1, val2, ...)
* insert multi values
* returns the list of the results of insertOne()
**/
SortedList.prototype.insert = function () {
return Array.prototype.map.call(arguments, function (val) {
return this.insertOne(val);
}, this);
};
/**
* sorted.remove(pos)
* remove the value in the given position
**/
SortedList.prototype.remove = function (pos) {
this.splice(pos, 1);
return this;
};
/**
* sorted.bsearch(val)
* @returns position of the value
**/
SortedList.prototype.bsearch = function (val) {
if (!this.length) return -1;
var mpos,
spos = 0,
epos = this.length;
while (epos - spos > 1) {
mpos = Math.floor((spos + epos) / 2);
var mval = this[mpos];
var comp = this._compare(val, mval);
if (comp == 0) return mpos;
if (comp > 0) spos = mpos;else epos = mpos;
}
return spos == 0 && this._compare(this[0], val) > 0 ? -1 : spos;
};
/**
* sorted.key(val)
* @returns first index if exists, null if not
**/
SortedList.prototype.key = function (val, bsResult) {
if (bsResult == null) bsResult = this.bsearch(val);
var pos = bsResult;
if (pos == -1 || this._compare(this[pos], val) < 0) return pos + 1 < this.length && this._compare(this[pos + 1], val) == 0 ? pos + 1 : null;
while (pos >= 1 && this._compare(this[pos - 1], val) == 0) {
pos--;
}return pos;
};
/**
* sorted.key(val)
* @returns indexes if exists, null if not
**/
SortedList.prototype.keys = function (val, bsResult) {
var ret = [];
if (bsResult == null) bsResult = this.bsearch(val);
var pos = bsResult;
while (pos >= 0 && this._compare(this[pos], val) == 0) {
ret.push(pos);
pos--;
}
var len = this.length;
pos = bsResult + 1;
while (pos < len && this._compare(this[pos], val) == 0) {
ret.push(pos);
pos++;
}
return ret.length ? ret : null;
};
/**
* sorted.unique()
* @param createNew : create new instance
* @returns first index if exists, null if not
**/
SortedList.prototype.unique = function (createNew) {
if (createNew) return this.filter(function (v, k) {
return k == 0 || this._compare(this[k - 1], v) != 0;
}, this);
var total = 0;
this.map(function (v, k) {
if (k == 0 || this._compare(this[k - 1], v) != 0) return null;
return k - total++;
}, this).forEach(function (k) {
if (k != null) this.remove(k);
}, this);
return this;
};
/**
* sorted.toArray()
* get raw array
**/
SortedList.prototype.toArray = function () {
return this.slice();
};
/**
* default filtration function
**/
SortedList.prototype._filter = function (val, pos) {
return true;
};
/**
* comparison functions
**/
SortedList.compares = {
"number": function number(a, b) {
var c = a - b;
return c > 0 ? 1 : c == 0 ? 0 : -1;
},
"string": function string(a, b) {
return a > b ? 1 : a == b ? 0 : -1;
}
};
/**
* sorted.compare(a, b)
* default comparison function
**/
SortedList.prototype._compare = SortedList.compares["string"];
module.exports = SortedList;
/***/ },
/* 8 */
/***/ function(module, exports, __webpack_require__) {
'use strict';
var helper = __webpack_require__(4);
var vpsc = __webpack_require__(9);
var DEFAULT_OPTIONS = {
lineSpacing: 2,
nodeSpacing: 3,
minPos: 0,
maxPos: null
};
function nodeToVariable(node) {
var v = new vpsc.Variable(node.targetPos);
v.node = node;
return v;
}
function removeOverlap(nodes, options) {
if (nodes.length > 0) {
options = helper.extend(DEFAULT_OPTIONS, options);
// For nodes with stub, set target position to stub's current position
nodes.forEach(function (node, index) {
node.targetPos = node.parent ? node.parent.currentPos : node.idealPos;
node.index = index;
});
nodes.sort(function (a, b) {
var diff = a.targetPos - b.targetPos;
if (diff !== 0) return diff;
var diff2 = a.isStub() - b.isStub();
if (diff2 !== 0) return diff2;
// If same position, use original order
return a.index - b.index;
});
var variables = nodes.map(nodeToVariable);
var constraints = [];
for (var i = 1; i < variables.length; i++) {
var v1 = variables[i - 1];
var v2 = variables[i];
var gap;
if (v1.node.isStub() && v2.node.isStub()) {
gap = (v1.node.width + v2.node.width) / 2 + options.lineSpacing;
} else {
gap = (v1.node.width + v2.node.width) / 2 + options.nodeSpacing;
}
constraints.push(new vpsc.Constraint(v1, v2, gap));
}
if (helper.isDefined(options.minPos)) {
var leftWall = new vpsc.Variable(options.minPos, 1e10);
var v = variables[0];
constraints.push(new vpsc.Constraint(leftWall, v, v.node.width / 2));
variables.unshift(leftWall);
}
if (helper.isDefined(options.maxPos)) {
var rightWall = new vpsc.Variable(options.maxPos, 1e10);
var lastv = helper.last(variables);
constraints.push(new vpsc.Constraint(lastv, rightWall, lastv.node.width / 2));
variables.push(rightWall);
}
new vpsc.Solver(variables, constraints).solve();
variables.filter(function (v) {
return v.node;
}).map(function (v) {
v.node.currentPos = Math.round(v.position());
return v;
});
}
return nodes;
}
removeOverlap.DEFAULT_OPTIONS = DEFAULT_OPTIONS;
module.exports = removeOverlap;
/***/ },
/* 9 */
/***/ function(module, exports) {
"use strict";
/*
This file is compiled from https://github.com/tgdwyer/WebCola/blob/master/WebCola/src/vpsc.ts
and modified slightly.
The MIT License (MIT)
Copyright (c) 2013 Tim Dwyer
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/
var vpsc = {};
var PositionStats = function () {
function PositionStats(scale) {
this.scale = scale;
this.AB = 0;
this.AD = 0;
this.A2 = 0;
}
PositionStats.prototype.addVariable = function (v) {
var ai = this.scale / v.scale;
var bi = v.offset / v.scale;
var wi = v.weight;
this.AB += wi * ai * bi;
this.AD += wi * ai * v.desiredPosition;
this.A2 += wi * ai * ai;
};
PositionStats.prototype.getPosn = function () {
return (this.AD - this.AB) / this.A2;
};
return PositionStats;
}();
vpsc.PositionStats = PositionStats;
var Constraint = function () {
function Constraint(left, right, gap, equality) {
if (equality === void 0) {
equality = false;
}
this.left = left;
this.right = right;
this.gap = gap;
this.equality = equality;
this.active = false;
this.unsatisfiable = false;
this.left = left;
this.right = right;
this.gap = gap;
this.equality = equality;
}
Constraint.prototype.slack = function () {
return this.unsatisfiable ? Number.MAX_VALUE : this.right.scale * this.right.position() - this.gap - this.left.scale * this.left.position();
};
return Constraint;
}();
vpsc.Constraint = Constraint;
var Variable = function () {
function Variable(desiredPosition, weight, scale) {
if (weight === void 0) {
weight = 1;
}
if (scale === void 0) {
scale = 1;
}
this.desiredPosition = desiredPosition;
this.weight = weight;
this.scale = scale;
this.offset = 0;
}
Variable.prototype.dfdv = function () {
return 2.0 * this.weight * (this.position() - this.desiredPosition);
};
Variable.prototype.position = function () {
return (this.block.ps.scale * this.block.posn + this.offset) / this.scale;
};
// visit neighbours by active constraints within the same block
Variable.prototype.visitNeighbours = function (prev, f) {
var ff = function ff(c, next) {
return c.active && prev !== next && f(c, next);
};
this.cOut.forEach(function (c) {
return ff(c, c.right);
});
this.cIn.forEach(function (c) {
return ff(c, c.left);
});
};
return Variable;
}();
vpsc.Variable = Variable;
var Block = function () {
function Block(v) {
this.vars = [];
v.offset = 0;
this.ps = new PositionStats(v.scale);
this.addVariable(v);
}
Block.prototype.addVariable = function (v) {
v.block = this;
this.vars.push(v);
this.ps.addVariable(v);
this.posn = this.ps.getPosn();
};
// move the block where it needs to be to minimize cost
Block.prototype.updateWeightedPosition = function () {
this.ps.AB = this.ps.AD = this.ps.A2 = 0;
for (var i = 0, n = this.vars.length; i < n; ++i) {
this.ps.addVariable(this.vars[i]);
}this.posn = this.ps.getPosn();
};
Block.prototype.compute_lm = function (v, u, postAction) {
var _this = this;
var dfdv = v.dfdv();
v.visitNeighbours(u, function (c, next) {
var _dfdv = _this.compute_lm(next, v, postAction);
if (next === c.right) {
dfdv += _dfdv * c.left.scale;
c.lm = _dfdv;
} else {
dfdv += _dfdv * c.right.scale;
c.lm = -_dfdv;
}
postAction(c);
});
return dfdv / v.scale;
};
Block.prototype.populateSplitBlock = function (v, prev) {
var _this = this;
v.visitNeighbours(prev, function (c, next) {
next.offset = v.offset + (next === c.right ? c.gap : -c.gap);
_this.addVariable(next);
_this.populateSplitBlock(next, v);
});
};
// traverse the active constraint tree applying visit to each active constraint
Block.prototype.traverse = function (visit, acc, v, prev) {
var _this = this;
if (v === void 0) {
v = this.vars[0];
}
if (prev === void 0) {
prev = null;
}
v.visitNeighbours(prev, function (c, next) {
acc.push(visit(c));
_this.traverse(visit, acc, next, v);
});
};
// calculate lagrangian multipliers on constraints and
// find the active constraint in this block with the smallest lagrangian.
// if the lagrangian is negative, then the constraint is a split candidate.
Block.prototype.findMinLM = function () {
var m = null;
this.compute_lm(this.vars[0], null, function (c) {
if (!c.equality && (m === null || c.lm < m.lm)) m = c;
});
return m;
};
Block.prototype.findMinLMBetween = function (lv, rv) {
this.compute_lm(lv, null, function () {});
var m = null;
this.findPath(lv, null, rv, function (c, next) {
if (!c.equality && c.right === next && (m === null || c.lm < m.lm)) m = c;
});
return m;
};
Block.prototype.findPath = function (v, prev, to, visit) {
var _this = this;
var endFound = false;
v.visitNeighbours(prev, function (c, next) {
if (!endFound && (next === to || _this.findPath(next, v, to, visit))) {
endFound = true;
visit(c, next);
}
});
return endFound;
};
// Search active constraint tree from u to see if there is a directed path to v.
// Returns true if path is found.
Block.prototype.isActiveDirectedPathBetween = function (u, v) {
if (u === v) return true;
var i = u.cOut.length;
while (i--) {
var c = u.cOut[i];
if (c.active && this.isActiveDirectedPathBetween(c.right, v)) return true;
}
return false;
};
// split the block into two by deactivating the specified constraint
Block.split = function (c) {
/* DEBUG
console.log("split on " + c);
console.assert(c.active, "attempt to split on inactive constraint");
DEBUG */
c.active = false;
return [Block.createSplitBlock(c.left), Block.createSplitBlock(c.right)];
};
Block.createSplitBlock = function (startVar) {
var b = new Block(startVar);
b.populateSplitBlock(startVar, null);
return b;
};
// find a split point somewhere between the specified variables
Block.prototype.splitBetween = function (vl, vr) {
/* DEBUG
console.assert(vl.block === this);
console.assert(vr.block === this);
DEBUG */
var c = this.findMinLMBetween(vl, vr);
if (c !== null) {
var bs = Block.split(c);
return { constraint: c, lb: bs[0], rb: bs[1] };
}
// couldn't find a split point - for example the active path is all equality constraints
return null;
};
Block.prototype.mergeAcross = function (b, c, dist) {
c.active = true;
for (var i = 0, n = b.vars.length; i < n; ++i) {
var v = b.vars[i];
v.offset += dist;
this.addVariable(v);
}
this.posn = this.ps.getPosn();
};
Block.prototype.cost = function () {
var sum = 0,
i = this.vars.length;
while (i--) {
var v = this.vars[i],
d = v.position() - v.desiredPosition;
sum += d * d * v.weight;
}
return sum;
};
return Block;
}();
vpsc.Block = Block;
var Blocks = function () {
function Blocks(vs) {
this.vs = vs;
var n = vs.length;
this.list = new Array(n);
while (n--) {
var b = new Block(vs[n]);
this.list[n] = b;
b.blockInd = n;
}
}
Blocks.prototype.cost = function () {
var sum = 0,
i = this.list.length;
while (i--) {
sum += this.list[i].cost();
}return sum;
};
Blocks.prototype.insert = function (b) {
/* DEBUG
console.assert(!this.contains(b), "blocks error: tried to reinsert block " + b.blockInd)
DEBUG */
b.blockInd = this.list.length;
this.list.push(b);
/* DEBUG
console.log("insert block: " + b.blockInd);
this.contains(b);
DEBUG */
};
Blocks.prototype.remove = function (b) {
/* DEBUG
console.log("remove block: " + b.blockInd);
console.assert(this.contains(b));
DEBUG */
var last = this.list.length - 1;
var swapBlock = this.list[last];
this.list.length = last;
if (b !== swapBlock) {
this.list[b.blockInd] = swapBlock;
swapBlock.blockInd = b.blockInd;
}
};
// merge the blocks on either side of the specified constraint, by copying the smaller block into the larger
// and deleting the smaller.
Blocks.prototype.merge = function (c) {
var l = c.left.block,
r = c.right.block;
/* DEBUG
console.assert(l!==r, "attempt to merge within the same block");
DEBUG */
var dist = c.right.offset - c.left.offset - c.gap;
if (l.vars.length < r.vars.length) {
r.mergeAcross(l, c, dist);
this.remove(l);
} else {
l.mergeAcross(r, c, -dist);
this.remove(r);
}
/* DEBUG
console.assert(Math.abs(c.slack()) < 1e-6, "Error: Constraint should be at equality after merge!");
console.log("merged on " + c);
DEBUG */
};
Blocks.prototype.forEach = function (f) {
this.list.forEach(f);
};
// useful, for example, after variable desired positions change.
Blocks.prototype.updateBlockPositions = function () {
this.list.forEach(function (b) {
return b.updateWeightedPosition();
});
};
// split each block across its constraint with the minimum lagrangian
Blocks.prototype.split = function (inactive) {
var _this = this;
this.updateBlockPositions();
this.list.forEach(function (b) {
var v = b.findMinLM();
if (v !== null && v.lm < Solver.LAGRANGIAN_TOLERANCE) {
b = v.left.block;
Block.split(v).forEach(function (nb) {
return _this.insert(nb);
});
_this.remove(b);
inactive.push(v);
}
});
};
return Blocks;
}();
vpsc.Blocks = Blocks;
var Solver = function () {
function Solver(vs, cs) {
this.vs = vs;
this.cs = cs;
this.vs = vs;
vs.forEach(function (v) {
v.cIn = [], v.cOut = [];
/* DEBUG
v.toString = () => "v" + vs.indexOf(v);
DEBUG */
});
this.cs = cs;
cs.forEach(function (c) {
c.left.cOut.push(c);
c.right.cIn.push(c);
/* DEBUG
c.toString = () => c.left + "+" + c.gap + "<=" + c.right + " slack=" + c.slack() + " active=" + c.active;
DEBUG */
});
this.inactive = cs.map(function (c) {
c.active = false;return c;
});
this.bs = null;
}
Solver.prototype.cost = function () {
return this.bs.cost();
};
// set starting positions without changing desired positions.
// Note: it throws away any previous block structure.
Solver.prototype.setStartingPositions = function (ps) {
this.inactive = this.cs.map(function (c) {
c.active = false;return c;
});
this.bs = new Blocks(this.vs);
this.bs.forEach(function (b, i) {
return b.posn = ps[i];
});
};
Solver.prototype.setDesiredPositions = function (ps) {
this.vs.forEach(function (v, i) {
return v.desiredPosition = ps[i];
});
};
/* DEBUG
private getId(v: Variable): number {
return this.vs.indexOf(v);
}
// sanity check of the index integrity of the inactive list
checkInactive(): void {
var inactiveCount = 0;
this.cs.forEach(c=> {
var i = this.inactive.indexOf(c);
console.assert(!c.active && i >= 0 || c.active && i < 0, "constraint should be in the inactive list if it is not active: " + c);
if (i >= 0) {
inactiveCount++;
} else {
console.assert(c.active, "inactive constraint not found in inactive list: " + c);
}
});
console.assert(inactiveCount === this.inactive.length, inactiveCount + " inactive constraints found, " + this.inactive.length + "in inactive list");
}
// after every call to satisfy the following should check should pass
checkSatisfied(): void {
this.cs.forEach(c=>console.assert(c.slack() >= vpsc.Solver.ZERO_UPPERBOUND, "Error: Unsatisfied constraint! "+c));
}
DEBUG */
Solver.prototype.mostViolated = function () {
var minSlack = Number.MAX_VALUE,
v = null,
l = this.inactive,
n = l.length,
deletePoint = n;
for (var i = 0; i < n; ++i) {
var c = l[i];
if (c.unsatisfiable) continue;
var slack = c.slack();
if (c.equality || slack < minSlack) {
minSlack = slack;
v = c;
deletePoint = i;
if (c.equality) break;
}
}
if (deletePoint !== n && (minSlack < Solver.ZERO_UPPERBOUND && !v.active || v.equality)) {
l[deletePoint] = l[n - 1];
l.length = n - 1;
}
return v;
};
// satisfy constraints by building block structure over violated constraints
// and moving the blocks to their desired positions
Solver.prototype.satisfy = function () {
if (this.bs == null) {
this.bs = new Blocks(this.vs);
}
/* DEBUG
console.log("satisfy: " + this.bs);
DEBUG */
this.bs.split(this.inactive);
var v = null;
while ((v = this.mostViolated()) && (v.equality || v.slack() < Solver.ZERO_UPPERBOUND && !v.active)) {
var lb = v.left.block,
rb = v.right.block;
/* DEBUG
console.log("most violated is: " + v);
this.bs.contains(lb);
this.bs.contains(rb);
DEBUG */
if (lb !== rb) {
this.bs.merge(v);
} else {
if (lb.isActiveDirectedPathBetween(v.right, v.left)) {
// cycle found!
v.unsatisfiable = true;
continue;
}
// constraint is within block, need to split first
var split = lb.splitBetween(v.left, v.right);
if (split !== null) {
this.bs.insert(split.lb);
this.bs.insert(split.rb);
this.bs.remove(lb);
this.inactive.push(split.constraint);
} else {
/* DEBUG
console.log("unsatisfiable constraint found");
DEBUG */
v.unsatisfiable = true;
continue;
}
if (v.slack() >= 0) {
/* DEBUG
console.log("violated constraint indirectly satisfied: " + v);
DEBUG */
// v was satisfied by the above split!
this.inactive.push(v);
} else {
/* DEBUG
console.log("merge after split:");
DEBUG */
this.bs.merge(v);
}
}
}
/* DEBUG
this.checkSatisfied();
DEBUG */
};
// repeatedly build and split block structure until we converge to an optimal solution
Solver.prototype.solve = function () {
this.satisfy();
var lastcost = Number.MAX_VALUE,
cost = this.bs.cost();
while (Math.abs(lastcost - cost) > 0.0001) {
this.satisfy();
lastcost = cost;
cost = this.bs.cost();
}
return cost;
};
Solver.LAGRANGIAN_TOLERANCE = -1e-4;
Solver.ZERO_UPPERBOUND = -1e-10;
return Solver;
}();
vpsc.Solver = Solver;
module.exports = vpsc;
/***/ },
/* 10 */
/***/ function(module, exports, __webpack_require__) {
'use strict';
var helper = __webpack_require__(4);
function Renderer(options) {
this.options = helper.extend({
layerGap: 60,
nodeHeight: 10,
direction: 'down'
}, options);
}
function lineTo(point) {
return 'L ' + point.join(' ');
}
function moveTo(point) {
return 'M ' + point.join(' ');
}
function curveTo(c1, c2, point2) {
return 'C ' + c1.join(' ') + ' ' + c2.join(' ') + ' ' + point2.join(' ');
}
function vCurveBetween(point1, point2) {
var midY = (point1[1] + point2[1]) / 2;
return curveTo([point1[0], midY], [point2[0], midY], point2);
}
function hCurveBetween(point1, point2) {
var midX = (point1[0] + point2[0]) / 2;
return curveTo([midX, point1[1]], [midX, point2[1]], point2);
}
Renderer.lineTo = lineTo;
Renderer.moveTo = moveTo;
Renderer.curveTo = curveTo;
Renderer.vCurveBetween = vCurveBetween;
Renderer.hCurveBetween = hCurveBetween;
Renderer.prototype.getWaypoints = function (node) {
var options = this.options;
var direction = options.direction;
var hops = node.getPathFromRoot();
var gap = options.nodeHeight + options.layerGap;
if (direction === 'left') {
return [[[0, hops[0].idealPos]]].concat(hops.map(function (hop, level) {
var xPos = gap * (level + 1) * -1;
return [[xPos + options.nodeHeight, hop.currentPos], [xPos, hop.currentPos]];
}));
} else if (direction === 'right') {
return [[[0, hops[0].idealPos]]].concat(hops.map(function (hop, level) {
var xPos = gap * (level + 1);
return [[xPos - options.nodeHeight, hop.currentPos], [xPos, hop.currentPos]];
}));
} else if (direction === 'up') {
return [[[hops[0].idealPos, 0]]].concat(hops.map(function (hop, level) {
var yPos = gap * (level + 1) * -1;
return [[hop.currentPos, yPos + options.nodeHeight], [hop.currentPos, yPos]];
}));
} else {
return [[[hops[0].idealPos, 0]]].concat(hops.map(function (hop, level) {
var yPos = gap * (level + 1);
return [[hop.currentPos, yPos - options.nodeHeight], [hop.currentPos, yPos]];
}));
}
};
Renderer.prototype.layout = function (nodes) {
var options = this.options;
var gap = options.layerGap + options.nodeHeight;
switch (options.direction) {
case 'left':
nodes.forEach(function (node) {
var pos = node.getLayerIndex() * gap + options.layerGap;
node.x = -pos - options.nodeHeight;
node.y = node.currentPos;
node.dx = options.nodeHeight;
node.dy = node.width;
});
break;
case 'right':
nodes.forEach(function (node) {
var pos = node.getLayerIndex() * gap + options.layerGap;
node.x = pos;
node.y = node.currentPos;
node.dx = options.nodeHeight;
node.dy = node.width;
});
break;
case 'up':
nodes.forEach(function (node) {
var pos = node.getLayerIndex() * gap + options.layerGap;
node.x = node.currentPos;
node.y = -pos - options.nodeHeight;
node.dx = node.width;
node.dy = options.nodeHeight;
});
break;
default:
case 'down':
nodes.forEach(function (node) {
var pos = node.getLayerIndex() * gap + options.layerGap;
node.x = node.currentPos;
node.y = pos;
node.dx = node.width;
node.dy = options.nodeHeight;
});
break;
}
return nodes;
};
Renderer.prototype.generatePath = function (node) {
var options = this.options;
var direction = options.direction;
var waypoints = this.getWaypoints(node, direction);
var steps = [moveTo(waypoints[0][0])];
if (direction === 'left' || direction === 'right') {
waypoints.reduce(function (prev, current, level) {
steps.push(hCurveBetween(prev[prev.length - 1], current[0]));
if (level < waypoints.length - 1) {
steps.push(lineTo(current[1]));
}
return current;
});
} else {
waypoints.reduce(function (prev, current, level) {
steps.push(vCurveBetween(prev[prev.length - 1], current[0]));
if (level < waypoints.length - 1) {
steps.push(lineTo(current[1]));
}
return current;
});
}
return steps.join(' ');
};
// return module
module.exports = Renderer;
/***/ }
/******/ ])
});
;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment