|
(function (global, factory) { |
|
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('d3-polygon'), require('d3-timer'), require('d3-dispatch'), require('d3-weighted-voronoi')) : |
|
typeof define === 'function' && define.amd ? define(['exports', 'd3-polygon', 'd3-timer', 'd3-dispatch', 'd3-weighted-voronoi'], factory) : |
|
(factory((global.d3 = global.d3 || {}),global.d3,global.d3,global.d3,global.d3)); |
|
}(this, function (exports,d3Polygon,d3Timer,d3Dispatch,d3WeightedVoronoi) { 'use strict'; |
|
|
|
function FlickeringMitigation() { |
|
/////// Inputs /////// |
|
this.growthChangesLength = DEFAULT_LENGTH; |
|
this.totalAvailableArea = NaN; |
|
|
|
//begin: internals |
|
this.lastAreaError = NaN; |
|
this.lastGrowth = NaN; |
|
this.growthChanges = []; |
|
this.growthChangeWeights = generateGrowthChangeWeights(this.growthChangesLength); //used to make recent changes weighter than older changes |
|
this.growthChangeWeightsSum = computeGrowthChangeWeightsSum(this.growthChangeWeights); |
|
//end: internals |
|
} |
|
|
|
const DEFAULT_LENGTH = 10; |
|
|
|
function direction(h0, h1) { |
|
return h0 >= h1 ? 1 : -1; |
|
} |
|
|
|
function generateGrowthChangeWeights(length) { |
|
const initialWeight = 3; // a magic number |
|
const weightDecrement = 1; // a magic number |
|
const minWeight = 1; |
|
|
|
let weightedCount = initialWeight; |
|
const growthChangeWeights = []; |
|
|
|
for (let i = 0; i < length; i++) { |
|
growthChangeWeights.push(weightedCount); |
|
weightedCount -= weightDecrement; |
|
if (weightedCount < minWeight) { |
|
weightedCount = minWeight; |
|
} |
|
} |
|
return growthChangeWeights; |
|
} |
|
|
|
function computeGrowthChangeWeightsSum(growthChangeWeights) { |
|
let growthChangeWeightsSum = 0; |
|
for (let i = 0; i < growthChangeWeights.length; i++) { |
|
growthChangeWeightsSum += growthChangeWeights[i]; |
|
} |
|
return growthChangeWeightsSum; |
|
} |
|
|
|
/////////////////////// |
|
///////// API ///////// |
|
/////////////////////// |
|
|
|
FlickeringMitigation.prototype.reset = function() { |
|
this.lastAreaError = NaN; |
|
this.lastGrowth = NaN; |
|
this.growthChanges = []; |
|
this.growthChangesLength = DEFAULT_LENGTH; |
|
this.growthChangeWeights = generateGrowthChangeWeights(this.growthChangesLength); |
|
this.growthChangeWeightsSum = computeGrowthChangeWeightsSum(this.growthChangeWeights); |
|
this.totalAvailableArea = NaN; |
|
|
|
return this; |
|
}; |
|
|
|
FlickeringMitigation.prototype.clear = function() { |
|
this.lastAreaError = NaN; |
|
this.lastGrowth = NaN; |
|
this.growthChanges = []; |
|
|
|
return this; |
|
}; |
|
|
|
FlickeringMitigation.prototype.length = function(_) { |
|
if (!arguments.length) { |
|
return this.growthChangesLength; |
|
} |
|
|
|
if (parseInt(_) > 0) { |
|
this.growthChangesLength = Math.floor(parseInt(_)); |
|
this.growthChangeWeights = generateGrowthChangeWeights(this.growthChangesLength); |
|
this.growthChangeWeightsSum = computeGrowthChangeWeightsSum(this.growthChangeWeights); |
|
} else { |
|
console.warn('FlickeringMitigation.length() accepts only positive integers; unable to handle ' + _); |
|
} |
|
return this; |
|
}; |
|
|
|
FlickeringMitigation.prototype.totalArea = function(_) { |
|
if (!arguments.length) { |
|
return this.totalAvailableArea; |
|
} |
|
|
|
if (parseFloat(_) > 0) { |
|
this.totalAvailableArea = parseFloat(_); |
|
} else { |
|
console.warn('FlickeringMitigation.totalArea() accepts only positive numbers; unable to handle ' + _); |
|
} |
|
return this; |
|
}; |
|
|
|
FlickeringMitigation.prototype.add = function(areaError) { |
|
let secondToLastAreaError, secondToLastGrowth; |
|
|
|
secondToLastAreaError = this.lastAreaError; |
|
this.lastAreaError = areaError; |
|
if (!isNaN(secondToLastAreaError)) { |
|
secondToLastGrowth = this.lastGrowth; |
|
this.lastGrowth = direction(this.lastAreaError, secondToLastAreaError); |
|
} |
|
if (!isNaN(secondToLastGrowth)) { |
|
this.growthChanges.unshift(this.lastGrowth != secondToLastGrowth); |
|
} |
|
|
|
if (this.growthChanges.length > this.growthChangesLength) { |
|
this.growthChanges.pop(); |
|
} |
|
return this; |
|
}; |
|
|
|
FlickeringMitigation.prototype.ratio = function() { |
|
let weightedChangeCount = 0; |
|
let ratio; |
|
|
|
if (this.growthChanges.length < this.growthChangesLength) { |
|
return 0; |
|
} |
|
if (this.lastAreaError > this.totalAvailableArea / 10) { |
|
return 0; |
|
} |
|
|
|
for (let i = 0; i < this.growthChangesLength; i++) { |
|
if (this.growthChanges[i]) { |
|
weightedChangeCount += this.growthChangeWeights[i]; |
|
} |
|
} |
|
|
|
ratio = weightedChangeCount / this.growthChangeWeightsSum; |
|
|
|
if (ratio > 0) { |
|
console.log('flickering mitigation ratio: ' + Math.floor(ratio * 1000) / 1000); |
|
} |
|
|
|
return ratio; |
|
}; |
|
|
|
function randomInitialPosition() { |
|
//begin: internals |
|
let clippingPolygon, extent, minX, maxX, minY, maxY, dx, dy; |
|
//end: internals |
|
|
|
/////////////////////// |
|
///////// API ///////// |
|
/////////////////////// |
|
|
|
function _random(d, i, arr, voronoiMapSimulation) { |
|
let shouldUpdateInternals = false; |
|
let x, y; |
|
|
|
if (clippingPolygon !== voronoiMapSimulation.clip()) { |
|
clippingPolygon = voronoiMapSimulation.clip(); |
|
extent = voronoiMapSimulation.extent(); |
|
shouldUpdateInternals = true; |
|
} |
|
|
|
if (shouldUpdateInternals) { |
|
updateInternals(); |
|
} |
|
|
|
x = minX + dx * voronoiMapSimulation.prng()(); |
|
y = minY + dy * voronoiMapSimulation.prng()(); |
|
while (!d3Polygon.polygonContains(clippingPolygon, [x, y])) { |
|
x = minX + dx * voronoiMapSimulation.prng()(); |
|
y = minY + dy * voronoiMapSimulation.prng()(); |
|
} |
|
return [x, y]; |
|
} |
|
|
|
/////////////////////// |
|
/////// Private /////// |
|
/////////////////////// |
|
|
|
function updateInternals() { |
|
minX = extent[0][0]; |
|
maxX = extent[1][0]; |
|
minY = extent[0][1]; |
|
maxY = extent[1][1]; |
|
dx = maxX - minX; |
|
dy = maxY - minY; |
|
} |
|
|
|
return _random; |
|
} |
|
|
|
function pie() { |
|
//begin: internals |
|
let startAngle = 0; |
|
let clippingPolygon, dataArray, dataArrayLength, clippingPolygonCentroid, halfIncircleRadius, angleBetweenData; |
|
//end: internals |
|
|
|
/////////////////////// |
|
///////// API ///////// |
|
/////////////////////// |
|
|
|
function _pie(d, i, arr, voronoiMapSimulation) { |
|
let shouldUpdateInternals = false; |
|
|
|
if (clippingPolygon !== voronoiMapSimulation.clip()) { |
|
clippingPolygon = voronoiMapSimulation.clip(); |
|
shouldUpdateInternals |= true; |
|
} |
|
if (dataArray !== arr) { |
|
dataArray = arr; |
|
shouldUpdateInternals |= true; |
|
} |
|
|
|
if (shouldUpdateInternals) { |
|
updateInternals(); |
|
} |
|
|
|
// add some randomness to prevent colinear/cocircular points |
|
// substract -0.5 so that the average jitter is still zero |
|
return [ |
|
clippingPolygonCentroid[0] + |
|
Math.cos(startAngle + i * angleBetweenData) * halfIncircleRadius + |
|
(voronoiMapSimulation.prng()() - 0.5) * 1e-3, |
|
clippingPolygonCentroid[1] + |
|
Math.sin(startAngle + i * angleBetweenData) * halfIncircleRadius + |
|
(voronoiMapSimulation.prng()() - 0.5) * 1e-3 |
|
]; |
|
} |
|
|
|
_pie.startAngle = function(_) { |
|
if (!arguments.length) { |
|
return startAngle; |
|
} |
|
|
|
startAngle = _; |
|
return _pie; |
|
}; |
|
|
|
/////////////////////// |
|
/////// Private /////// |
|
/////////////////////// |
|
|
|
function updateInternals() { |
|
clippingPolygonCentroid = d3Polygon.polygonCentroid(clippingPolygon); |
|
halfIncircleRadius = computeMinDistFromEdges(clippingPolygonCentroid, clippingPolygon) / 2; |
|
dataArrayLength = dataArray.length; |
|
angleBetweenData = (2 * Math.PI) / dataArrayLength; |
|
} |
|
|
|
function computeMinDistFromEdges(vertex, clippingPolygon) { |
|
let minDistFromEdges = Infinity; |
|
let edgeIndex = 0, |
|
edgeVertex0 = clippingPolygon[clippingPolygon.length - 1], |
|
edgeVertex1 = clippingPolygon[edgeIndex]; |
|
let distFromCurrentEdge; |
|
|
|
while (edgeIndex < clippingPolygon.length) { |
|
distFromCurrentEdge = vDistance(vertex, edgeVertex0, edgeVertex1); |
|
if (distFromCurrentEdge < minDistFromEdges) { |
|
minDistFromEdges = distFromCurrentEdge; |
|
} |
|
edgeIndex++; |
|
edgeVertex0 = edgeVertex1; |
|
edgeVertex1 = clippingPolygon[edgeIndex]; |
|
} |
|
|
|
return minDistFromEdges; |
|
} |
|
|
|
//from https://stackoverflow.com/questions/849211/shortest-distance-between-a-point-and-a-line-segment |
|
function vDistance(vertex, edgeVertex0, edgeVertex1) { |
|
const x = vertex[0], |
|
y = vertex[1], |
|
x1 = edgeVertex0[0], |
|
y1 = edgeVertex0[1], |
|
x2 = edgeVertex1[0], |
|
y2 = edgeVertex1[1]; |
|
const A = x - x1, |
|
B = y - y1, |
|
C = x2 - x1, |
|
D = y2 - y1; |
|
const dot = A * C + B * D; |
|
const len_sq = C * C + D * D; |
|
let param = -1; |
|
|
|
if (len_sq != 0) |
|
//in case of 0 length line |
|
param = dot / len_sq; |
|
|
|
let xx, yy; |
|
|
|
if (param < 0) { |
|
// this should not arise as clippingpolygon is convex |
|
xx = x1; |
|
yy = y1; |
|
} else if (param > 1) { |
|
// this should not arise as clippingpolygon is convex |
|
xx = x2; |
|
yy = y2; |
|
} else { |
|
xx = x1 + param * C; |
|
yy = y1 + param * D; |
|
} |
|
|
|
const dx = x - xx; |
|
const dy = y - yy; |
|
return Math.sqrt(dx * dx + dy * dy); |
|
} |
|
|
|
return _pie; |
|
} |
|
|
|
function halfAverageAreaInitialWeight() { |
|
//begin: internals |
|
let clippingPolygon, dataArray, siteCount, totalArea, halfAverageArea; |
|
//end: internals |
|
|
|
/////////////////////// |
|
///////// API ///////// |
|
/////////////////////// |
|
function _halfAverageArea(d, i, arr, voronoiMapSimulation) { |
|
let shouldUpdateInternals = false; |
|
if (clippingPolygon !== voronoiMapSimulation.clip()) { |
|
clippingPolygon = voronoiMapSimulation.clip(); |
|
shouldUpdateInternals |= true; |
|
} |
|
if (dataArray !== arr) { |
|
dataArray = arr; |
|
shouldUpdateInternals |= true; |
|
} |
|
|
|
if (shouldUpdateInternals) { |
|
updateInternals(); |
|
} |
|
|
|
return halfAverageArea; |
|
} |
|
|
|
/////////////////////// |
|
/////// Private /////// |
|
/////////////////////// |
|
|
|
function updateInternals() { |
|
siteCount = dataArray.length; |
|
totalArea = d3Polygon.polygonArea(clippingPolygon); |
|
halfAverageArea = totalArea / siteCount / 2; // half of the average area of the the clipping polygon |
|
} |
|
|
|
return _halfAverageArea; |
|
} |
|
|
|
function voronoiMapSimulation(data) { |
|
//begin: constants |
|
const DEFAULT_CONVERGENCE_RATIO = 0.01; |
|
const DEFAULT_MAX_ITERATION_COUNT = 50; |
|
const DEFAULT_MIN_WEIGHT_RATIO = 0.01; |
|
const DEFAULT_PRNG = Math.random; |
|
const DEFAULT_INITIAL_POSITION = randomInitialPosition(); |
|
const DEFAULT_INITIAL_WEIGHT = halfAverageAreaInitialWeight(); |
|
const RANDOM_INITIAL_POSITION = randomInitialPosition(); |
|
const epsilon = 1; |
|
//end: constants |
|
|
|
/////// Inputs /////// |
|
let weight = d => d.weight; // accessor to the weight |
|
let convergenceRatio = DEFAULT_CONVERGENCE_RATIO; // targeted allowed error ratio; default 0.01 stops computation when cell areas error <= 1% clipping polygon's area |
|
let maxIterationCount = DEFAULT_MAX_ITERATION_COUNT; // maximum allowed iteration; stops computation even if convergence is not reached; use a large amount for a sole converge-based computation stop |
|
let minWeightRatio = DEFAULT_MIN_WEIGHT_RATIO; // used to compute the minimum allowed weight; default 0.01 means 1% of max weight; handle near-zero weights, and leaves enought space for cell hovering |
|
let prng = DEFAULT_PRNG; // pseudorandom number generator |
|
let initialPosition = DEFAULT_INITIAL_POSITION; // accessor to the initial position; defaults to a random position inside the clipping polygon |
|
let initialWeight = DEFAULT_INITIAL_WEIGHT; // accessor to the initial weight; defaults to the average area of the clipping polygon |
|
|
|
//begin: internals |
|
const weightedVoronoi = d3WeightedVoronoi.weightedVoronoi(), |
|
flickeringMitigation = new FlickeringMitigation(); |
|
let shouldInitialize = true, // should initialize due to changes via APIs |
|
siteCount, // number of sites |
|
totalArea, // area of the clipping polygon |
|
areaErrorTreshold, // targeted allowed area error (= totalArea * convergenceRatio); below this treshold, map is considered obtained and computation stops |
|
iterationCount, // current iteration |
|
polygons, // current computed polygons |
|
areaError, // current area error |
|
converged, // true if (areaError < areaErrorTreshold) |
|
ended; // stores if computation is ended, either if computation has converged or if it has reached the maximum allowed iteration |
|
//end: internals |
|
//being: internals/simulation |
|
let simulation; |
|
const stepper = d3Timer.timer(step), |
|
event = d3Dispatch.dispatch('tick', 'end'); |
|
//end: internals/simulation |
|
|
|
//begin: algorithm conf. |
|
const handleOverweightedVariant = 1; // this option still exists 'cause for further experiments |
|
let handleOverweighted; |
|
//end: algorithm conf. |
|
|
|
//begin: utils |
|
function sqr(d) { |
|
return Math.pow(d, 2); |
|
} |
|
|
|
function squaredDistance(s0, s1) { |
|
return sqr(s1.x - s0.x) + sqr(s1.y - s0.y); |
|
} |
|
//end: utils |
|
|
|
/////////////////////// |
|
///////// API ///////// |
|
/////////////////////// |
|
|
|
simulation = { |
|
tick: tick, |
|
|
|
restart: function() { |
|
stepper.restart(step); |
|
return simulation; |
|
}, |
|
|
|
stop: function() { |
|
stepper.stop(); |
|
return simulation; |
|
}, |
|
|
|
weight: function(_) { |
|
if (!arguments.length) { |
|
return weight; |
|
} |
|
|
|
weight = _; |
|
shouldInitialize = true; |
|
return simulation; |
|
}, |
|
|
|
convergenceRatio: function(_) { |
|
if (!arguments.length) { |
|
return convergenceRatio; |
|
} |
|
|
|
convergenceRatio = _; |
|
shouldInitialize = true; |
|
return simulation; |
|
}, |
|
|
|
maxIterationCount: function(_) { |
|
if (!arguments.length) { |
|
return maxIterationCount; |
|
} |
|
|
|
maxIterationCount = _; |
|
return simulation; |
|
}, |
|
|
|
minWeightRatio: function(_) { |
|
if (!arguments.length) { |
|
return minWeightRatio; |
|
} |
|
|
|
minWeightRatio = _; |
|
shouldInitialize = true; |
|
return simulation; |
|
}, |
|
|
|
clip: function(_) { |
|
if (!arguments.length) { |
|
return weightedVoronoi.clip(); |
|
} |
|
|
|
weightedVoronoi.clip(_); |
|
shouldInitialize = true; |
|
return simulation; |
|
}, |
|
|
|
extent: function(_) { |
|
if (!arguments.length) { |
|
return weightedVoronoi.extent(); |
|
} |
|
|
|
weightedVoronoi.extent(_); |
|
shouldInitialize = true; |
|
return simulation; |
|
}, |
|
|
|
size: function(_) { |
|
if (!arguments.length) { |
|
return weightedVoronoi.size(); |
|
} |
|
|
|
weightedVoronoi.size(_); |
|
shouldInitialize = true; |
|
return simulation; |
|
}, |
|
|
|
prng: function(_) { |
|
if (!arguments.length) { |
|
return prng; |
|
} |
|
|
|
prng = _; |
|
shouldInitialize = true; |
|
return simulation; |
|
}, |
|
|
|
initialPosition: function(_) { |
|
if (!arguments.length) { |
|
return initialPosition; |
|
} |
|
|
|
initialPosition = _; |
|
shouldInitialize = true; |
|
return simulation; |
|
}, |
|
|
|
initialWeight: function(_) { |
|
if (!arguments.length) { |
|
return initialWeight; |
|
} |
|
|
|
initialWeight = _; |
|
shouldInitialize = true; |
|
return simulation; |
|
}, |
|
|
|
state: function() { |
|
if (shouldInitialize) { |
|
initializeSimulation(); |
|
} |
|
return { |
|
ended: ended, |
|
iterationCount: iterationCount, |
|
convergenceRatio: areaError / totalArea, |
|
polygons: polygons |
|
}; |
|
}, |
|
|
|
on: function(name, _) { |
|
if (arguments.length === 1) { |
|
return event.on(name); |
|
} |
|
|
|
event.on(name, _); |
|
return simulation; |
|
} |
|
}; |
|
|
|
/////////////////////// |
|
/////// Private /////// |
|
/////////////////////// |
|
|
|
//begin: simulation's main loop |
|
function step() { |
|
tick(); |
|
event.call('tick', simulation); |
|
if (ended) { |
|
stepper.stop(); |
|
event.call('end', simulation); |
|
} |
|
} |
|
//end: simulation's main loop |
|
|
|
//begin: algorithm used at each iteration |
|
function tick() { |
|
if (!ended) { |
|
if (shouldInitialize) { |
|
initializeSimulation(); |
|
} |
|
polygons = adapt(polygons, flickeringMitigation.ratio()); |
|
iterationCount++; |
|
areaError = computeAreaError(polygons); |
|
flickeringMitigation.add(areaError); |
|
converged = areaError < areaErrorTreshold; |
|
ended = converged || iterationCount >= maxIterationCount; |
|
// console.log("error %: "+Math.round(areaError*100*1000/totalArea)/1000); |
|
} |
|
} |
|
//end: algorithm used at each iteration |
|
|
|
function initializeSimulation() { |
|
//begin: handle algorithm's variants |
|
setHandleOverweighted(); |
|
//end: handle algorithm's variants |
|
|
|
siteCount = data.length; |
|
totalArea = Math.abs(d3Polygon.polygonArea(weightedVoronoi.clip())); |
|
areaErrorTreshold = convergenceRatio * totalArea; |
|
flickeringMitigation.clear().totalArea(totalArea); |
|
|
|
iterationCount = 0; |
|
converged = false; |
|
polygons = initialize(data, simulation); |
|
ended = false; |
|
shouldInitialize = false; |
|
} |
|
|
|
function initialize(data, simulation) { |
|
const maxWeight = data.reduce((max, d) => { |
|
return Math.max(max, weight(d)); |
|
}, -Infinity), |
|
minAllowedWeight = maxWeight * minWeightRatio; |
|
let weights, mapPoints; |
|
|
|
//begin: extract weights |
|
weights = data.map((d, i, arr) => { |
|
return { |
|
index: i, |
|
weight: Math.max(weight(d), minAllowedWeight), |
|
initialPosition: initialPosition(d, i, arr, simulation), |
|
initialWeight: initialWeight(d, i, arr, simulation), |
|
originalData: d |
|
}; |
|
}); |
|
//end: extract weights |
|
|
|
// create map-related points |
|
// (with targetedArea, initial position and initialWeight) |
|
mapPoints = createMapPoints(weights, simulation); |
|
return weightedVoronoi(mapPoints); |
|
} |
|
|
|
function createMapPoints(basePoints, simulation) { |
|
const totalWeight = basePoints.reduce((acc, bp) => { |
|
return (acc += bp.weight); |
|
}, 0); |
|
let initialPosition; |
|
|
|
return basePoints.map((bp, i, bps) => { |
|
initialPosition = bp.initialPosition; |
|
|
|
if (!d3Polygon.polygonContains(weightedVoronoi.clip(), initialPosition)) { |
|
initialPosition = DEFAULT_INITIAL_POSITION(bp, i, bps, simulation); |
|
} |
|
|
|
return { |
|
index: bp.index, |
|
targetedArea: (totalArea * bp.weight) / totalWeight, |
|
data: bp, |
|
x: initialPosition[0], |
|
y: initialPosition[1], |
|
weight: bp.initialWeight // ArlindNocaj/Voronoi-Treemap-Library uses an epsilonesque initial weight; using heavier initial weights allows faster weight adjustements, hence faster stabilization |
|
}; |
|
}); |
|
} |
|
|
|
function adapt(polygons, flickeringMitigationRatio) { |
|
let adaptedMapPoints; |
|
|
|
adaptPositions(polygons, flickeringMitigationRatio); |
|
adaptedMapPoints = polygons.map(p => { |
|
return p.site.originalObject; |
|
}); |
|
polygons = weightedVoronoi(adaptedMapPoints); |
|
if (polygons.length < siteCount) { |
|
console.log('at least 1 site has no area, which is not supposed to arise'); |
|
debugger; |
|
} |
|
|
|
adaptWeights(polygons, flickeringMitigationRatio); |
|
adaptedMapPoints = polygons.map(p => { |
|
return p.site.originalObject; |
|
}); |
|
polygons = weightedVoronoi(adaptedMapPoints); |
|
if (polygons.length < siteCount) { |
|
console.log('at least 1 site has no area, which is not supposed to arise'); |
|
debugger; |
|
} |
|
|
|
return polygons; |
|
} |
|
|
|
function adaptPositions(polygons, flickeringMitigationRatio) { |
|
const newMapPoints = [], |
|
flickeringInfluence = 0.5; |
|
let flickeringMitigation, d, polygon, mapPoint, centroid, dx, dy; |
|
|
|
flickeringMitigation = flickeringInfluence * flickeringMitigationRatio; |
|
d = 1 - flickeringMitigation; // in [0.5, 1] |
|
for (let i = 0; i < siteCount; i++) { |
|
polygon = polygons[i]; |
|
mapPoint = polygon.site.originalObject; |
|
centroid = d3Polygon.polygonCentroid(polygon); |
|
|
|
dx = centroid[0] - mapPoint.x; |
|
dy = centroid[1] - mapPoint.y; |
|
|
|
//begin: handle excessive change; |
|
dx *= d; |
|
dy *= d; |
|
//end: handle excessive change; |
|
|
|
mapPoint.x += dx; |
|
mapPoint.y += dy; |
|
|
|
newMapPoints.push(mapPoint); |
|
} |
|
|
|
handleOverweighted(newMapPoints); |
|
} |
|
|
|
function adaptWeights(polygons, flickeringMitigationRatio) { |
|
const newMapPoints = [], |
|
flickeringInfluence = 0.1; |
|
let flickeringMitigation, polygon, mapPoint, currentArea, adaptRatio, adaptedWeight; |
|
|
|
flickeringMitigation = flickeringInfluence * flickeringMitigationRatio; |
|
for (let i = 0; i < siteCount; i++) { |
|
polygon = polygons[i]; |
|
mapPoint = polygon.site.originalObject; |
|
currentArea = d3Polygon.polygonArea(polygon); |
|
adaptRatio = mapPoint.targetedArea / currentArea; |
|
|
|
//begin: handle excessive change; |
|
adaptRatio = Math.max(adaptRatio, 1 - flickeringInfluence + flickeringMitigation); // in [(1-flickeringInfluence), 1] |
|
adaptRatio = Math.min(adaptRatio, 1 + flickeringInfluence - flickeringMitigation); // in [1, (1+flickeringInfluence)] |
|
//end: handle excessive change; |
|
|
|
adaptedWeight = mapPoint.weight * adaptRatio; |
|
adaptedWeight = Math.max(adaptedWeight, epsilon); |
|
|
|
mapPoint.weight = adaptedWeight; |
|
|
|
newMapPoints.push(mapPoint); |
|
} |
|
|
|
handleOverweighted(newMapPoints); |
|
} |
|
|
|
// heuristics: lower heavy weights |
|
function handleOverweighted0(mapPoints) { |
|
let fixCount = 0; |
|
let fixApplied, tpi, tpj, weightest, lightest, sqrD, adaptedWeight; |
|
do { |
|
fixApplied = false; |
|
for (let i = 0; i < siteCount; i++) { |
|
tpi = mapPoints[i]; |
|
for (let j = i + 1; j < siteCount; j++) { |
|
tpj = mapPoints[j]; |
|
if (tpi.weight > tpj.weight) { |
|
weightest = tpi; |
|
lightest = tpj; |
|
} else { |
|
weightest = tpj; |
|
lightest = tpi; |
|
} |
|
sqrD = squaredDistance(tpi, tpj); |
|
if (sqrD < weightest.weight - lightest.weight) { |
|
// adaptedWeight = sqrD - epsilon; // as in ArlindNocaj/Voronoi-Treemap-Library |
|
// adaptedWeight = sqrD + lightest.weight - epsilon; // works, but below heuristics performs better (less flickering) |
|
adaptedWeight = sqrD + lightest.weight / 2; |
|
adaptedWeight = Math.max(adaptedWeight, epsilon); |
|
weightest.weight = adaptedWeight; |
|
fixApplied = true; |
|
fixCount++; |
|
break; |
|
} |
|
} |
|
if (fixApplied) { |
|
break; |
|
} |
|
} |
|
} while (fixApplied); |
|
|
|
/* |
|
if (fixCount>0) { |
|
console.log("# fix: "+fixCount); |
|
} |
|
*/ |
|
} |
|
|
|
// heuristics: increase light weights |
|
function handleOverweighted1(mapPoints) { |
|
const fixCount = 0; |
|
let fixApplied, tpi, tpj, weightest, lightest, sqrD, overweight; |
|
do { |
|
fixApplied = false; |
|
for (let i = 0; i < siteCount; i++) { |
|
tpi = mapPoints[i]; |
|
for (let j = i + 1; j < siteCount; j++) { |
|
tpj = mapPoints[j]; |
|
if (tpi.weight > tpj.weight) { |
|
weightest = tpi; |
|
lightest = tpj; |
|
} else { |
|
weightest = tpj; |
|
lightest = tpi; |
|
} |
|
sqrD = squaredDistance(tpi, tpj); |
|
if (sqrD < weightest.weight - lightest.weight) { |
|
overweight = weightest.weight - lightest.weight - sqrD; |
|
lightest.weight += overweight + epsilon; |
|
fixApplied = true; |
|
fixCount++; |
|
break; |
|
} |
|
} |
|
if (fixApplied) { |
|
break; |
|
} |
|
} |
|
} while (fixApplied); |
|
|
|
/* |
|
if (fixCount>0) { |
|
console.log("# fix: "+fixCount); |
|
} |
|
*/ |
|
} |
|
|
|
function computeAreaError(polygons) { |
|
//convergence based on summation of all sites current areas |
|
let areaErrorSum = 0; |
|
let polygon, mapPoint, currentArea; |
|
for (let i = 0; i < siteCount; i++) { |
|
polygon = polygons[i]; |
|
mapPoint = polygon.site.originalObject; |
|
currentArea = d3Polygon.polygonArea(polygon); |
|
areaErrorSum += Math.abs(mapPoint.targetedArea - currentArea); |
|
} |
|
return areaErrorSum; |
|
} |
|
|
|
function setHandleOverweighted() { |
|
switch (handleOverweightedVariant) { |
|
case 0: |
|
handleOverweighted = handleOverweighted0; |
|
break; |
|
case 1: |
|
handleOverweighted = handleOverweighted1; |
|
break; |
|
default: |
|
console.log("Variant of 'handleOverweighted' is unknown"); |
|
} |
|
} |
|
|
|
return simulation; |
|
} |
|
|
|
exports.voronoiMapSimulation = voronoiMapSimulation; |
|
exports.voronoiMapInitialPositionRandom = randomInitialPosition; |
|
exports.voronoiMapInitialPositionPie = pie; |
|
|
|
Object.defineProperty(exports, '__esModule', { value: true }); |
|
|
|
})); |