I have a graph of lines paths .
Actually when I zoom it applies a transform attribute.
I would like to make a semantic zoom by zooming only on the x-axis.
Here's the code for zooming
private manageZoom(svgs: AllSvg, allAxis: AllAxis, dimension: Dimension): D3ZoomBehavior {
const zoom: D3ZoomBehavior = d3
.zoom()
.scaleExtent([1, 40])
.translateExtent([
[0, 0],
[dimension.width, dimension.height]
])
.on('zoom', zoomed.bind(null, allAxis));
svgs.svgContainer.call(zoom);
return zoom;
function zoomed({ xAxis, xAxisBottom, yAxis }: AllAxis, { transform }: any) {
svgs.sillons.attr('transform', transform);
xAxisBottom.axisContainer.call(xAxisBottom.axis.scale(transform.rescaleX(xAxisBottom.scale)) as any);
}
}
the sillons object is an array of paths + text + circles
I would that the lines get re-drawed in the right position as the x-axis get larger, but not zoom sillons on y-axis.
I have checked many posts but can't repoduce them to solve my issue. for example
When you set up something along the lines of
svg.call(
d3.zoom()
.on("zoom", zoom)
)
the zoom function can be just about anything that you want. The first argument of zoom is the zoom event itself. Let's denote it by evt. Then
evt.transform.k tells you the scale factor,
evt.transform.x tells you the horizontal translation, and
evt.transform.y tells you the vertical translation.
You don't have to use all of those, though. Rather, you can redraw your image however you want.
Here's a slightly cute example that rescales the image only horizontally.
let w = 500;
let h = 100;
let svg = d3
.select("#container")
.append("svg")
.attr("width", w)
.attr("height", h)
.style("border", "solid 1px black");
let n = 500;
let pts0 = d3.range(n).map((_) => [d3.randomNormal(w / 2, w / 20)(), 0]);
let pts1 = pts0.map((pt) => [w - pt[0], h]);
let g = svg.append("g");
let link_group = g.append("g");
link_group
.selectAll("path")
.data(d3.range(n))
.join("path")
.attr("d", (i) => d3.linkVertical()({ source: pts0[i], target: pts1[i] }))
.attr("fill", "none")
.attr("stroke", "#000")
.attr("stroke-opacity", 0.1)
.attr("stroke-width", 1.5);
let all_pts = pts0.concat(pts1);
let circle_group = g.append("g");
let circles = circle_group
.attr("fill", "black")
.attr("fill-opacity", 0.2)
.selectAll("circle")
.data(all_pts)
.join("circle")
.attr("cx", (d) => d[0])
.attr("cy", (d) => d[1])
.attr("data-x", (d) => d[0])
.attr("r", 4);
svg.call(
d3
.zoom()
.scaleExtent([1 / 4, 20])
.duration(500)
.on("zoom", function (evt) {
let k = evt.transform.k;
link_group.selectAll("path").attr("d", function (i) {
let x00 = pts0[i][0];
let x01 = k * (x00 - w / 2) + w / 2;
let x10 = pts1[i][0];
let x11 = k * (x10 - w / 2) + w / 2;
return d3.linkVertical()({ source: [x01, 0], target: [x11, h] });
});
circle_group
.selectAll("circle")
.nodes()
.forEach(function (c) {
let x0 = c.getAttribute("data-x");
let k = evt.transform.k;
let x1 = k * (x0 - w / 2) + w / 2;
c.setAttribute("cx", x1);
});
})
);
<script src="https://d3js.org/d3.v7.min.js"></script>
<div id="container"></div>
I worked in the d3 library and created nested wheels. I have no idea how to add texts into the wheel, spirally from inside out. The number starting point doesn't matter, and numbers must spirally outwards according the previous position.
Codes
let allAxis = (data.map(function(i, j) {
return i.name
})),
total = allAxis.length,
radius = Math.min(options.width / 2, options.height / 2),
angleSlice = Math.PI * 2 / total,
Format = d3.format('');
let rScale = d3.scale.linear()
.domain([0, options.circles.maxValue])
.range([50, radius]);
let svg = d3.select("body").append("svg")
.attr("width", options.width + options.margins.left + options.margins.right)
.attr("height", options.height + options.margins.top + options.margins.bottom);
let g = svg.append("g")
.attr("transform", "translate(" + (options.width / 2 + options.margins.left) + "," + (options.height / 2 + options.margins.top) + ")");
let axisGrid = g.append("g")
.attr("class", "axisWraper");
let axis = axisGrid.selectAll(".axis")
.data(allAxis)
.enter()
.append("g")
.attr("class", "axis")
//append them lines
axis.append("line")
.attr("x1", 0)
.attr("y1", 0)
.attr("x2", function(d, i) {
let tempX2 = radius * Math.cos(angleSlice * i - Math.PI / 2);
return tempX2;
})
.attr("y2", function(d, i) {
let tempY = radius * Math.sin(angleSlice * i - Math.PI / 2);
return tempY;
})
.attr("class", "line")
.attr("stroke", "black")
.attr("fill", "none");
//Draw background circles
axisGrid.selectAll(".levels")
.data([12,11,10,9,8,7,6, 5, 4, 3, 2, 1])
.enter()
.append("circle")
.attr("class", function(d, i) {
return `gridCircle-${d}`
})
.attr("r", function(d, i) {
return parseInt(radius / options.circles.levels * d, 10);
})
.attr("stroke", "black")
.attr("fill-opacity", function(d, i) {
return options.circles.opacity;
});
axisGrid.select(".gridCircle-1").attr("fill-opacity", 1);
axisGrid.select(".gridCircle-2").attr("fill-opacity", 1);
Expected Result
Updated #1 (with PointRadial)
Here's the fiddle: http://jsfiddle.net/arcanabliss/8yjacdoz/73/
You need pointRadial:
Returns the point [x, y] for the given angle in radians, with 0 at -y
(12 o’clock) and positive angles proceeding clockwise, and the given
radius.
You can play around with the example below in order to fit to your arrangement e.g. in order to start from 3 o'clock and rotate counter-clockwise etc you just need to play with degrees and radius:
const degrees = (i % numbersPerRing) * (360 / numbersPerRing);
const radius = (Math.floor(i / numbersPerRing) + 1) * ringSize;
// numbers
const numbersPerRing = 8;
const ringSize = 30;
const originX = 120;
const originY = 120;
const numbers = [... new Array(18)].map((d, i) => i + 1);
const diameters = [... new Array(4)].map((d, i) => (i * ringSize) + 10);
const points = numbers.map((n, i) => {
const degrees = (i % numbersPerRing) * (360 / numbersPerRing);
const radius = (Math.floor(i / numbersPerRing) + 1) * ringSize;
const point = d3.pointRadial(degrees * (Math.PI / 180), radius);
return {
"value": n,
"degrees": degrees,
"point": point
}
});
// svg
const svg = d3.select("body")
.append("svg")
.attr("width", originX * 2)
.attr("height", originY * 2);
// circles
svg.selectAll("circles")
.data(diameters)
.enter()
.append("circle")
.attr("cx", originX)
.attr("cy", originY)
.attr("r", d => d)
.attr("fill", "none")
.attr("stroke", "#aaaaaa");
// labels
svg.selectAll("labels")
.data(points)
.enter()
.append("text")
.attr("x", (d, i) => d.point[0])
.attr("y", (d, i) => d.point[1])
.attr("transform", `translate(${originX},${originX})`)
.attr("text-anchor", (d) => (d.degrees > 180) ? "end" : "start")
.text(d => d.value);
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
I have a d3 (v7) visualization where I have a variable number of circles being drawn on the screen depending on my data set.
How can I get arrows connecting these circles? I'm trying to follow this guide: https://observablehq.com/#harrylove/draw-an-arrow-between-circles-with-d3-links
However, this is only for a set number of circles (2) and I will have a variable number of them depending on my dataset.
Below is my current d3 code that draws circles:
var svgContainer = d3.select("body")
.append("svg")
.attr("width", 800)
.attr("height", 200);
var circles = svgContainer.selectAll("circle")
.data(nodeObjs)
.enter()
.append("circle");
circles
.attr("cx", function (d, i) {return i * 100 + 30})
.attr("cy", 60)
.attr("r", 30)
.style("fill", "steelblue");
To make the observable example dynamic there are a few factors to take into account:
you need 2 link functions; 1 for horizontal and 1 for vertical - below I have linkH and linkV instead of just link
the link function doesn't need to be called immediately so lose the ({ source: linkSource, target: linkTarget}); - you are going to need an array of links instead
some choice between linkH and linkV - you can test if the x-gap is greater than the y-gap between two circles and choose a horizontal link; and vice versa
in the example I've made the horizontal vs vertical decision in the link creation; then call linkH or linkV in the .attr("d", ...) section
in the case that the arrow runs right to left or top to bottom you need to reverse the sign on the adjustment of the link x and y
See the working example below:
const svgWidth = 480;
const svgHeight = 180;
const svg = d3.select("body")
.append("svg")
.attr("width", svgWidth)
.attr("height", svgHeight);
// Define the arrowhead marker variables
const markerBoxWidth = 8;
const markerBoxHeight = 8;
const refX = markerBoxWidth / 2;
const refY = markerBoxHeight / 2;
const markerWidth = markerBoxWidth / 2;
const markerHeight = markerBoxHeight / 2;
const arrowPoints = [[0, 0], [0, 8], [8, 4]];
// Add the arrowhead marker definition to the svg element
svg
.append("defs")
.append("marker")
.attr("id", "arrow")
.attr("viewBox", [0, 0, markerBoxWidth, markerBoxHeight])
.attr("refX", refX)
.attr("refY", refY)
.attr("markerWidth", markerBoxWidth)
.attr("markerHeight", markerBoxHeight)
.attr("orient", "auto-start-reverse")
.append("path")
.attr("d", d3.line()(arrowPoints))
.attr("stroke", "black");
// horizontal link
const linkH = d3
.linkHorizontal()
.x(d => d.x)
.y(d => d.y);
// vertical link
const linkV = d3
.linkVertical()
.x(d => d.x)
.y(d => d.y);
// circle data
const n = (Math.floor(Math.random() * 12) * 2) + 2;
const circleRadius = 10;
const nodes = [];
const links = [];
for (let i=0; i<n; i++) {
nodes.push({
x: Math.floor(Math.random() * (svgWidth - 20)) + 20,
y: Math.floor(Math.random() * (svgHeight - 20)) + 20,
r: circleRadius
});
}
for (let i=0; i<n; i+=2) {
const xdelta = Math.abs(nodes[i + 1].x - nodes[i].x);
const ydelta = Math.abs(nodes[i + 1].y - nodes[i].y);
links.push({
source: { x: nodes[i].x, y: nodes[i].y },
target: { x: nodes[i + 1].x, y: nodes[i + 1].y },
arrowDirection: ydelta >= xdelta ? "V" : "H"
});
}
const circles = svg.selectAll(".node")
.data(nodes)
.enter()
.append("circle")
.attr("class", "node");
circles
.attr("cx", (d, i) => d.x)
.attr("cy", (d, i) => d.y)
.attr("r", d => d.r);
const arrows = svg.selectAll(".arrow")
.data(links)
.enter()
.append("path")
.attr("class", "arrow");
arrows
.attr("d", (d, i) => {
let reversed;
if (d.arrowDirection === "H") {
reversed = d.source.x < d.target.x ? 1 : -1;
d.source.x += circleRadius * reversed;
d.target.x -= (circleRadius + markerWidth) * reversed;
return linkH(d);
} else {
reversed = d.source.y > d.target.y ? 1 : -1;
d.source.y -= circleRadius * reversed;
d.target.y += (circleRadius + markerWidth) * reversed;
return linkV(d);
}
})
.attr("marker-end", "url(#arrow)");
.node {
fill: green;
stroke: steelblue;
}
.arrow {
stroke: black;
fill: none;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/7.0.0/d3.min.js"></script>
I have been trying to create a stacked segment chart for the past few days and between all the trigonometry and key stacking it has truly been a nightmare for me to apply what seems to be the most basic of tweaks. Currently my segment chart has data that is sorted in descending order based on Active equity. I want to change this to have the data sorted by total.
const csvData = `State,Active equity,Passive equity,Fixed income,Mixed assets
BlackRock,1,17,0,0
Fidelity,13,2,0,0
SSgA,12,0,0,0
Hang Seng,11,0,0,0
UBS,9,0,0,1
Schroders,6,0,2,1
JP Morgan,5,2,0,1
Value Partners,1,0,6,0
First State,5,0,0,0
Invesco,4,1,0,0
HSBC,1,1,1,1
DBS,0,2,1,0
BOCI,1,1,1,0
CSOP,0,2,1,0
Principal,1,1,0,0
Allianz,2,1,0,0
Yuanta,0,2,1,0
Manulife,1,0,1,0
Aberdeen,2,0,0,0
Mirae,1,1,0,0
,0,0,0,0`;
//const data = d3.csvParse(csvData, d3.autoType);
var margins = {top:20, bottom:300, left:30, right:100};
var height = 600;
var width = 900;
var totalWidth = width+margins.left+margins.right;
var totalHeight = height+margins.top+margins.bottom;
var outerRadius = (400 / 2);
var innerRadius = 15;
var svg = d3.select('body')
.append('svg')
.attr('width', totalWidth)
.attr('height', totalHeight);
var graphGroup = svg.append('g')
.attr('transform', "translate(250,250)");
var x = d3.scaleBand()
.range([0, 2 * Math.PI])
.align(0);
var y = d3.scaleRadial()
.range([innerRadius, outerRadius]);
var z = d3.scaleOrdinal()
.range(["#003366", "#4f81b9", "#95b3d7", "#f6d18b"]);
d3.csv("csvData", function(d, i, columns) {
for (i = 1, t = 0; i < columns.length; ++i) t += d[columns[i]] = +d[columns[i]];
d.total = t;
return d;
}, function(error, data) {
if (error) throw error;
weave(data, function(a, b) { return b[data.columns[6]] - a[data.columns[6]]; });
x.domain(data.map(function(d) { return d.State; }));
y.domain([0, d3.max(data, function(d) { return d.total; })*1.3]);
z.domain(data.columns.slice(1));
graphGroup.append('g')
.selectAll('g')
.data(d3.stack().keys(data.columns.slice(1))(data))
.enter().append("g")
.selectAll(".bg-arc2")
.data(function(d) { return d; })
.enter().append("path")
.attr("d", d3.arc()
.innerRadius(innerRadius)
.outerRadius(outerRadius+2)
.startAngle(function(d) { return x(d.data.State); })
.endAngle(function(d) { return x(d.data.State) + x.bandwidth()*.90; })
.padAngle(0.1)
.padRadius(innerRadius))
.attr('class','bg-arc2')
.attr('fill','none')
.attr('stroke-width','4px')
.attr('stroke','#003366');
graphGroup.append('circle')
.attr('cx',0)
.attr('cy',0)
.attr('r',200)
.style('fill','#d9d9d9');
graphGroup.append('g')
.selectAll('g')
.data(d3.stack().keys(data.columns.slice(1))(data))
.enter().append("g")
.selectAll(".bg-arc")
.data(function(d) { return d; })
.enter().append("path")
.attr("d", d3.arc()
.innerRadius(innerRadius)
.outerRadius(outerRadius)
.startAngle(function(d) { return x(d.data.State); })
.endAngle(function(d) { return x(d.data.State) + x.bandwidth()*.95; })
.padAngle(0.1)
.padRadius(innerRadius))
.attr('class','bg-arc')
.attr('fill','#fff');
graphGroup.append('circle')
.attr('cx',0)
.attr('cy',0)
.attr('r',innerRadius)
.style('fill','#fff');
var stackedData = d3.stack().keys(data.columns.slice(1))(data);
var stackedData2 = stackedData.sort(function(a,b) { return d3.descending(a[0].data.total, b[0].data.total)});
console.log(stackedData[0][0].data.total)
console.log(stackedData2);
graphGroup.append("g")
.selectAll("g")
.data(stackedData)
.enter().append("g")
.attr("fill", function(d) { return z(d.key); })
.selectAll("path")
.data(function(d) { return d; })
.enter().append("path")
.attr("d", d3.arc()
.innerRadius(function(d) { return y(d[0]); })
.outerRadius(function(d) { return y(d[1]); })
.startAngle(function(d) { return x(d.data.State); })
.endAngle(function(d) { return x(d.data.State) + x.bandwidth()*.95; })
.padAngle(0.04)
.padRadius(innerRadius));
var label = graphGroup.append("g")
.selectAll("g")
.data(data)
.enter().append("g")
.attr("text-anchor", "middle")
.attr("transform", function(d) { return "rotate(" + ((x(d.State) + x.bandwidth() / 2) * 180 / Math.PI - 90) + ")translate(" + (outerRadius+25) + ",0)"; });
label.append("text")
.attr("transform", function(d) { return (x(d.State) + x.bandwidth() / 2 + Math.PI / 2) % (2 * Math.PI) < Math.PI ? "rotate(90)translate(0,16)" : "rotate(-90)translate(0,-9)"; })
.text(function(d) { return d.State; });
var yAxis = graphGroup.append("g")
.attr("text-anchor", "end");
var yTick = yAxis
.selectAll("g")
.data(y.ticks(10).slice(1))
.enter().append("g");
});
function weave(array, compare) {
var i = -1, j, n = array.sort(compare).length, weave = new Array(n);
while (++i < n) weave[i] = array[(j = i << 1) >= n ? (n - i << 1) - 1 : j];
while (--n >= 0) array[n] = weave[n];
}
<script src="https://d3js.org/d3.v4.min.js"></script>
<script src="https://gist.githubusercontent.com/mbostock/3686329aa6e1f5938df8eef12ec353fe/raw/1ab722df937c3ac86cac8292e34cfc7279b017f8/d3-scale-radial.js"></script>
Relevant code here:
var stackedData = d3.stack().keys(data.columns.slice(1))(data);
var stackedData2 = stackedData.sort(function(a,b) { return d3.descending(a[0].data.total, b[0].data.total)});
console.log(stackedData[0][0].data.total)
console.log(stackedData2);
I checked via the console log to make sure I was slicing at the correct place, which I was, as confirmed by console.log(stackedData[0][0].data.total) which returned the correct value of 18.
However I can't apply the sort as desired. The data is still sorted by Active equity and not total.
Question
The default sort for stacked radial charts seems to be whatever the first variable is. In my case, it's Active equity. With that in mind, based off my progress above, what is keeping me from applying the descending order sort by data.total as opposed to the default?
Your question is not clear: are you talking about sorting the segments within each slice, or are you talking about sorting the slices themselves?
For the first case, there is an method named order, that you can use with the stack generator (the link goes to v5 docs, which are a bit different from v4). However, because you said "I want to change [the order] to have the data sorted by total", it seems to me that you're talking about sorting the slices. If that is correct, two observations: it's not sorted by Active equity, right now it's just the order of the objects in the original data array.
For sorting by total you just need to change that array:
data.sort(function(a, b) {
return b.total - a.total;
});
Also, get rid of the weave function.
Here is the result:
(function(global, factory) {
typeof exports === "object" && typeof module !== "undefined" ? factory(exports, require("d3-scale")) :
typeof define === "function" && define.amd ? define(["exports", "d3-scale"], factory) :
(factory(global.d3 = global.d3 || {}, global.d3));
}(this, function(exports, d3Scale) {
'use strict';
function square(x) {
return x * x;
}
function radial() {
var linear = d3Scale.scaleLinear();
function scale(x) {
return Math.sqrt(linear(x));
}
scale.domain = function(_) {
return arguments.length ? (linear.domain(_), scale) : linear.domain();
};
scale.nice = function(count) {
return (linear.nice(count), scale);
};
scale.range = function(_) {
return arguments.length ? (linear.range(_.map(square)), scale) : linear.range().map(Math.sqrt);
};
scale.ticks = linear.ticks;
scale.tickFormat = linear.tickFormat;
return scale;
}
exports.scaleRadial = radial;
Object.defineProperty(exports, '__esModule', {
value: true
});
}));
const csvData = `State,Active equity,Passive equity,Fixed income,Mixed assets
BlackRock,1,17,0,0
Fidelity,13,2,0,0
SSgA,12,0,0,0
Hang Seng,11,0,0,0
UBS,9,0,0,1
Schroders,6,0,2,1
JP Morgan,5,2,0,1
Value Partners,1,0,6,0
First State,5,0,0,0
Invesco,4,1,0,0
HSBC,1,1,1,1
DBS,0,2,1,0
BOCI,1,1,1,0
CSOP,0,2,1,0
Principal,1,1,0,0
Allianz,2,1,0,0
Yuanta,0,2,1,0
Manulife,1,0,1,0
Aberdeen,2,0,0,0
Mirae,1,1,0,0
,0,0,0,0`;
const data = d3.csvParse(csvData, function(d, i, columns) {
for (i = 1, t = 0; i < columns.length; ++i) t += d[columns[i]] = +d[columns[i]];
d.total = t;
return d;
});
data.sort(function(a, b) {
return b.total - a.total;
});
var margins = {
top: 20,
bottom: 300,
left: 30,
right: 100
};
var height = 600;
var width = 900;
var totalWidth = width + margins.left + margins.right;
var totalHeight = height + margins.top + margins.bottom;
var outerRadius = (400 / 2);
var innerRadius = 15;
var svg = d3.select('body')
.append('svg')
.attr('width', totalWidth)
.attr('height', totalHeight);
var graphGroup = svg.append('g')
.attr('transform', "translate(250,250)");
var x = d3.scaleBand()
.range([0, 2 * Math.PI])
.align(0);
var y = d3.scaleRadial()
.range([innerRadius, outerRadius]);
var z = d3.scaleOrdinal()
.range(["#003366", "#4f81b9", "#95b3d7", "#f6d18b"]);
x.domain(data.map(function(d) {
return d.State;
}));
y.domain([0, d3.max(data, function(d) {
return d.total;
}) * 1.3]);
z.domain(data.columns.slice(1));
graphGroup.append('g')
.selectAll('g')
.data(d3.stack().keys(data.columns.slice(1))(data))
.enter().append("g")
.selectAll(".bg-arc2")
.data(function(d) {
return d;
})
.enter().append("path")
.attr("d", d3.arc()
.innerRadius(innerRadius)
.outerRadius(outerRadius + 2)
.startAngle(function(d) {
return x(d.data.State);
})
.endAngle(function(d) {
return x(d.data.State) + x.bandwidth() * .90;
})
.padAngle(0.1)
.padRadius(innerRadius))
.attr('class', 'bg-arc2')
.attr('fill', 'none')
.attr('stroke-width', '4px')
.attr('stroke', '#003366');
graphGroup.append('circle')
.attr('cx', 0)
.attr('cy', 0)
.attr('r', 200)
.style('fill', '#d9d9d9');
graphGroup.append('g')
.selectAll('g')
.data(d3.stack().keys(data.columns.slice(1))(data))
.enter().append("g")
.selectAll(".bg-arc")
.data(function(d) {
return d;
})
.enter().append("path")
.attr("d", d3.arc()
.innerRadius(innerRadius)
.outerRadius(outerRadius)
.startAngle(function(d) {
return x(d.data.State);
})
.endAngle(function(d) {
return x(d.data.State) + x.bandwidth() * .95;
})
.padAngle(0.1)
.padRadius(innerRadius))
.attr('class', 'bg-arc')
.attr('fill', '#fff');
graphGroup.append('circle')
.attr('cx', 0)
.attr('cy', 0)
.attr('r', innerRadius)
.style('fill', '#fff');
var stackedData = d3.stack()
.keys(data.columns.slice(1))
(data);
graphGroup.append("g")
.selectAll("g")
.data(stackedData)
.enter().append("g")
.attr("fill", function(d) {
return z(d.key);
})
.selectAll("path")
.data(function(d) {
return d;
})
.enter().append("path")
.attr("d", d3.arc()
.innerRadius(function(d) {
return y(d[0]);
})
.outerRadius(function(d) {
return y(d[1]);
})
.startAngle(function(d) {
return x(d.data.State);
})
.endAngle(function(d) {
return x(d.data.State) + x.bandwidth() * .95;
})
.padAngle(0.04)
.padRadius(innerRadius));
var label = graphGroup.append("g")
.selectAll("g")
.data(data)
.enter().append("g")
.attr("text-anchor", "middle")
.attr("transform", function(d) {
return "rotate(" + ((x(d.State) + x.bandwidth() / 2) * 180 / Math.PI - 90) + ")translate(" + (outerRadius + 25) + ",0)";
});
label.append("text")
.attr("transform", function(d) {
return (x(d.State) + x.bandwidth() / 2 + Math.PI / 2) % (2 * Math.PI) < Math.PI ? "rotate(90)translate(0,16)" : "rotate(-90)translate(0,-9)";
})
.text(function(d) {
return d.State;
});
var yAxis = graphGroup.append("g")
.attr("text-anchor", "end");
var yTick = yAxis
.selectAll("g")
.data(y.ticks(10).slice(1))
.enter().append("g");
function weave(array, compare) {
var i = -1,
j, n = array.sort(compare).length,
weave = new Array(n);
while (++i < n) weave[i] = array[(j = i << 1) >= n ? (n - i << 1) - 1 : j];
while (--n >= 0) array[n] = weave[n];
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.13.0/d3.min.js"></script>