Skip to content

Instantly share code, notes, and snippets.

@veltman
Last active November 8, 2019 08:57
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save veltman/4d53741c724ad9261fe238045acd98c1 to your computer and use it in GitHub Desktop.
Save veltman/4d53741c724ad9261fe238045acd98c1 to your computer and use it in GitHub Desktop.
Stacked area label placement
<!DOCTYPE html>
<meta charset="utf-8">
<style>
text {
font-family: sans-serif;
font-size: 14px;
fill: #222;
}
.axis line, .axis path {
stroke: #222;
}
.area text {
font-size: 18px;
text-anchor: middle;
}
.hidden {
display: none;
}
</style>
<svg width="960" height="500"></svg>
<script src="//d3js.org/d3.v4.min.js"></script>
<script src="polylabel.min.js"></script>
<script>
var margin = { top: 20, right: 20, bottom: 30, left: 50 },
width = 960 - margin.left - margin.right,
height = 500 - margin.top - margin.bottom,
random = d3.randomNormal(0, 3),
turtles = ["Leonardo", "Donatello", "Raphael", "Michelangelo"],
colors = ["#ef9a9a", "#9fa8da", "#ffe082", "#80cbc4"];
var svg = d3.select("svg").append("g")
.attr("transform", "translate(" + margin.left + " " + margin.top + ")");
var x = d3.scaleLinear().range([0, width]),
y = d3.scaleLinear().range([height, 0]);
var series = svg.selectAll(".area")
.data(turtles)
.enter()
.append("g")
.attr("class", "area");
series.append("path")
.attr("fill", (d, i) => colors[i]);
var xg = svg.append("g")
.attr("class", "axis x")
.attr("transform", "translate(0 " + height + ")");
var yg = svg.append("g")
.attr("class", "axis y");
series.append("text")
.attr("dy", 5)
.text(d => d);
var stack = d3.stack().keys(turtles);
var line = d3.line()
.curve(d3.curveMonotoneX);
randomize();
function randomize() {
var data = [];
for (var i = 0; i < 40; i++) {
data[i] = {};
turtles.forEach(function(turtle){
data[i][turtle] = Math.max(0, random() + (i ? data[i - 1][turtle] : 20));
});
}
var stacked = stack(data);
x.domain([0, data.length - 1]);
y.domain([0, d3.max(stacked[stacked.length - 1].map(d => d[1]))]);
series.data(getPositions(stacked))
.select("path")
.attr("d", d => d.path);
series.select("text")
.classed("hidden", isHidden)
.attr("x", d => d.label[0])
.attr("y", d => d.label[1]);
xg.call(d3.axisBottom(x).tickSizeOuter(0));
yg.call(d3.axisLeft(y).tickSizeOuter(0));
setTimeout(randomize, 750);
}
function getPositions(stacked) {
return stacked.map(function(area){
var top = area.map((f, j) => [x(j), y(f[1])]),
bottom = area.map((f, j) => [x(j), y(f[0])]).reverse();
// Exclude the left- and right-most points from the polygon to avoid the edges a bit
return {
area: area,
label: polylabel([top.slice(1, area.length - 1).concat(bottom.slice(1, area.length - 1))]),
path: line(top) + line(bottom).replace("M", "L") + "Z"
};
});
}
// Does label fit in its assigned position?
function isHidden(d) {
var bbox = this.getBBox(),
labelStart = d.label[0] - bbox.width / 2,
labelEnd = d.label[0] + bbox.width / 2,
startIndex,
endIndex;
// Overlaps the left or right edge
if (labelEnd > width || labelStart < 0) {
return true;
}
// left x value
startIndex = Math.max(0, Math.floor(x.invert(labelStart)));
// right x value
endIndex = Math.min(d.area.length - 1, Math.ceil(x.invert(labelEnd)));
for (var i = startIndex; i <= endIndex; i++) {
// Would intersect the bottom
if (y(d.area[i][0]) < d.label[1] + 5 + bbox.height / 2) {
return true;
}
// Would intersect the top
if (y(d.area[i][1]) > d.label[1] + 5 - bbox.height / 2) {
return true;
}
}
return false;
}
</script>
(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.polylabel=f()}})(function(){var define,module,exports;return function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s}({1:[function(require,module,exports){"use strict";var Queue=require("tinyqueue");module.exports=polylabel;module.exports.default=polylabel;function polylabel(polygon,precision,debug){precision=precision||1;var minX,minY,maxX,maxY;for(var i=0;i<polygon[0].length;i++){var p=polygon[0][i];if(!i||p[0]<minX)minX=p[0];if(!i||p[1]<minY)minY=p[1];if(!i||p[0]>maxX)maxX=p[0];if(!i||p[1]>maxY)maxY=p[1]}var width=maxX-minX;var height=maxY-minY;var cellSize=Math.min(width,height);var h=cellSize/2;var cellQueue=new Queue(null,compareMax);for(var x=minX;x<maxX;x+=cellSize){for(var y=minY;y<maxY;y+=cellSize){cellQueue.push(new Cell(x+h,y+h,h,polygon))}}var bestCell=getCentroidCell(polygon);var bboxCell=new Cell(minX+width/2,minY+height/2,0,polygon);if(bboxCell.d>bestCell.d)bestCell=bboxCell;var numProbes=cellQueue.length;while(cellQueue.length){var cell=cellQueue.pop();if(cell.d>bestCell.d){bestCell=cell;if(debug)console.log("found best %d after %d probes",Math.round(1e4*cell.d)/1e4,numProbes)}if(cell.max-bestCell.d<=precision)continue;h=cell.h/2;cellQueue.push(new Cell(cell.x-h,cell.y-h,h,polygon));cellQueue.push(new Cell(cell.x+h,cell.y-h,h,polygon));cellQueue.push(new Cell(cell.x-h,cell.y+h,h,polygon));cellQueue.push(new Cell(cell.x+h,cell.y+h,h,polygon));numProbes+=4}if(debug){console.log("num probes: "+numProbes);console.log("best distance: "+bestCell.d)}return[bestCell.x,bestCell.y]}function compareMax(a,b){return b.max-a.max}function Cell(x,y,h,polygon){this.x=x;this.y=y;this.h=h;this.d=pointToPolygonDist(x,y,polygon);this.max=this.d+this.h*Math.SQRT2}function pointToPolygonDist(x,y,polygon){var inside=false;var minDistSq=Infinity;for(var k=0;k<polygon.length;k++){var ring=polygon[k];for(var i=0,len=ring.length,j=len-1;i<len;j=i++){var a=ring[i];var b=ring[j];if(a[1]>y!==b[1]>y&&x<(b[0]-a[0])*(y-a[1])/(b[1]-a[1])+a[0])inside=!inside;minDistSq=Math.min(minDistSq,getSegDistSq(x,y,a,b))}}return(inside?1:-1)*Math.sqrt(minDistSq)}function getCentroidCell(polygon){var area=0;var x=0;var y=0;var points=polygon[0];for(var i=0,len=points.length,j=len-1;i<len;j=i++){var a=points[i];var b=points[j];var f=a[0]*b[1]-b[0]*a[1];x+=(a[0]+b[0])*f;y+=(a[1]+b[1])*f;area+=f*3}return new Cell(x/area,y/area,0,polygon)}function getSegDistSq(px,py,a,b){var x=a[0];var y=a[1];var dx=b[0]-x;var dy=b[1]-y;if(dx!==0||dy!==0){var t=((px-x)*dx+(py-y)*dy)/(dx*dx+dy*dy);if(t>1){x=b[0];y=b[1]}else if(t>0){x+=dx*t;y+=dy*t}}dx=px-x;dy=py-y;return dx*dx+dy*dy}},{tinyqueue:2}],2:[function(require,module,exports){"use strict";module.exports=TinyQueue;function TinyQueue(data,compare){if(!(this instanceof TinyQueue))return new TinyQueue(data,compare);this.data=data||[];this.length=this.data.length;this.compare=compare||defaultCompare;if(data)for(var i=Math.floor(this.length/2);i>=0;i--)this._down(i)}function defaultCompare(a,b){return a<b?-1:a>b?1:0}TinyQueue.prototype={push:function(item){this.data.push(item);this.length++;this._up(this.length-1)},pop:function(){var top=this.data[0];this.data[0]=this.data[this.length-1];this.length--;this.data.pop();this._down(0);return top},peek:function(){return this.data[0]},_up:function(pos){var data=this.data,compare=this.compare;while(pos>0){var parent=Math.floor((pos-1)/2);if(compare(data[pos],data[parent])<0){swap(data,parent,pos);pos=parent}else break}},_down:function(pos){var data=this.data,compare=this.compare,len=this.length;while(true){var left=2*pos+1,right=left+1,min=pos;if(left<len&&compare(data[left],data[min])<0)min=left;if(right<len&&compare(data[right],data[min])<0)min=right;if(min===pos)return;swap(data,min,pos);pos=min}}};function swap(data,i,j){var tmp=data[i];data[i]=data[j];data[j]=tmp}},{}]},{},[1])(1)});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment