I have a project where I draw a doughnut/pie chart. Each slice in the chart consists of a group which in term has a path(the slice) and an image.
In order to transition the slices I have used the general update pattern to update the slices and added two attrTween methods to handle the transitions of the slices.
In order to add the images to the slices I have first put each slice into a group. Then I add an image to each group and use the arc.centroid method in order to position the images in the center of each slice. This works very well the first time I load the in the chart. But when the chart updates, the images stay in the center of their group as it was positioned previously.
I have console logged both the d element and the output of the arc.controid method with that d element. You can see clearly that the coordinates of the d elements don't change and that is why the images don't get an updated new position. But I do not get why they still get the old version of this d element and not the new updated one.
.append("svg:image")
.attr("transform", (d, i) => {
if (i === 1) {
console.log(d);
console.log(this.arc.centroid(d));
}
var x = this.arc.centroid(d)[0] - image_width / 2;
var y = this.arc.centroid(d)[1] - image_height / 2;
return "translate(" + x + "," + y + ")";
})
This is a snippet of the code I am using. I have tried to keep this as short as possible. But all the elements that are included are needed in order to get this snippet to work for this particular problem:
var margin = 1;
this.width = 250;
this.height = 250;
this.index = 0;
this.radius = Math.min(this.width, this.height) / 2 - margin;
this.svg = d3
.select(".canvas")
.append("svg")
.attr("width", this.width)
.attr("height", this.height)
.append("g")
.attr(
"transform",
"translate(" + this.width / 2 + "," + this.height / 2 + ")"
);
this.pie = d3
.pie()
.sort(null)
.value(d => d.value);
this.arc = d3
.arc()
.outerRadius(100)
.innerRadius(50);
const setSlicesOnDoughnut = (data) => {
this.arcs = this.svg.selectAll("path").data(this.pie(data[this.index]));
this.arcs.join(
enter => {
enter
.append("g")
.append("path")
.attr("class", "arc")
.attr("fill", "#206BF3")
.attr("stroke", "#2D3546")
.style("stroke-width", "2px")
.each(function(d) {
this._current = d;
})
.transition()
.duration(1000)
.attrTween("d", arcTweenEnter);
},
update => {
update
.transition()
.duration(1000)
.attrTween("d", arcTweenUpdate);
},
exit => {
exit.remove();
}
);
}
const addImagesToSlices = () => {
var image_width = 20;
var image_height = 20;
this.svg.selectAll("image").remove();
this.svg
.selectAll("g")
.append("svg:image")
.attr("transform", (d, i) => {
if (i === 1) {
console.log(d);
console.log(this.arc.centroid(d));
}
var x = this.arc.centroid(d)[0] - image_width / 2;
var y = this.arc.centroid(d)[1] - image_height / 2;
return "translate(" + x + "," + y + ")";
})
.attr("class", "logo")
.attr("class", function(d) {
return `${d.data.key}-logo`;
})
.attr("href", d => d.data.icon)
.attr("width", image_width)
.attr("height", image_height)
.attr("opacity", 0)
.transition()
.duration(1500)
.attr("opacity", 1);
}
var data = [
[{
key: "One",
value: 20,
icon: "http://files.gamebanana.com/img/ico/sprays/4f68c8d10306a.png"
},
{
key: "Two",
value: 30,
icon: "http://files.gamebanana.com/img/ico/sprays/4f68c8d10306a.png"
},
{
key: "Three",
value: 10,
icon: "http://files.gamebanana.com/img/ico/sprays/4f68c8d10306a.png"
},
{
key: "Four",
value: 15,
icon: "http://files.gamebanana.com/img/ico/sprays/4f68c8d10306a.png"
}
],
[{
key: "One",
value: 30,
icon: "http://files.gamebanana.com/img/ico/sprays/4f68c8d10306a.png"
},
{
key: "Two",
value: 15,
icon: "http://files.gamebanana.com/img/ico/sprays/4f68c8d10306a.png"
},
{
key: "Three",
value: 20,
icon: "http://files.gamebanana.com/img/ico/sprays/4f68c8d10306a.png"
},
{
key: "Four",
value: 10,
icon: "http://files.gamebanana.com/img/ico/sprays/4f68c8d10306a.png"
}
]
]
const arcTweenEnter = (d) => {
var i = d3.interpolate(d.endAngle, d.startAngle);
return t => {
d.startAngle = i(t);
return this.arc(d);
};
}
const arcTweenUpdate = (d, i, n) => {
var interpolate = d3.interpolate(n[i]._current, d);
n[i]._current = d;
return t => {
return this.arc(interpolate(t));
};
}
setSlicesOnDoughnut(data);
addImagesToSlices();
const swap = document.querySelector(".swap");
swap.addEventListener("click", () => {
if (this.index === 0) this.index = 1;
else this.index = 0;
setSlicesOnDoughnut(data);
addImagesToSlices();
});
<button class="swap">swap</button>
<div class="canvas"></div>
<script src="https://d3js.org/d3.v6.js"></script>
The issue is that you never update the datum bound to the g. Let's look at how you enter and update the wedges:
this.arcs = this.svg.selectAll("path").data(this.pie(data[this.index]));
this.arcs.join(
enter => {
enter
.append("g")
.append("path")
...
},
update => {
update
.transition()
...
},
You select all the paths and bind data to them (selectAll("path")). On enter you append a g, which means the g gets a bound datum, as does the child path. On update however, as you've only selected paths, you only bind new data to the paths. The parent g is left with the original bound datum.
Instead, let's select the parent g with selectAll, and tweak our update function slightly to account for this:
// select parent g elements instead of paths:
this.arcs = this.svg.selectAll("g").data(this.pie(data[this.index]));
this.arcs.join(
enter => {
enter
.append("g")
.append("path")
...
},
update => {
update
.select("path") // select the child path.
.transition()
...
},
Now when we (re-)append the images, they are using the most recent datum bound to their parent g:
var margin = 1;
this.width = 250;
this.height = 250;
this.index = 0;
this.radius = Math.min(this.width, this.height) / 2 - margin;
this.svg = d3
.select(".canvas")
.append("svg")
.attr("width", this.width)
.attr("height", this.height)
.append("g")
.attr(
"transform",
"translate(" + this.width / 2 + "," + this.height / 2 + ")"
);
this.pie = d3
.pie()
.sort(null)
.value(d => d.value);
this.arc = d3
.arc()
.outerRadius(100)
.innerRadius(50);
const setSlicesOnDoughnut = (data) => {
this.arcs = this.svg.selectAll("g").data(this.pie(data[this.index]));
this.arcs.join(
enter => {
enter
.append("g")
.append("path")
.attr("class", "arc")
.attr("fill", "#206BF3")
.attr("stroke", "#2D3546")
.style("stroke-width", "2px")
.each(function(d) {
this._current = d;
})
.transition()
.duration(1000)
.attrTween("d", arcTweenEnter);
},
update => {
update
.select("path")
.transition()
.duration(1000)
.attrTween("d", arcTweenUpdate);
},
exit => {
exit.remove();
}
);
}
const addImagesToSlices = () => {
var image_width = 20;
var image_height = 20;
this.svg.selectAll("image").remove();
this.svg
.selectAll("g")
.append("svg:image")
.attr("transform", (d, i) => {
if (i === 1) {
console.log(this.arc.centroid(d));
}
var x = this.arc.centroid(d)[0] - image_width / 2;
var y = this.arc.centroid(d)[1] - image_height / 2;
return "translate(" + x + "," + y + ")";
})
.attr("class", "logo")
.attr("class", function(d) {
return `${d.data.key}-logo`;
})
.attr("href", d => d.data.icon)
.attr("width", image_width)
.attr("height", image_height)
.attr("opacity", 0)
.transition()
.duration(1500)
.attr("opacity", 1);
}
var data = [
[{
key: "One",
value: 20,
icon: "http://files.gamebanana.com/img/ico/sprays/4f68c8d10306a.png"
},
{
key: "Two",
value: 30,
icon: "http://files.gamebanana.com/img/ico/sprays/4f68c8d10306a.png"
},
{
key: "Three",
value: 10,
icon: "http://files.gamebanana.com/img/ico/sprays/4f68c8d10306a.png"
},
{
key: "Four",
value: 15,
icon: "http://files.gamebanana.com/img/ico/sprays/4f68c8d10306a.png"
}
],
[{
key: "One",
value: 30,
icon: "http://files.gamebanana.com/img/ico/sprays/4f68c8d10306a.png"
},
{
key: "Two",
value: 15,
icon: "http://files.gamebanana.com/img/ico/sprays/4f68c8d10306a.png"
},
{
key: "Three",
value: 20,
icon: "http://files.gamebanana.com/img/ico/sprays/4f68c8d10306a.png"
},
{
key: "Four",
value: 100,
icon: "http://files.gamebanana.com/img/ico/sprays/4f68c8d10306a.png"
}
]
]
const arcTweenEnter = (d) => {
var i = d3.interpolate(d.endAngle, d.startAngle);
return t => {
d.startAngle = i(t);
return this.arc(d);
};
}
const arcTweenUpdate = (d, i, n) => {
var interpolate = d3.interpolate(n[i]._current, d);
n[i]._current = d;
return t => {
return this.arc(interpolate(t));
};
}
setSlicesOnDoughnut(data);
addImagesToSlices();
const swap = document.querySelector(".swap");
swap.addEventListener("click", () => {
if (this.index === 0) this.index = 1;
else this.index = 0;
setSlicesOnDoughnut(data);
addImagesToSlices();
});
<button class="swap">swap</button>
<div class="canvas"></div>
<script src="https://d3js.org/d3.v6.js"></script>
Related
here is an attempt to make a data tree so that whenever the user clicks on any element the element must be removed from the dataset and the tree will once again be regenerated.
for example, before clicking
let us suppose after clicking on p3 the new graph generated is
the code through which the graph is generated is
const svg = d3.select("body").append('svg');
const margin = { left: 80, right: 20, top: 20, bottom: 20 }
var NODES;
const height = 700 - margin.top - margin.bottom;
const width = 800 - margin.left - margin.right;
const nodeElements = [];
let treeData = { id: 0, name: 'p0', children: [{ id: 1, name: 'p1', children: [{ id: 3, name: 'p3', children: [] },] }, { id: 2, name: 'p2', children: [{ id: 4, name: 'p4', children: [] },] }] };
let duration = 1513;
let i = 0;
let root;
let treemap = d3.tree().size([height, width])
svg.attr('height', height + margin.top + margin.bottom)
.attr('width', width + margin.right + margin.left)
.append('g')
.attr('transform', `translate(${margin.left},${margin.top})`)
root = d3.hierarchy(treeData, (d) => {
console.log(d);
return d.children;
})
root.x0 = height / 2;
root.y0 = 0;
console.log('ROOT::', root);
update(root);
function update(source) {
let treedata = treemap(root);
let nodes = treedata.descendants();
NODES = nodes;
nodes.forEach(d => {
d.y = d.depth * width / 5;
});
let node = svg.selectAll("g.node").data(nodes, (d) => d.id || (d.id = ++i));
// links
function diagonal(s, d) {
path = `M ${s.y} ${s.x}
C ${(s.y + d.y) / 2} ${s.x}
${(s.y + d.y) / 2} ${d.x}
${d.y} ${d.x}`;
return path;
}
let links = treedata.descendants().slice(1);
let link = svg.selectAll('path.link').data(links, (d) => {
return d.id;
})
let linkEnter = link
.enter()
.insert('path', 'g')
.attr('class', 'link')
.attr('d', (d) => {
let o = { x: source.x0, y: source.y0 + 40 }
return diagonal(o, o)
})
let linkUpdate = linkEnter.merge(link);
linkUpdate
.transition()
.duration(duration)
.attr("d", (d) => {
return diagonal(d, d.parent);
});
let linkExit = link
.exit()
.transition()
.attr('d', (d) => {
let o = { x: source.x0, y: source.y0 }
return diagonal(o, o);
})
.remove();
let nodeEnter = node
.enter()
.append("g")
.attr("class", "node")
.attr("transform", d => {
return `translate(${source.y0 + 20},${source.x0})`
})
.on("click", clicked);
nodeEnter.append('circle')
.attr('class', 'node')
.attr('r', 0)
.style('fill', d => {
return d._children ? "red" : "white";
})
let nodeUpdate = nodeEnter.merge(node);
nodeUpdate.transition()
.duration(duration)
.attr("transform", d => `translate(${d.y + 20},${d.x})`)
.attr("opacity", 1)
nodeUpdate.select("circle.node")
.attr('r', 10)
.style("fill", d => d._children ? "red" : "black")
.attr("cursor", "pointer");
nodeUpdate.append('rect')
.attr('x', 0)
.attr('y', -20)
.attr('rx', 5)
.attr('ry', 5)
.attr('width', 80)
.attr('height', 40)
.attr('fill', 'grey')
nodeUpdate.append('text')
.attr('x', 0)
.attr('y', 0)
.attr('dx', 10)
.text(d => {
console.log(d.data.name)
return d.data.name;
});
nodeExit = node.exit()
.transition()
.duration(duration)
.attr("transform", function () { return `translate(${source.y + 20},${source.x})` })
.attr("opacity", 0.5)
.remove();
// collapsing of the nodes
nodes.forEach(d => {
d.x0 = d.x;
d.y0 = d.y;
})
}
function clicked(event, d) {
let child;
child = childrenCollector(d.id - 1, treeData.children)
root = d3.hierarchy(treeData, (da) => {
return da.children;
})
console.log("MANIPULATED:::", root)
root.x0 = height / 2;
root.y0 = 0;
update(root);
}
function childrenCollector(sourceId, nestedArray) {
const i = nestedArray.findIndex(({ id }) => id === sourceId);
let found;
if (i > -1) [found] = nestedArray.splice(i, 1)
else nestedArray.some(({ children }) =>
found = childrenCollector(sourceId, children)
);
return found;
}
the problem being faced is once it generates the new structure the new one is not getting modified.
REASON:::: in the clicked() function at 'd.id' when the element is clicked first the id value is equal to the id given in the data but after modification when I click the 'd.id' value keeps on increasing. Why is it happening, even when I am generating the graph over a new dataset
I am trying to accomplish something similar to what is here : https://www.opportunityatlas.org/. If you proceed further to this link and click on 'Show Distribution' to see the graph and select 'On Screen' and then move the cursor around the map you will see the size of the rectangles changes and also the update patterns works i.e. if a rectangle was already there it moves horizontally to the new value.
I have tried doing the same but could not achieve the update part. Could you please point me out where I missed out. I have attached a part of my code where there are two data sets data1 and data2 with some id's common but as you can see when you click on update to change the data set all rectangles are in the enter phase and none of them change position for existing ones (none in update phase). It will be helpful if someone can guide me through this one. If there is some other way to implement the same graph that I provided in the link, that would also be helpful if there is some other approach. Thanks in advance !
let initialRender = true;
let selectedData1 = true;
const margin = {
top: 10,
right: 30,
bottom: 30,
left: 30
},
width = 550 - margin.left - margin.right,
height = 150 - margin.top - margin.bottom;
function printChart(asd, data, dataGradients) {
const svg = d3.select('#data-viz')
// const isZoomed = map.getZoom() > zoomThreshold;
// if (isZoomed !== changedZoom) {
// initialRender = true;
// changedZoom = isZoomed;
// }
// X axis and scale ------------->>>>>>>>>>>>>>>>>>>>
const xScale = d3.scaleLinear()
.domain(d3.extent(data.map(d => d.value)))
.range([0, width])
const xAxisCall = d3.axisBottom(xScale)
.tickFormat(d3.format(".2s"))
.ticks(5)
.tickSizeOuter(0);
let xAxis = null
if (initialRender) {
d3.select(".axis-x").remove()
xAxis = svg.append("g")
.attr("class", "axis-x")
.attr("transform", "translate(0," + 115 + ")")
initialRender = false
} else {
xAxis = d3.select(".axis-x")
}
xAxis.transition()
.duration(2000)
.ease(d3.easeSinInOut)
.call(xAxisCall)
// X axis and scale <<<<<<<<<<<<<<<<-----------------------------
const binMin = 5;
const binMax = 150;
const tDuration = 3000;
// Just to calculate max elements in each bin ---------->>>>>>>>>>>>>>>>>>
let histogram = d3.histogram()
.value(d => d.value)
.domain(xScale.domain())
.thresholds(xScale.ticks(10));
let bins = histogram(data).filter(d => d.length > 0);
console.log(bins);
const max = d3.max(bins.map(bin => bin.length))
const maxBinSize = max <= 10 ? 10 : max
// Just to calculate max elements in each bin <<<<<<<<<<<<----------------
// Decide parameters for histogram ------------>>>>>>>>>>>>>>>>>
const dotSizeScale = d3.scaleLinear()
.domain([binMin, binMax])
.range([10, 4])
const dotSize = dotSizeScale(maxBinSize);
const dotSpacingScale = d3.scaleLinear()
.domain([binMin, binMax])
.range([12, 6])
const dotSpacing = dotSpacingScale(maxBinSize);
const thresholdScale = d3.scaleLinear()
.domain([binMin, binMax])
.range([10, 100])
const threshold = thresholdScale(maxBinSize);
const yTransformMarginScale = d3.scaleLinear()
.domain([binMin, binMax])
.range([100, 100])
const yTransformMargin = yTransformMarginScale(maxBinSize);
if (dotSize !== 10) {
d3.selectAll('.gBin').remove()
d3.selectAll('rect').remove()
}
histogram = d3.histogram()
.value(d => d.value)
.domain(xScale.domain())
.thresholds(xScale.ticks(threshold));
bins = histogram(data).filter(d => d.length > 0);
// Decide parameters for histogram <<<<<<<<<<<<<<<<<<<<--------------------------
// Y axis scale -------------------->>>>>>>>>>>>>>>>>>>>
var yScale = d3.scaleLinear()
.range([height, 0]);
yScale.domain([0, d3.max(bins, (d) => d.length)]);
svg.append("g")
.attr("class", "axis-y")
.call(d3.axisLeft(yScale));
d3.select(".axis-y")
.remove()
// Y axis scale <<<<<<<<<<<<<<<<<<<<<<<-----------------
const binGroup = svg.selectAll(".gBin")
.data(bins,
(d) => {
console.log('id 1', d.x0)
return d.x0
}
)
binGroup
.exit()
.transition()
.duration(2000)
.style("opacity", 0)
.remove()
const binGroupEnter = binGroup
.enter()
.append("g")
.merge(binGroup)
.attr("class", "gBin")
.attr("x", 1)
.attr("transform", function(d) {
return "translate(" + xScale(d.x0) + "," + yTransformMargin + ")";
})
.attr("width", 10)
const elements = binGroupEnter.selectAll("rect")
.data(d => d.map((p, i) => ({
id: p.id,
idx: i,
value: p.value,
})),
function(d) {
console.log('id 2', d)
return d.id
}
)
elements.exit()
.transition()
.duration(tDuration)
.style("opacity", 0)
.remove()
elements
.enter()
.append("rect")
.merge(elements)
.attr("y", -(height + margin.top))
// .on("mouseover", tooltipOn)
// .on("mouseout", tooltipOff)
.transition()
.delay(function(d, i) {
return 50 * i;
})
.duration(tDuration)
.attr("id", d => d.value)
.attr("y", (d, i) => -(i * dotSpacing))
.attr("width", dotSize)
.attr("height", dotSize)
// .style("fill", (d) => getBinColor(d.value, dataGradients))
.style("fill", 'red')
}
const data1 = [{
id: 1,
value: 14
}, {
id: 13,
value: 12
}, {
id: 2,
value: 50
}, {
id: 32,
value: 142
}]
const data2 = [{
id: 1,
value: 135
}, {
id: 7,
value: 2
}, {
id: 2,
value: 50
}, {
id: 32,
value: 50
}]
printChart(null, data1, null)
function changeData() {
selectedData1 ?
printChart(null, data2, null) :
printChart(null, data1, null)
selectedData1 = !selectedData1
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.13.0/d3.min.js"></script>
<button onclick="changeData()"> Update data </button>
<svg width="550" height="250" id="data-viz">
<g transform="translate(30, 100)">
</g>
</svg>
Your problem appears to be these lines:
if (dotSize !== 10) {
d3.selectAll('.gBin').remove();
d3.selectAll('rect').remove();
}
All your elements are removed before any selections are calculated so everything (both your bin g and element rect) become enter.
Another interesting thing is your data key for your bins. Since you are using the x0 your g will also enter/exit depending on how the histogram function calculates the bins.
<html>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.13.0/d3.min.js"></script>
<button onclick="changeData()">Update data</button>
<svg width="550" height="250" id="data-viz">
<g transform="translate(30, 100)"></g>
</svg>
<script>
let initialRender = true;
let selectedData1 = true;
const margin = {
top: 10,
right: 30,
bottom: 30,
left: 30,
},
width = 550 - margin.left - margin.right,
height = 150 - margin.top - margin.bottom;
function printChart(asd, data, dataGradients) {
console.clear();
const svg = d3.select('#data-viz');
// const isZoomed = map.getZoom() > zoomThreshold;
// if (isZoomed !== changedZoom) {
// initialRender = true;
// changedZoom = isZoomed;
// }
// X axis and scale ------------->>>>>>>>>>>>>>>>>>>>
const xScale = d3
.scaleLinear()
.domain(d3.extent(data.map((d) => d.value)))
.range([0, width]);
const xAxisCall = d3
.axisBottom(xScale)
.tickFormat(d3.format('.2s'))
.ticks(5)
.tickSizeOuter(0);
let xAxis = null;
if (initialRender) {
d3.select('.axis-x').remove();
xAxis = svg
.append('g')
.attr('class', 'axis-x')
.attr('transform', 'translate(0,' + 115 + ')');
initialRender = false;
} else {
xAxis = d3.select('.axis-x');
}
xAxis.transition().duration(2000).ease(d3.easeSinInOut).call(xAxisCall);
// X axis and scale <<<<<<<<<<<<<<<<-----------------------------
const binMin = 5;
const binMax = 150;
const tDuration = 3000;
// Just to calculate max elements in each bin ---------->>>>>>>>>>>>>>>>>>
let histogram = d3
.histogram()
.value((d) => d.value)
.domain(xScale.domain())
.thresholds(xScale.ticks(10));
let bins = histogram(data).filter((d) => d.length > 0);
//console.log(bins);
const max = d3.max(bins.map((bin) => bin.length));
const maxBinSize = max <= 10 ? 10 : max;
// Just to calculate max elements in each bin <<<<<<<<<<<<----------------
// Decide parameters for histogram ------------>>>>>>>>>>>>>>>>>
const dotSizeScale = d3
.scaleLinear()
.domain([binMin, binMax])
.range([10, 4]);
const dotSize = dotSizeScale(maxBinSize);
const dotSpacingScale = d3
.scaleLinear()
.domain([binMin, binMax])
.range([12, 6]);
const dotSpacing = dotSpacingScale(maxBinSize);
const thresholdScale = d3
.scaleLinear()
.domain([binMin, binMax])
.range([10, 100]);
const threshold = thresholdScale(maxBinSize);
const yTransformMarginScale = d3
.scaleLinear()
.domain([binMin, binMax])
.range([100, 100]);
const yTransformMargin = yTransformMarginScale(maxBinSize);
/*
if (dotSize !== 10) {
d3.selectAll('.gBin').remove()
d3.selectAll('rect').remove()
}
*/
histogram = d3
.histogram()
.value((d) => d.value)
.domain(xScale.domain())
.thresholds(xScale.ticks(threshold));
bins = histogram(data).filter((d) => d.length > 0);
// Decide parameters for histogram <<<<<<<<<<<<<<<<<<<<--------------------------
// Y axis scale -------------------->>>>>>>>>>>>>>>>>>>>
var yScale = d3.scaleLinear().range([height, 0]);
yScale.domain([0, d3.max(bins, (d) => d.length)]);
svg.append('g').attr('class', 'axis-y').call(d3.axisLeft(yScale));
d3.select('.axis-y').remove();
// Y axis scale <<<<<<<<<<<<<<<<<<<<<<<-----------------
const binGroup = svg.selectAll('.gBin').data(bins, (d) => {
//console.log('id 1', d.x0)
return d.x0;
});
binGroup.exit().transition().duration(2000).style('opacity', 0).remove();
const binGroupEnter = binGroup
.enter()
.append('g')
.merge(binGroup)
.attr('class', 'gBin')
.attr('x', 1)
.attr('transform', function (d) {
return 'translate(' + xScale(d.x0) + ',' + yTransformMargin + ')';
})
.attr('width', 10);
const elements = binGroupEnter.selectAll('rect').data(
(d) =>
d.map((p, i) => ({
id: p.id,
idx: i,
value: p.value,
})),
function (d) {
//console.log('id 2', d)
return d.id;
}
);
let eex = elements
.exit()
.transition()
.duration(tDuration)
.style('opacity', 0)
.remove();
console.log("rects exiting", eex.nodes().map(e => "rect" + e.getAttribute('id')))
let een = elements
.enter()
.append('rect')
.attr('id', (d) => d.value);
console.log("rects entering", een.nodes().map(e => "rect" + e.getAttribute('id')))
let eem = een
.merge(elements);
console.log("rects merge", eem.nodes().map(e => "rect" + e.getAttribute('id')))
eem
.attr('y', -(height + margin.top))
// .on("mouseover", tooltipOn)
// .on("mouseout", tooltipOff)
.transition()
.delay(function (d, i) {
return 50 * i;
})
.duration(tDuration)
.attr('y', (d, i) => -(i * dotSpacing))
.attr('width', dotSize)
.attr('height', dotSize)
// .style("fill", (d) => getBinColor(d.value, dataGradients))
.style('fill', 'red');
}
const data1 = [
{
id: 1,
value: 14,
},
{
id: 13,
value: 12,
},
{
id: 2,
value: 50,
},
{
id: 32,
value: 142,
},
];
const data2 = [
{
id: 1,
value: 135,
},
{
id: 7,
value: 2,
},
{
id: 2,
value: 50,
},
{
id: 32,
value: 50,
},
];
printChart(null, data1, null);
function changeData() {
selectedData1
? printChart(null, data2, null)
: printChart(null, data1, null);
selectedData1 = !selectedData1;
}
</script>
</html>
I have created a donut chart with D3 that uses two data sets and displays slightly different size rings for each. I would like to add labels to the data set(for a legend), but the selectAll("path") expects each data set to be a simple array of values, so I have been unable to add the labels.
Below is the code I have so far and a fiddle:
Fiddle
var dataset = {
apples: [13245, 28479, 11111, 11000, 3876],
oranges: [53245, 28479, 19697, 24037, 19654],
};
var width = d3.select('#duration').node().offsetWidth,
height = 300,
cwidth = 33;
var colorO = ['#1352A4', '#2478E5', '#5D9CEC', '#A4C7F4', '#DBE8FB'];
var colorA = ['#58A53B', '#83C969', '#A8D996', '#CDE9C3', '#E6F4E1'];
var pie = d3.layout.pie()
.sort(null);
var arc = d3.svg.arc();
var svg = d3.select("#duration svg")
.append("g")
.attr("transform", "translate(" + width / 2 + "," + height / 2 + ")");
var gs = svg.selectAll("g").data(d3.values(dataset)).enter().append("g");
var path = gs.selectAll("path")
.data(function(d, i) { return pie(d); })
.enter().append("path")
.attr("fill", function(d, i, j) {
if (j == 0) {
return colorO[i];
} else {
return colorA[i];
}
})
.attr("d", function(d, i, j) {
if (j == 0) {
return arc.innerRadius(75 + cwidth * j - 17).outerRadius(cwidth * (j + 2.9))(d);
} else {
return arc.innerRadius(75 + cwidth * j - 5).outerRadius(cwidth * (j + 2.5))(d);
}
});
expects each data set to be a simple array of values
This is not true. You can and should use an array of objects. Then use the value accessor to target a property of your object for the pie function. Here's how I'd re-factor your code:
var dataset = {
apples: [{
value: 13245,
color: '#1352A4',
label: 'one'
}, {
value: 28479,
color: '#5D9CEC',
label: 'two'
}, {
value: 11111,
color: '#1352A4',
label: 'three'
}, {
value: 11000,
color: '#A4C7F4',
label: 'four'
}, {
value: 3876,
color: '#DBE8FB',
label: 'five'
}],
oranges: [{
value: 53245,
color: '#58A53B',
label: 'one'
}, {
value: 28479,
color: '#83C969',
label: 'two'
}, {
value: 19697,
color: '#A8D996',
label: 'three'
}, {
value: 24037,
color: '#CDE9C3',
label: 'four'
}, {
value: 19654,
color: '#E6F4E1',
label: 'five'
}]
};
var width = d3.select('#duration').node().offsetWidth,
height = 300,
cwidth = 33;
var pie = d3.layout.pie()
.sort(null)
.value(function(d) {
return d.value;
})
var innerArc = d3.svg.arc()
.innerRadius(58)
.outerRadius(cwidth * 2.9);
var outerArc = d3.svg.arc()
.innerRadius(70 + cwidth)
.outerRadius(cwidth * 3.5);
var svg = d3.select("#duration svg")
.append("g")
.attr("transform", "translate(" + width / 2 + "," + height / 2 + ")");
var gs = svg.selectAll("g").data(d3.values(dataset)).enter().append("g");
var en = gs.selectAll("path")
.data(function(d, i) {
return pie(d);
})
.enter();
en.append("path")
.attr("fill", function(d) {
return d.data.color;
})
.attr("d", function(d, i, j) {
return j === 0 ? innerArc(d) : outerArc(d);
});
en.append("text")
.text(function(d) {
return d.data.label;
})
.attr("transform", function(d, i, j) {
return j === 0 ? "translate(" + innerArc.centroid(d) + ")" : "translate(" + outerArc.centroid(d) + ")";
});
<script src="https://d3js.org/d3.v3.min.js"></script>
<div id="duration">
<svg style="height:300px;width:100%"></svg>
</div>
I'm developing a legend toggling d3.js pie chart application using this jsfiddle as my latest version http://jsfiddle.net/Qh9X5/3328/ .
I am aiming to get a streamlined working example where the legend can toggle the slices, trying to deactivate all slices - resorts in a reset which reactivates all the slices. Splitting up presentation and application layer logic.
Tweening needs improvement too - as the slices pop into existence then re-tween smoothly.
How do I improve/fix the various bugs in this code base?
onLegendClick: function(dt, i){
//_toggle rectangle in legend
var completeData = jQuery.extend(true, [], methods.currentDataSet);
newDataSet = completeData;
if(methods.manipulatedData){
newDataSet = methods.manipulatedData;
}
d3.selectAll('rect')
.data([dt], function(d) {
return d.data.label;
})
.style("fill-opacity", function(d, j) {
var isActive = Math.abs(1-d3.select(this).style("fill-opacity"));
if(isActive){
newDataSet[j].total = completeData[j].total;
}else{
newDataSet[j].total = 0;
}
return isActive;
});
//animate slices
methods.animateSlices(newDataSet);
//stash manipulated data
methods.manipulatedData = newDataSet;
}
Here is the entire js code - I've used the tidyup. I wasn't sure about using the shortcuts as I'm not sure the values will be correct. The latest fiddle - http://jsfiddle.net/Qh9X5/3340/
$(document).ready(function () {
var pieChart = {
el: "",
init: function (el, options) {
var clone = jQuery.extend(true, {}, options["data"]);
pieChart.el = el;
pieChart.setup(clone, options["width"], options["height"], options["r"], options["ir"]);
},
getArc: function (radius, innerradius) {
var arc = d3.svg.arc()
.innerRadius(innerradius)
.outerRadius(radius);
return arc;
},
setup: function (dataset, w, h, r, ir) {
var padding = 80;
this.width = w;
this.height = h;
this.radius = r
this.innerradius = ir;
this.color = d3.scale.category20();
this.pie = d3.layout.pie()
.sort(null)
.value(function (d) {
return d.total;
});
this.arc = this.getArc(this.radius, this.innerradius);
this.svg = d3.select(pieChart.el["selector"]).append("svg")
.attr("width", this.width + padding)
.attr("height", this.height + padding);
this.holder = this.svg.append("g")
.attr("transform", "translate(" + ((this.width / 2) + (padding / 2)) + "," + ((this.height / 2) + (padding / 2)) + ")");
this.piec = this.holder.append("g")
.attr("class", "piechart");
this.segments = this.holder.append("g")
.attr("class", "segments");
this.labels = this.holder.append("g")
.attr("class", "labels");
this.pointers = this.holder.append("g")
.attr("class", "pointers");
this.legend = this.svg.append("g")
.attr("class", "legend")
.attr("transform", "translate(" + -(this.width / 4) + "," + this.height + ")");
},
oldPieData: "",
pieTween: function (r, ir, d, i) {
var that = this;
var theOldDataInPie = pieChart.oldPieData;
// Interpolate the arcs in data space
var s0;
var e0;
if (theOldDataInPie[i]) {
s0 = theOldDataInPie[i].startAngle;
e0 = theOldDataInPie[i].endAngle;
} else if (!(theOldDataInPie[i]) && theOldDataInPie[i - 1]) {
s0 = theOldDataInPie[i - 1].endAngle;
e0 = theOldDataInPie[i - 1].endAngle;
} else if (!(theOldDataInPie[i - 1]) && theOldDataInPie.length > 0) {
s0 = theOldDataInPie[theOldDataInPie.length - 1].endAngle;
e0 = theOldDataInPie[theOldDataInPie.length - 1].endAngle;
} else {
s0 = 0;
e0 = 0;
}
var i = d3.interpolate({
startAngle: s0,
endAngle: e0
}, {
startAngle: d.startAngle,
endAngle: d.endAngle
});
return function (t) {
var b = i(t);
return pieChart.getArc(r, ir)(b);
};
},
removePieTween: function (r, ir, d, i) {
var that = this;
s0 = 2 * Math.PI;
e0 = 2 * Math.PI;
var i = d3.interpolate({
startAngle: d.startAngle,
endAngle: d.endAngle
}, {
startAngle: s0,
endAngle: e0
});
return function (t) {
var b = i(t);
return pieChart.getArc(r, ir)(b);
};
},
animateSlices: function (dataSet) {
var r = $(pieChart.el["selector"]).data("r");
var ir = $(pieChart.el["selector"]).data("ir");
this.piedata = pieChart.pie(dataSet);
//__slices
this.path = pieChart.segments.selectAll("path.pie")
.data(this.piedata, function (d) {
return d.data.label
});
this.path.enter().append("path")
.attr("class", "pie")
.attr("fill", function (d, i) {
return pieChart.color(i);
})
.attr("stroke", "#ffffff")
.transition()
.duration(300)
.attrTween("d", function (d, i) {
return pieChart.pieTween(r, ir, d, i);
});
this.path.transition()
.duration(300)
.attrTween("d", function (d, i) {
return pieChart.pieTween(r, ir, d, i);
});
this.path.exit()
.transition()
.duration(300)
.attrTween("d", function (d, i) {
return pieChart.removePieTween(r, ir, d, i);
})
.remove();
//__slices
//__labels
var labels = pieChart.labels.selectAll("text")
.data(this.piedata, function (d) {
return d.data.label
});
labels.enter()
.append("text")
.attr("text-anchor", "middle")
labels.attr("x", function (d) {
var a = d.startAngle + (d.endAngle - d.startAngle) / 2 - Math.PI / 2;
d.cx = Math.cos(a) * (ir + ((r - ir) / 2));
return d.x = Math.cos(a) * (r + 20);
})
.attr("y", function (d) {
var a = d.startAngle + (d.endAngle - d.startAngle) / 2 - Math.PI / 2;
d.cy = Math.sin(a) * (ir + ((r - ir) / 2));
return d.y = Math.sin(a) * (r + 20);
})
.attr("opacity", function (d) {
var opacityLevel = 1;
if (d.value == 0) {
opacityLevel = 0;
}
return opacityLevel;
})
.text(function (d) {
return d.data.label;
})
.each(function (d) {
var bbox = this.getBBox();
d.sx = d.x - bbox.width / 2 - 2;
d.ox = d.x + bbox.width / 2 + 2;
d.sy = d.oy = d.y + 5;
})
.transition()
.duration(300)
labels.transition()
.duration(300)
labels.exit().remove();
//__labels
//__pointers
pieChart.pointers.append("defs").append("marker")
.attr("id", "circ")
.attr("markerWidth", 6)
.attr("markerHeight", 6)
.attr("refX", 3)
.attr("refY", 3)
.append("circle")
.attr("cx", 3)
.attr("cy", 3)
.attr("r", 3);
var pointers = pieChart.pointers.selectAll("path.pointer")
.data(this.piedata, function (d) {
return d.data.label
});
pointers.enter()
.append("path")
.attr("class", "pointer")
.style("fill", "none")
.style("stroke", "black")
.attr("marker-end", "url(#circ)");
pointers.attr("d", function (d) {
if (d.cx > d.ox) {
return "M" + d.sx + "," + d.sy + "L" + d.ox + "," + d.oy + " " + d.cx + "," + d.cy;
} else {
return "M" + d.ox + "," + d.oy + "L" + d.sx + "," + d.sy + " " + d.cx + "," + d.cy;
}
})
.attr("opacity", function (d) {
var opacityLevel = 1;
if (d.value == 0) {
opacityLevel = 0;
}
return opacityLevel;
})
.transition()
.duration(300)
pointers.transition()
.duration(300)
pointers.exit().remove();
},
onToggle: function (sliceData, index) {
//_toggle rectangle in legend
//_toggle slice
var completeData = jQuery.extend(true, [], pieChart.currentDataSet);
var dataLength = completeData.length;
var newDataSet = completeData;
if (pieChart.manipulatedData) {
newDataSet = pieChart.manipulatedData;
}
d3.selectAll('rect')
.data([sliceData], function (d) {
return d.data.label;
})
.style("fill-opacity", function (d) {
var isActive = Math.abs(1 - d3.select(this).style("fill-opacity"));
if (isActive) {
newDataSet[index].total = completeData[index].total;
newDataSet[index].value = completeData[index].value;
} else {
newDataSet[index].total = 0;
newDataSet[index].value = 0;
}
return isActive;
});
//if all elements are to be not shown - reset to show all slices again.
//animate slices
pieChart.animateSlices(newDataSet);
//stash manipulated data
pieChart.manipulatedData = newDataSet;
},
update: function (el, dataSet) {
var that = this;
pieChart.el = el;
pieChart.svg = d3.select(pieChart.el["selector"] + " .piechart");
pieChart.segments = d3.select(pieChart.el["selector"] + " .segments");
pieChart.labels = d3.select(pieChart.el["selector"] + " .labels");
pieChart.pointers = d3.select(pieChart.el["selector"] + " .pointers");
pieChart.legend = d3.select(pieChart.el["selector"] + " .legend");
dataSet.forEach(function (d) {
d.total = +d.value;
});
pieChart.currentDataSet = dataSet;
pieChart.animateSlices(dataSet);
//__legends
var w = 200;
// add legend
var legend = pieChart.legend; //.append("g")
var legendRects = legend.selectAll('rect')
.data(this.piedata, function (d) {
return d.data.label
});
legendRects.enter()
.append("rect")
.attr("x", w - 65)
.attr("y", function (d, i) {
return i * 20;
})
.attr("width", 10)
.attr("height", 10)
.style("fill", function (d, i) {
return pieChart.color(i);
})
.style("stroke", function (d, i) {
return pieChart.color(i);
})
.on('click', function(d, i){
pieChart.onToggle(d, i);
})
.transition()
.duration(300)
legendRects.style("fill", function (d, i) {
return pieChart.color(i);
})
.style("stroke", function (d, i) {
return pieChart.color(i);
})
.transition()
.duration(300)
legendRects.exit().remove();
var legendText = legend.selectAll('text.label')
.data(this.piedata, function (d) {
return d.data.label
});
legendText.enter()
.append("text")
.attr("class", "label")
.attr("x", w - 52)
.attr("y", function (d, i) {
return i * 20 + 9;
})
.text(function (d) {
return d.data.label;
})
.transition()
.duration(300)
legendText.text(function (d) {
return d.data.label;
})
.transition()
.duration(300)
legendText.exit().remove();
var legendTextVals = legend.selectAll('text.vals')
.data(this.piedata, function (d) {
return d.data.label
});
legendTextVals.enter()
.append("text")
.attr("class", "vals")
.attr("x", w + 20)
.attr("y", function (d, i) {
return i * 20 + 9;
})
.text(function (d) {
return d.data.value;
})
.transition()
.duration(300)
legendTextVals.text(function (d) {
return d.data.value;
})
.transition()
.duration(300)
legendTextVals.exit().remove();
//__pointers
this.oldPieData = this.piedata;
}
};
var dataCharts = [{
"data": [{
"segments": [{
"label": "apple",
"value": 53245
}, {
"label": "cherry",
"value": 145
}, {
"label": "pear",
"value": 2245
}, {
"label": "bananana",
"value": 15325
}]
}]
}, {
"data": [{
"segments": [{
"label": "milk",
"value": 122
}, {
"label": "cheese",
"value": 44
}, {
"label": "grapes",
"value": 533
}]
}]
}, {
"data": [{
"segments": [{
"label": "pineapple",
"value": 1532
}, {
"label": "orange",
"value": 1435
}, {
"label": "grapes",
"value": 22
}]
}]
}, {
"data": [{
"segments": [{
"label": "lemons",
"value": 133
}, {
"label": "mango",
"value": 435
}, {
"label": "melon",
"value": 2122
}]
}]
}];
var clone = jQuery.extend(true, {}, dataCharts);
//__invoke concentric
$('[data-role="piechart"]').each(function (index) {
var selector = "piechart" + index;
$(this).attr("id", selector);
var options = {
data: clone[0].data,
width: $(this).data("width"),
height: $(this).data("height"),
r: $(this).data("r"),
ir: $(this).data("ir")
}
pieChart.init($("#" + selector), options);
pieChart.update($("#" + selector), clone[0].data[0].segments);
});
$(".testers a").on("click", function (e) {
e.preventDefault();
var clone = jQuery.extend(true, {}, dataCharts);
var min = 0;
var max = 3;
//__invoke pie chart
$('[data-role="piechart"]').each(function (index) {
pos = Math.floor(Math.random() * (max - min + 1)) + min;
pieChart.update($("#" + $(this).attr("id")), clone[pos].data[0].segments);
});
});
});
http://jsfiddle.net/NYEaX/1791/
In creating a relationship chart - that shows common traits - I am struggling to create the curved arcs that will match the position of the dots.
What is the best way of plotting these arcs so it dips below the horizon?
var data = [{
"userName": "Rihanna",
"userImage": "https://encrypted-tbn1.gstatic.com/images?q=tbn:ANd9GcSTzjaQlkAJswpiRZByvgsb3CVrfNNLLwjFHMrkZ_bzdPOWdxDE2Q",
"userDetails": [{
"Skills & Expertise": [{
"id": 2,
"tag": "Javascript"
}, {
"id": 3,
"tag": "Design"
}],
"Location": [{
"id": 0,
"tag": "London"
}, {
"id": 1,
"tag": "Germany"
}],
"Company": [{
"id": 0,
"tag": "The Old County"
}]
}]
}, {
"userName": "Brad",
"userImage": "https://lh3.googleusercontent.com/-XdASQvEzIzE/AAAAAAAAAAI/AAAAAAAAAls/5vbx7yVLDnc/photo.jpg",
"userDetails": [{
"Skills & Expertise": [{
"id": 0,
"tag": "JAVA"
}, {
"id": 1,
"tag": "PHP"
}, {
"id": 2,
"tag": "Javascript"
}],
"Location": [{
"id": 0,
"tag": "London"
}],
"Company": [{
"id": 0,
"tag": "The Old County"
}, {
"id": 1,
"tag": "Bakerlight"
}]
}]
}]
var viz = d3.select("#viz")
.append("svg")
.attr("width", 600)
.attr("height", 600)
.append("g")
.attr("transform", "translate(40,100)")
var patternsSvg = viz
.append('g')
.attr('class', 'patterns');
function colores_google(n) {
var colores_g = ["#f7b363", "#448875", "#c12f39", "#2b2d39", "#f8dd2f"];
return colores_g[n % colores_g.length];
}
function getRadius(d) {
var count = d.commonTags.split(",").length;
var ratio = count * 2.3;
if (count == 1) {
ratio = 8;
}
return ratio;
}
//create patterns for user images
$.each(data, function(index, value) {
var defs = patternsSvg.append('svg:defs');
defs.append('svg:pattern')
.attr('id', index + "-" + value.userName.toLowerCase())
.attr('width', 1)
.attr('height', 1)
.append('svg:image')
.attr('xlink:href', value.userImage)
.attr('x', 0)
.attr('y', 0)
.attr('width', 75)
.attr('height', 75);
console.log(value.userDetails[0]);
});
//create common data assement
var data1 = [{
"commonLabel": "Groups",
"commonTags": "test1, test2, test3, test4, test5, test6, test7"
}, {
"commonLabel": "Skills & Expertise",
"commonTags": "test1, test2, test3, test1, test2, test3, test1, test2, test3, test1, test2"
}, {
"commonLabel": "Location",
"commonTags": "test1"
}, {
"commonLabel": "Company",
"commonTags": "test1"
}]
//add curved paths
var distanceBetween = 70;
var pathStart = -400;
var path = viz.append("svg:g").selectAll("path")
.data(data1)
path
.enter().append("svg:path")
.attr("class", function(d) {
return "link "
})
path.attr("d", function(d, i) {
var sx = 0;
var tx = 235;
var sy = 120;
var ty = 120;
pathStart += 125;
var dx = 0;
var dy = getRadius(d) + (distanceBetween * i) - pathStart;
var dr = Math.sqrt(dx * dx + dy * dy);
console.log("dy", dy);
return "M" + sx + "," + sy + "A" + dr + "," + dr + " 0 0,1 " + tx + "," + ty;
});
//add curved paths
//create circles to hold the user images
var circle = viz.append("svg:g").selectAll("circle")
.data(data);
//enter
circle
.enter()
.append("svg:circle")
.attr("id", function(d) {
return d.userName;
})
.attr("r", function(d) {
return "30";
})
.attr("cx", function(d, i) {
return "235" * i;
})
.attr("cy", function(d, i) {
return "120";
})
.style("fill", function(d, i) {
return "url(#" + i + "-" + d.userName.toLowerCase() + ")";
})
var distanceBetween = 65;
var circle = viz.append("svg:g").selectAll("circle")
.data(data1);
//enter
circle
.enter()
.append("svg:circle")
.attr("id", function(d) {
return d.commonLabel;
})
.attr("r", function(d) {
return getRadius(d);
})
.attr("cx", function(d, i) {
return 125;
})
.attr("cy", function(d, i) {
return distanceBetween * i;
})
.style("fill", function(d, i) {
return colores_google(i);
});
var text = viz.append("svg:g").selectAll("g")
.data(data1)
text
.enter().append("svg:g");
text.append("svg:text")
.attr("text-anchor", "middle")
.attr("x", "125")
.attr("y", function(d, i) {
return getRadius(d) + 15 + (distanceBetween * i);
})
.text(function(d) {
return d.commonLabel;
})
.attr("id", function(d) {
return "text" + d.commonLabel;
});
var counters = viz.append("svg:g").selectAll("g")
.data(data1)
counters
.enter().append("svg:g");
counters.append("svg:text")
.attr("text-anchor", "middle")
.attr("x", "125")
.attr("y", function(d, i) {
return ((getRadius(d) / 2) + (distanceBetween * i)) - 3;
})
.text(function(d) {
var count = d.commonTags.split(",").length;
if (count > 1) {
return count;
}
})
.attr("id", function(d) {
return "textcount" + d.commonLabel;
});
Live demo:
http://jsfiddle.net/blackmiaool/p58a0w3h/1/
First of all, you should remove all the magic numbers to keep the code clean and portable.
This one, for example:
pathStart += 125;
How to draw arcs correctly is a math problem. The code is as blow:
path.attr("d", function (d, i) {
const sx = 0;
const sy = height/2;
const a=width/2;
const b = ((data1.length-1)/2-i)*distanceBetween;
const c = Math.sqrt(a * a + b * b);
const angle=Math.atan(a/b);
let r;
if(b===0){
r=0;
}else{
r=1/2*c/Math.cos(angle);//also equals c/b*(c/2)
// r=c/b*(c/2);
}
return `M${sx},${sy} A${r},${r} 0 0,${b>0?1:0} ${width},${height/2}`;
});
And the diagram: