I have circles that is dynamically generated on my view with different sizes (d3.pack()) . Now I added a click event on it so that as it gets clicked it expands. Now, I want to elegantly reset when another circle is clicked? I did my reset similar to this answer D3 - Resetting an SVG object animation
But here's a snippet of my code
var objects= [
{ id: '1477', amounts: 7, color: '#ffd800' },
{ id: '1490', amounts: 10, color: '#b65959' },
{ id: '1300', amounts: 90, color: '#ff006e' },
{ id: '4000', amounts: 50, color: '#ffd800' },
{ id: '9000', amounts: 20, color: '#b20101' },
{ id: '1212', amounts: 28, color: '#ff006e' },
{ id: '2323', amounts: 7, color: '#ffd800' }
]
var width = 700,
height = 800,
color = d3.scale.category20b(),
maxDiameter = 500;
var container = d3.select('.chart')
var svg = container.append('svg')
.attr('width', maxDiameter * 2)
.attr('height', maxDiameter)
.attr('class', 'bubble')
var bubble = d3.layout.pack()
.sort(null)
.size([maxDiameter, maxDiameter])
.value(function (d) { return d.size; })
.padding(1.5)
var nodes = bubble
.nodes(processData(objects))
.filter(function (d) {
return !d.children;
})
var gCircle = svg.append('g')
var circle = gCircle.selectAll('circle')
.data(nodes)
.enter()
.append('circle')
.attr('transform', function (d) {
return 'translate(' + d.x + ',' + d.y + ')';
})
.attr('r', function (d) {
return d.r;
})
.attr('fill', function (d) { return d.color;})
.attr('class', function (d) { return d.className; })
// onclick
circle.on('click', function (e, i) {
d3.select(this).transition()
.attr("x", function (d) {
console.log('d x ' + d.x);
return d.x;
})
.attr("y", function (d) {
console.log('d y ' + d.y);
return d.y;
})
.attr("r", function (d) {
console.log('d r ' + d.r);
return d3.select(this).attr('r') == d.r ? (d.r * 100) : d.r;
})
.duration(500);
});
function processData(data) {
var obj = data;
var newDataSet = [];
for (var l = 0; l < obj.length; l++) {
var objInData= obj[l];
newDataSet.push({ name: objInData.id, className: objInData.id, size: objInData.amounts, color: objInData.color });
}
return { children: newDataSet };
}
Before expanding the clicked circle, set all other circles to the initial size:
circle.transition()
.duration(500)
.attr('r', function (d) {
return d.r;
});
Here is the demo:
var objects= [
{ id: '1477', amounts: 7, color: '#ffd800' },
{ id: '1490', amounts: 10, color: '#b65959' },
{ id: '1300', amounts: 90, color: '#ff006e' },
{ id: '4000', amounts: 50, color: '#ffd800' },
{ id: '9000', amounts: 20, color: '#b20101' },
{ id: '1212', amounts: 28, color: '#ff006e' },
{ id: '2323', amounts: 7, color: '#ffd800' }
]
var width = 500,
height = 400,
color = d3.scale.category20b(),
maxDiameter = 500;
var container = d3.select('body')
var svg = container.append('svg')
.attr('width', maxDiameter * 2)
.attr('height', maxDiameter)
.attr('class', 'bubble')
var bubble = d3.layout.pack()
.sort(null)
.size([maxDiameter, maxDiameter])
.value(function (d) { return d.size; })
.padding(1.5)
var nodes = bubble
.nodes(processData(objects))
.filter(function (d) {
return !d.children;
})
var gCircle = svg.append('g')
var circle = gCircle.selectAll('circle')
.data(nodes)
.enter()
.append('circle')
.attr('transform', function (d) {
return 'translate(' + d.x + ',' + d.y + ')';
})
.attr('r', function (d) {
return d.r;
})
.attr('fill', function (d) { return d.color;})
.attr('class', function (d) { return d.className; })
// onclick
circle.on('click', function (e, i) {
circle.transition().duration(500).attr('r', function (d) {
return d.r;
})
d3.select(this).transition()
.attr("x", function (d) {
return d.x;
})
.attr("y", function (d) {
return d.y;
})
.attr("r", function (d) {
return d3.select(this).attr('r') == d.r ? (d.r * 2) : d.r;
})
.duration(500);
});
function processData(data) {
var obj = data;
var newDataSet = [];
for (var l = 0; l < obj.length; l++) {
var objInData= obj[l];
newDataSet.push({ name: objInData.id, className: objInData.id, size: objInData.amounts, color: objInData.color });
}
return { children: newDataSet };
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
PS: instead of expanding to r*100, in this demo the circles are expanding to just r*2.
Related
I have created a D3 donut chart by using a code on codepen but I am unable to add the labels to it. I want the labels to be added on the side of every portion. For creating this donut chart I have used D3.js:
This is the code I have used:
<script type="text/javascript">
var dataset = [
{ name: 'Savings', count: 3250 },
{ name: 'Housing', count: 1707 },
{ name: 'Transportation', count: 377 },
{ name: 'Misc', count: 365 },
{ name: 'Insurance', count: 314 },
{ name: 'Utilities', count: 294 },
{ name: 'Student Loans', count: 262 },
{ name: 'Food', count: 250 },
{ name: 'Phone', count: 10 },
];
var total=0;
dataset.forEach(function(d){
total+= d.count;
});
var pie=d3.layout.pie()
.value(function(d){return d.count})
.sort(null);
var data_ready = pie(d3.entries(dataset))
var w=300,h=300;
var outerRadiusArc=w/2;
var innerRadiusArc=100;
var shadowWidth=10;
var outerRadiusArcShadow=innerRadiusArc+1;
var innerRadiusArcShadow=innerRadiusArc-shadowWidth;
var color = d3.scale.ordinal()
.range(['#f5e232', '#64eb34' , '#2d72e0', '#e3251b', '#d61be3', '#f0b00e', '#0ef0c3', '#e61240', '#db12e6']).domain(["Saving", "Housing", "Transportayion", "Misc", "Insurance", "Utilities", "Student Loan", "Food", "Phone"])
;
var svg=d3.select("#chart")
.append("svg")
.attr({
width:w,
height:h,
class:'shadow'
}).append('g')
.attr({
transform:'translate('+w/2+','+h/2+')'
});
var createChart=function(svg,outerRadius,innerRadius,fillFunction,className){
var arc=d3.svg.arc()
.innerRadius(outerRadius)
.outerRadius(innerRadius);
var path=svg.selectAll('.'+className)
.data(pie(dataset))
.enter()
.append('path')
.attr({
class:className,
d:arc,
fill:fillFunction
});
path.transition()
.duration(1000)
.attrTween('d', function(d) {
var interpolate = d3.interpolate({startAngle: 0, endAngle: 0}, d);
return function(t) {
return arc(interpolate(t));
};
});
};
createChart(svg,outerRadiusArc,innerRadiusArc,function(d,i){
return color(d.data.name);
},'path1');
createChart(svg,outerRadiusArcShadow,innerRadiusArcShadow,function(d,i){
var c=d3.hsl(color(d.data.name));
return d3.hsl((c.h+5), (c.s -.07), (c.l -.15));
},'path2');
var addText= function (text,y,size) {
svg.append('text')
.text(text)
.attr({
'text-anchor':'middle',
y:y
})
.style({
fill:'black',
'font-size':size,
});
};
var addTexttwo= function (text,x,y,size) {
svg.append('text')
.text(text)
.attr({
'text-anchor':'middle',
y:y,
x:x,
})
.style({
fill:'white',
'font-size':size,
});
};
var restOfTheData=function(){
addText(function(){
return "$6,830";
},40,'30px');
addText(function(){
return "Shine's";
},-20, '20px');
addText(function(){
return "Monthly Budget";
},0, '20px');
};
setTimeout(restOfTheData,1000);
function numberWithCommas(x) {
return x.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",");
}
</script>
I want the result to look somewhat like this. With the labels on the side of the donut
This code puts the labels around the pie:
const textRadius = d => outerRadiusArc + (d.data.count < 50 ? 15 : 5);
const textX = d => textRadius(d) * Math.sin((d.startAngle + d.endAngle) / 2);
const textY = d => textRadius(d) * -Math.cos((d.startAngle + d.endAngle) / 2);
const textAnchor = d => (d.startAngle + d.endAngle) / 2 > Math.PI ? 'end' : 'start';
svg.selectAll('text.label')
.data(pie(dataset))
.enter()
.append('text')
.classed('label', true)
.text(d => d.data.name)
.attr('x', textX)
.attr('y', textY)
.attr('text-anchor', textAnchor)
.attr('alignment-baseline', 'middle')
var dataset = [
{ name: 'Savings', count: 3250 },
{ name: 'Housing', count: 1707 },
{ name: 'Transportation', count: 377 },
{ name: 'Misc', count: 365 },
{ name: 'Insurance', count: 314 },
{ name: 'Utilities', count: 294 },
{ name: 'Student Loans', count: 262 },
{ name: 'Food', count: 250 },
{ name: 'Phone', count: 10 },
];
var total=0;
dataset.forEach(function(d){
total+= d.count;
});
var pie=d3.layout.pie()
.value(function(d){return d.count})
.sort(null);
var data_ready = pie(d3.entries(dataset))
var w=400,h=300;
var outerRadiusArc=120;
var innerRadiusArc=90;
var shadowWidth=10;
var outerRadiusArcShadow=innerRadiusArc+1;
var innerRadiusArcShadow=innerRadiusArc-shadowWidth;
var color = d3.scale.ordinal()
.range(['red', '#f5e232', 'orange' , '#2d72e0', '#e3251b', '#d61be3', '#f0b00e', '#0ef0c3', '#e61240', '#db12e6']).domain(["Saving", "Housing", "Transportayion", "Misc", "Insurance", "Utilities", "Student Loan", "Food", "Phone"])
;
var svg = d3.select("#chart")
.append("svg")
.attr({
width:w,
height:h,
class:'shadow'
}).append('g')
.attr({
transform:'translate('+w/2+','+h/2+')'
});
var createChart=function(svg,outerRadius,innerRadius,fillFunction,className){
var arc=d3.svg.arc()
.innerRadius(outerRadius)
.outerRadius(innerRadius);
var path=svg.selectAll('.'+className)
.data(pie(dataset))
.enter()
.append('path')
.attr({
class:className,
d:arc,
fill:fillFunction
});
path.transition()
.duration(1000)
.attrTween('d', function(d) {
var interpolate = d3.interpolate({startAngle: 0, endAngle: 0}, d);
return function(t) {
return arc(interpolate(t));
};
})
.each(d => {
console.log(d);
})
const textRadius = d => outerRadiusArc + (d.data.count < 50 ? 15 : 5);
const textX = d => textRadius(d) * Math.sin((d.startAngle + d.endAngle) / 2);
const textY = d => textRadius(d) * -Math.cos((d.startAngle + d.endAngle) / 2);
const textAnchor = d => (d.startAngle + d.endAngle) / 2 > Math.PI ? 'end' : 'start';
svg.selectAll('text.label')
.data(pie(dataset))
.enter()
.append('text')
.classed('label', true)
.text(d => d.data.name)
.attr('x', textX)
.attr('y', textY)
.attr('text-anchor', textAnchor)
.attr('alignment-baseline', 'middle')
/*
svg.selectAll('circle.point')
.data(pie(dataset))
.enter()
.append('circle')
.classed('point', true)
.attr('r', 3)
.attr('cx', textX)
.attr('cy', textY)
*/
};
createChart(svg,outerRadiusArc,innerRadiusArc,function(d,i){
return color(d.data.name);
},'path1');
createChart(svg,outerRadiusArcShadow,innerRadiusArcShadow,function(d,i){
var c=d3.hsl(color(d.data.name));
return d3.hsl((c.h+5), (c.s -.07), (c.l -.15));
},'path2');
var addText= function (text,y,size) {
svg.append('text')
.text(text)
.attr({
'text-anchor':'middle',
y:y
})
.style({
fill:'black',
'font-size':size,
});
};
var addTexttwo= function (text,x,y,size) {
svg.append('text')
.text(text)
.attr({
'text-anchor':'middle',
y:y,
x:x,
})
.style({
fill:'white',
'font-size':size,
});
};
var restOfTheData=function(){
addText(function(){
return "$6,830";
},40,'30px');
addText(function(){
return "Shine's";
},-20, '20px');
addText(function(){
return "Monthly Budget";
},0, '20px');
};
setTimeout(restOfTheData,1000);
function numberWithCommas(x) {
return x.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",");
}
.label {
font-family: 'Ubuntu';
font-size: 10px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.17/d3.min.js"></script>
<div id="chart" />
I have the following d3 code:
import React, { Component } from 'react';
import ReactDOM from 'react-dom';
import PropTypes from 'prop-types';
import * as d3 from 'd3';
import tip from 'd3';
export default class BubbleChart extends Component {
constructor(props){
super(props);
this.renderChart = this.renderChart.bind(this);
this.renderBubbles = this.renderBubbles.bind(this);
this.renderLegend = this.renderLegend.bind(this);
}
componentDidMount() {
this.svg = ReactDOM.findDOMNode(this);
this.renderChart();
}
componentDidUpdate() {
const {
width,
height,
} = this.props;
if(width !== 0 && height !== 0) {
this.renderChart();
}
}
render() {
const {
width,
height,
} = this.props;
return (
<svg width={width} height={height} />
)
}
renderChart() {
const {
overflow,
graph,
data,
height,
width,
padding,
showLegend,
legendPercentage,
} = this.props;
// Reset the svg element to a empty state.
this.svg.innerHTML = '';
// Allow bubbles overflowing its SVG container in visual aspect if props(overflow) is true.
if(overflow)
this.svg.style.overflow = "visible";
const bubblesWidth = showLegend ? width * (1 - (legendPercentage / 100)) : width;
const legendWidth = width - bubblesWidth;
const color = d3.scaleOrdinal(d3.schemeCategory20c);
const pack = d3.pack()
.size([bubblesWidth * graph.zoom, bubblesWidth * graph.zoom])
.padding(padding);
// Process the data to have a hierarchy structure;
const root = d3.hierarchy({children: data})
.sum(function(d) { return d.value; })
.sort(function(a, b) { return b.value - a.value; })
.each((d) => {
if(d.data.label) {
d.label = d.data.label;
d.id = d.data.label.toLowerCase().replace(/ |\//g, "-");
}
});
// Pass the data to the pack layout to calculate the distribution.
const nodes = pack(root).leaves();
// Call to the function that draw the bubbles.
this.renderBubbles(bubblesWidth, nodes, color);
// Call to the function that draw the legend.
if (showLegend) {
this.renderLegend(legendWidth, height, bubblesWidth, nodes, color);
}
}
renderBubbles(width, nodes, color) {
const {
graph,
data,
bubbleClickFun,
valueFont,
labelFont
} = this.props;
const bubbleChart = d3.select(this.svg).append("g")
.attr("class", "bubble-chart")
.attr("transform", function(d) { return "translate(" + (width * graph.offsetX) + "," + (width * graph.offsetY) + ")"; });;
const node = bubbleChart.selectAll(".node")
.data(nodes)
.enter().append("g")
.attr("class", "node")
.attr("transform", function(d) { return "translate(" + d.x + "," + d.y + ")"; })
.on("click", function(d) {
bubbleClickFun(d.label);
});
node.append("circle")
.attr("id", function(d) { return d.id; })
.attr("r", function(d) { return d.r - (d.r * .04); })
.style("fill", function(d) { return d.data.color ? d.data.color : color(nodes.indexOf(d)); })
.style("z-index", 5)
.on('mouseover', function(d) {
d3.select(this).attr("r", d.r * 2.04);
})
.on('mouseout', function(d) {
const r = d.r - (d.r * 0.04);
d3.select(this).attr("r", r);
});
node.append("clipPath")
.attr("id", function(d) { return "clip-" + d.id; })
.append("use")
.attr("xlink:href", function(d) { return "#" + d.id; });
// node.append("text")
// .attr("class", "value-text")
// .style("font-size", `${valueFont.size}px`)
// .attr("clip-path", function(d) { return "url(#clip-" + d.id + ")"; })
// .style("font-weight", (d) => {
// return valueFont.weight ? valueFont.weight : 600;
// })
// .style("font-family", valueFont.family)
// .style("fill", () => {
// return valueFont.color ? valueFont.color : '#000';
// })
// .style("stroke", () => {
// return valueFont.lineColor ? valueFont.lineColor : '#000';
// })
// .style("stroke-width", () => {
// return valueFont.lineWeight ? valueFont.lineWeight : 0;
// })
// .text(function(d) { return d.value; });
node.append("text")
.attr("class", "label-text")
.style("font-size", `${labelFont.size}px`)
.attr("clip-path", function(d) { return "url(#clip-" + d.id + ")"; })
.style("font-weight", (d) => {
return labelFont.weight ? labelFont.weight : 600;
})
.style("font-family", labelFont.family)
.style("fill", () => {
return labelFont.color ? labelFont.color : '#000';
})
.style("stroke", () => {
return labelFont.lineColor ? labelFont.lineColor : '#000';
})
.style("stroke-width", () => {
return labelFont.lineWeight ? labelFont.lineWeight : 0;
})
.text(function(d) {
return d.label;
});
// Center the texts inside the circles.
d3.selectAll(".label-text").attr("x", function(d) {
const self = d3.select(this);
const width = self.node().getBBox().width;
return -(width/2);
})
.style("opacity", function(d) {
const self = d3.select(this);
const width = self.node().getBBox().width;
d.hideLabel = width*1.05 > (d.r*2);
return d.hideLabel ? 0 : 1;
})
.attr("y", function(d) {
return labelFont.size/2
})
// Center the texts inside the circles.
d3.selectAll(".label-text").attr("x", function(d) {
const self = d3.select(this);
const width = self.node().getBBox().width;
return -(width/2);
})
.attr("y", function(d) {
if (!d.hideLabel) {
return valueFont.size / 3;
} else {
return -valueFont.size * 0.5;
}
});
node.append("title")
.text(function(d) { return d.label; });
}
renderLegend(width, height, offset, nodes, color) {
const {
data,
legendClickFun,
legendFont,
} = this.props;
const bubble = d3.select('.bubble-chart');
const bubbleHeight = bubble.node().getBBox().height;
const legend = d3.select(this.svg).append("g")
.attr("transform", function() { return `translate(${offset},${(bubbleHeight)* 0.05})`; })
.attr("class", "legend");
let textOffset = 0;
const texts = legend.selectAll(".legend-text")
.data(nodes)
.enter()
.append("g")
.attr("transform", (d, i) => {
const offset = textOffset;
textOffset+= legendFont.size + 10;
return `translate(0,${offset})`;
})
.on('mouseover', function(d) {
d3.select('#' + d.id).attr("r", d.r * 1.04);
})
.on('mouseout', function(d) {
const r = d.r - (d.r * 0.04);
d3.select('#' + d.id).attr("r", r);
})
.on("click", function(d) {
legendClickFun(d.label);
});;
texts.append("rect")
.attr("width", 30)
.attr("height", legendFont.size)
.attr("x", 0)
.attr("y", -legendFont.size)
.style("fill", "transparent");
texts.append("rect")
.attr("width", legendFont.size)
.attr("height", legendFont.size)
.attr("x", 0)
.attr("y", -legendFont.size)
.style("fill", function(d) { return d.data.color ? d.data.color : color(nodes.indexOf(d)); });
texts.append("text")
.style("font-size", `${legendFont.size}px`)
.style("font-weight", (d) => {
return legendFont.weight ? legendFont.weight : 600;
})
.style("font-family", legendFont.family)
.style("fill", () => {
return legendFont.color ? legendFont.color : '#000';
})
.style("stroke", () => {
return legendFont.lineColor ? legendFont.lineColor : '#000';
})
.style("stroke-width", () => {
return legendFont.lineWeight ? legendFont.lineWeight : 0;
})
.attr("x", (d) => { return legendFont.size + 10 })
.attr("y", 0)
.text((d) => { return d.label });
}
}
BubbleChart.propTypes = {
overflow: PropTypes.bool,
graph: PropTypes.shape({
zoom: PropTypes.number,
offsetX: PropTypes.number,
offsetY: PropTypes.number,
}),
width: PropTypes.number,
height: PropTypes.number,
padding: PropTypes.number,
showLegend: PropTypes.bool,
legendPercentage: PropTypes.number,
legendFont: PropTypes.shape({
family: PropTypes.string,
size: PropTypes.number,
color: PropTypes.string,
weight: PropTypes.string,
}),
valueFont: PropTypes.shape({
family: PropTypes.string,
size: PropTypes.number,
color: PropTypes.string,
weight: PropTypes.string,
}),
labelFont: PropTypes.shape({
family: PropTypes.string,
size: PropTypes.number,
color: PropTypes.string,
weight: PropTypes.string,
}),
}
BubbleChart.defaultProps = {
overflow: false,
graph: {
zoom: 1.1,
offsetX: -0.05,
offsetY: -0.01,
},
width: 1000,
height: 800,
padding: 0,
showLegend: true,
legendPercentage: 20,
legendFont: {
family: 'Arial',
size: 12,
color: '#000',
weight: 'bold',
},
valueFont: {
family: 'Arial',
size: 16,
color: '#fff',
weight: 'bold',
},
labelFont: {
family: 'Arial',
size: 11,
color: '#fff',
weight: 'normal',
},
bubbleClickFun: (label) => {console.log(`Bubble ${label} is clicked ...`)
},
legendClickFun: (label) => {console.log(`Legend ${label} is clicked ...`)}
}
when I click on the bubbleClickFun,I want a popover to be displayed with "label-text" attribute.I am using d3 inside a React component.Also a modal or tooltip works too.Any popover or panel kind of thing,that pops up as soon as the bubble is clicked works fine.
Also,I would be able to change the size of popover.This is really important
Current Behavior: All the nodes are in random colors.
Expected Behavior: All the parent nodes should be in same colors(blue for example) and all the child nodes should in same colors(light blue).
How to achieve this ?
Here is the working jsfiddle: Fiddle
var color = d3.scaleOrdinal(d3.schemeCategory10);
grads = svg.append("defs").selectAll("radialGradient")
.data(graph.nodes)
.enter()
.append("radialGradient")
.attr("gradientUnits", "objectBoundingBox")
.attr("cx", 0)
.attr("fill", function(d) { return color(d.id); })
.attr("cy", 0)
.attr("r", "100%")
.attr("id", function(d, i) { return "grad" + i; });
grads.append("stop")
.attr("offset", "0%")
.style("stop-color", "white");
grads.append("stop")
.attr("offset", "100%")
.style("stop-color", function(d) { return color(d.id); });
nodeElements = g.append("g")
.attr("class", "nodes")
.selectAll("circle")
.data(graph.nodes)
.enter().append("circle")
.attr("r", 60)
.attr("stroke", "#fff")
.attr('stroke-width', 21)
.attr("id", function(d) { return d.id })
//.attr("fill", function(d) {return color(d.id)})
.attr('fill', function(d, i) { return 'url(#grad' + i + ')'; })
Then you need to change the color based on the group. Change this line as follow:
.attr("fill", function(d) {return color(d.group)})
Please find the working code below:
var graph = {
'nodes': [{
'id': 'Material_Definition',
'group': 0
},
{
'id': 'Item1',
'group': 1
},
{
'id': 'Item2',
'group': 1
},
{
'id': 'Item3',
'group': 1
},
{
'id': 'Item4',
'group': 1
},
{
'id': 'Item5',
'group': 1
},
{
'id': 'SubItem1_Item1',
'group': 2
},
{
'id': 'SubItem2_Item1',
'group': 2
}
],
'links': [
/* Material Definition linked to Items */
{
'source': 'Material_Definition',
'target': 'Item1',
'value': 1,
'type': 'A'
},
{
'source': 'Material_Definition',
'target': 'Item2',
'value': 8,
'type': 'A'
},
{
'source': 'Material_Definition',
'target': 'Item3',
'value': 10,
'type': 'A'
},
{
'source': 'Material_Definition',
'target': 'Item3',
'value': 1,
'type': 'A'
},
{
'source': 'Material_Definition',
'target': 'Item4',
'value': 1,
'type': 'A'
},
{
'source': 'Material_Definition',
'target': 'Item5',
'value': 1,
'type': 'A'
},
/* Item1 is linked to SubItems */
{
'source': 'Item1',
'target': 'SubItem1_Item1',
'value': 2,
'type': 'A'
},
{
'source': 'Item1',
'target': 'SubItem2_Item1',
'value': 1,
'type': 'A'
},
/* Interconnected Items */
{
'source': 'Item5',
'target': 'Item4',
'value': 2,
'type': 'A'
},
{
'source': 'Item2',
'target': 'Item3',
'value': 1,
'type': 'A'
},
]
};
var svg = d3.select("svg"),
width = +svg.attr("width"),
height = +svg.attr("height")
var store = Object.assign({}, graph);
var color = d3.scaleOrdinal(d3.schemeCategory10);
var zoom_handler = d3.zoom().on("zoom", zoom_actions);
var linkElements, nodeElements, textElements, grads;
// zoom_handler(svg);
var simulation = d3.forceSimulation()
.force("link", d3.forceLink().distance(700).id(function(d) {
return d.id;
}))
.force("charge", d3.forceManyBody().strength(-1000))
.force("center", d3.forceCenter(width / 2, height / 2));
var g = svg.append("g")
.attr("class", "everything");
svg.call(zoom_handler)
.call(zoom_handler.transform, d3.zoomIdentity.translate(200, 150).scale(0.2));
linkElements = svg.append("g").selectAll(".link"),
nodeElements = svg.append("g").selectAll(".nodes");
grads = svg.append("g").selectAll(".grads");
textElements = svg.append("g").selectAll(".texts");
drawGraph();
function drawGraph() {
// empty current Graph contents
g.html('')
grads = svg.append("defs").selectAll("radialGradient")
.data(graph.nodes)
.enter()
.append("radialGradient")
.attr("gradientUnits", "objectBoundingBox")
.attr("cx", 0)
.attr("fill", function(d) {
return color(d.group);
})
.attr("cy", 0)
.attr("r", "100%")
.attr("id", function(d, i) {
return "grad" + i;
});
grads.append("stop")
.attr("offset", "0%")
.style("stop-color", "white");
grads.append("stop")
.attr("offset", "100%")
.style("stop-color", function(d) {
return color(d.id);
});
linkElements = g.append("g")
.attr("class", "links")
.selectAll("line")
.data(graph.links)
.enter().append("line")
.style("stroke-width", 5.5)
.style("stroke", "grey")
nodeElements = g.append("g")
.attr("class", "nodes")
.selectAll("circle")
.data(graph.nodes)
.enter().append("circle")
.attr("r", 60)
.attr("stroke", "#fff")
.attr('stroke-width', 21)
.attr("id", function(d) {
return d.id
})
.attr("fill", function(d) {
return color(d.group)
})
/* .attr('fill', function(d, i) { return 'url(#grad' + i + ')'; }) */
.on('contextmenu', function(d) {
d3.event.preventDefault();
menu(d3.mouse(svg.node())[0], d3.mouse(svg.node())[1]);
})
.on('mouseover', selectNode)
.on('mouseout', releaseNode)
.call(d3.drag()
.on("start", dragstarted)
.on("drag", dragged)
.on("end", dragended));
textElements = g.append("g") // use g.append instead of svg.append to enable zoom
.attr("class", "texts")
.selectAll("text")
.data(graph.nodes)
.enter().append("text")
.attr("text-anchor", "end")
.text(function(node) {
return node.id
})
.attr("font-size", 55)
.attr("font-family", "sans-serif")
.attr("fill", "black")
.attr("style", "font-weight:bold;")
.attr("dx", 30)
.attr("dy", 80)
}
function ticked() {
linkElements
.attr("x1", function(d) {
return d.source.x;
})
.attr("y1", function(d) {
return d.source.y;
})
.attr("x2", function(d) {
return d.target.x;
})
.attr("y2", function(d) {
return d.target.y;
});
nodeElements
.attr("cx", function(d) {
return d.x;
})
.attr("cy", function(d) {
return d.y;
})
.each(d => {
d3.select('#t_' + d.id).attr('x', d.x + 10).attr('y', d.y + 3);
});
textElements
.attr('x', function(d) {
return d.x
})
.attr('y', function(d) {
return d.y
});
}
simulation
.nodes(graph.nodes)
.on("tick", ticked);
simulation.force("link")
.links(graph.links);
function zoom_actions() {
g.attr("transform", d3.event.transform)
}
function dragstarted(d) {
if (!d3.event.active) simulation.alphaTarget(0.3).restart();
d.fx = d.x;
d.fy = d.y;
}
function dragged(d) {
d.fx = d3.event.x;
d.fy = d3.event.y;
}
function dragended(d) {
if (!d3.event.active) simulation.alphaTarget(0);
d.fx = null;
d.fy = null;
}
function selectNode(selectedNode) {
var neighbors = getNeighbors(selectedNode)
nodeElements.transition().duration(500)
.attr('r', function(node) {
return getNodeRadius(node, neighbors);
});
nodeElements.attr('fill', function(node) {
return getNodeColor(node, neighbors, selectedNode);
})
textElements.transition().duration(500).style('font-size', function(node) {
return getTextColor(node, neighbors)
})
}
function releaseNode() {
nodeElements.transition().duration(500)
.attr('r', 60);
nodeElements.attr('fill', function(d, i) {
return 'url(#grad' + i + ')';
})
linkElements.style('stroke', 'grey');
}
function getNeighbors(node) {
return graph.links.reduce(function(neighbors, link) {
if (link.target.id === node.id) {
neighbors.push(link.source.id)
} else if (link.source.id === node.id) {
neighbors.push(link.target.id)
}
return neighbors
}, [node.id])
}
function getNodeColor(node, neighbors, selectedNode) {
// If is neighbor
if (Array.isArray(neighbors) && neighbors.indexOf(node.id) > -1) {
return 'url(#grad' + selectedNode.index + ')'
// return node.level === 1 ? '#9C4A9C' : 'rgba(251, 130, 30, 1)'
} else {
return 'url(#grad' + node.index + ')'
}
//return node.level === 0 ? '#91007B' : '#D8ABD8'
}
function getNodeRadius(node, neighbors) {
// If is neighbor
if (neighbors.indexOf(node.id) > -1) {
return '100'
} else {
return '60'
}
}
function isNeighborLink(node, link) {
return link.target.id === node.id || link.source.id === node.id
}
function getTextColor(node, neighbors) {
return Array.isArray(neighbors) && neighbors.indexOf(node.id) > -1 ? '40px' : '25px'
}
var filter = document.querySelector('#filter');
filter.addEventListener('change', function(event) {
filterData(event.target.value);
drawGraph();
})
var resetFilter = document.querySelector('#reset');
resetFilter.addEventListener('click', function(event) {
drawGraph();
})
function filterData(id) {
graph = Object.assign({}, store);
graph.nodes = [];
graph.links = [];
dummyStore = [id];
store.links.forEach(function(link) {
if (link.source.id === id) {
graph.links.push(link);
dummyStore.push(link.target.id);
} else if (link.target.id === id) {
graph.links.push(link);
dummyStore.push(link.source.id)
}
});
store.nodes.forEach(function(node) {
if (dummyStore.includes(node.id)) {
graph.nodes.push(node);
}
})
}
.links line {
stroke: #999;
stroke-opacity: 0.6;
}
.nodes circle {
stroke: #000;
stroke-width: 1.5px;
}
text {
font-size: 10px;
}
<script src="https://d3js.org/d3.v4.min.js"></script>
Filter: <input type="text" name="filter" id="filter" />
<button id='reset'>Reset Filter</button>
<svg width="798" height="400"></svg>
I'm currently fiddling with polygons in d3 and would like to update individual polygons whilst the user is dragging a point. Drawing them initially works fine, but I can't get the update to work. The fiddle below contains my awful attempt at getting it to work:
https://jsfiddle.net/z4g5817z/9/
Relevant code:
const areas = [{
uid: 'ajf9v0',
points: [{
x: 52,
y: 92
},
{
x: 50,
y: 151
},
{
x: 123,
y: 149
},
{
x: 125,
y: 91
}
],
foo: 'bar',
// ...
},
{
uid: 'ufnf12',
points: [{
x: 350,
y: 250
},
{
x: 450,
y: 250
},
{
x: 450,
y: 275
},
{
x: 350,
y: 275
}
],
foo: 'baz',
// ...
}
];
const svg = d3.select('#root');
svg.attr('width', 500)
.attr('height', 500);
const areasGroup = svg.append('g')
.attr('class', 'areas');
function drawAreas(areas) {
console.log('Called draw');
const self = this;
const aGroup = areasGroup.selectAll('g.area')
.data(areas, (d) => {
console.log('Areas', d.points.map((d) => [d.x, d.y].join('#')).join('#'));
return d.points.map((d) => [d.x, d.y].join('#')).join('#');
});
areasGroup.exit().remove();
const areaGroups = aGroup.enter()
.append('g')
.attr('class', 'area');
//const areaPolygon = area.append('g')
// .attr('class', 'polygon');
//const areaPoints = area.append('g')
// .attr('class', 'points');
const polygon = areaGroups.selectAll('polygon')
.data((d) => {
console.log('Polygon data points', [d.points]);
return [d.points];
}, (d) => {
console.log('Polygon key', d.map((d) => [d.x, d.y].join('#')).join('#'));
return d.map((d) => [d.x, d.y].join('#')).join('#');
});
polygon.enter()
.append('polygon')
.merge(polygon)
.attr('points', (d) => {
console.log('Polygon points', d);
return d.map((d) => [d.x, d.y].join(',')).join(' ');
})
.attr('stroke', '#007bff')
.attr('stroke-width', 1)
.attr('fill', '#007bff')
.attr('fill-opacity', 0.25)
.on('click', this.handlePolygonSelection)
polygon.exit().remove();
const circles = areaGroups.selectAll('circle')
.data((d) => d.points, (d) => d.x + '#' + d.y);
circles.enter()
.append('circle')
.attr('r', 4)
.attr('cx', (d) => d.x)
.attr('cy', (d) => d.y)
.attr('fill', '#007bff')
.on('click', (d, idx, j) => {
const parentArea = d3.select(j[idx].parentNode).datum().points;
const i = parentArea.findIndex((p) => p.x === d.x && p.y === d.y);
if (i === parentArea.length) {
parentArea.pop();
} else if (i === 0) {
parentArea.shift();
} else {
parentArea.splice(i, 1);
}
this.drawAreas(areas);
})
.call(d3.drag()
.on('start', function(d) {
d3.select(this).classed('active', true)
})
.on('drag', function(d) {
d3.select(this)
.attr('cx', d.x = d3.event.x)
.attr('cy', d.y = d3.event.y);
self.drawAreas(areas);
})
.on('end', function(d) {
d3.select(this).classed('active', false)
}));
circles.exit().remove();
}
this.drawAreas(areas);
Thank you to anybody who takes time to have a look, any and all help is appreciated.
So it looks like I found the issue: https://jsfiddle.net/z4g5817z/91/
Changing
const polygon = areaGroups.selectAll('polygon')
to
const polygon = areasGroup.selectAll('g.area').selectAll('polygon')
seems to have fixed it. I'm assuming this has to do with the areaGroups selection only handling enter events.
An alternative would be to keep it the way it is now and change
const areaGroups = aGroup.enter()
.append('g')
.attr('class', 'area');
to
const areaGroups = aGroup.enter()
.append('g')
.merge(aGroup)
.attr('class', 'area');
which will produce the same result, as the update event is now also handled appropriately.
I am trying to compose a D3 pie component in each node of a tree.
I am able to build separately the tree and one pie, but I couldn't figure out how to compose them.
Basically, I have the following json data:
window.json = {
"health": [{
"value": 60
}, {
"value": 10
}, {
"value": 30
}],
"color": orange,
"children": [{
"health": [{
"value": 60
}, {
"value": 20
}, {
"value": 20
}],
"color": green
}, {
"health": [{
"value": 40
}, {
"value": 30
}, {
"value": 30
}],
"color": orange
}]
};
It represents the tree. Each node contains data for a pie: it's the "health" properties.
I've build the tree here: http://jsfiddle.net/4srt30pj/4/
I can build a single pie: http://jsfiddle.net/4srt30pj/5/
But I can't see how to mix them together, so that each node shows a pie. I've tried to create a function that draws a pie component:
function drawPie(selection, node) {
selection.data(node, function(d, i) {
console.log(node);
console.log(d);
console.log(i);
return pie(d.health);
})
.enter()
.append('path')
.attr('d', arc)
.attr('fill', function (d, i) {
return color(i);
});
}
Then call it for each tree nodes:
drawPie(vis.selectAll("g.node"), nodes);
(the code is there: http://jsfiddle.net/4srt30pj/6/ )
But it doesn't show the pies.
Is it possible to achieve this composition?
You are close. Try:
function drawPie(d) {
d3.select(this)
.selectAll('path')
.data(pie(d.health))
.enter()
.append('path')
.attr('d', arc)
.attr('fill', function(d, i) {
return color(i);
});
}
nodeEnter.each(drawPie);
Full working sample:
<!DOCTYPE html>
<html>
<head>
<script data-require="d3#3.5.3" data-semver="3.5.3" src="//cdnjs.cloudflare.com/ajax/libs/d3/3.5.3/d3.js"></script>
<style>
path.link {
fill: none;
stroke-width: 5px;
}
svg text {
font-family: Roboto, Arial;
}
.selected {
display: none;
}
</style>
</head>
<body>
<script>
var red = "#f5696d";
var green = "#40bc96";
var orange = "#fabd57";
window.json = {
"health": [{
"value": 60
}, {
"value": 10
}, {
"value": 30
}],
"color": orange,
"children": [{
"health": [{
"value": 60
}, {
"value": 20
}, {
"value": 20
}],
"color": green
}, {
"health": [{
"value": 40
}, {
"value": 30
}, {
"value": 30
}],
"color": orange
}]
};
var w = 100;
var h = 60;
var i = 0;
var root;
var tree = d3.layout.tree()
.nodeSize([w + 10, h + 20])
.separation(function(a, b) {
return (a.parent == b.parent ? 1 : 1.5);
});
var diagonal = d3.svg.diagonal()
.projection(function(d) {
return [d.x, d.y];
});
var vis = d3.select("body").append("svg:svg")
.attr("width", 500)
.attr("height", 500)
.append("svg:g")
.attr("transform", "translate(" + 250 + "," + 30 + ")");
root = window.json;
root.x0 = 0;
root.y0 = 0;
function toggleAll(d) {
if (d.children) {
d.children.forEach(toggleAll);
toggle(d);
}
}
var arc = d3.svg.arc()
.outerRadius(30)
.innerRadius(0);
var pie = d3.layout.pie()
.value(function(d) {
return d.value;
})
.sort(null);
var color = d3.scale.ordinal()
.range(['#40bc96', '#fabd57', '#f5696d']);
function drawPie(d) {
d3.select(this)
.selectAll('path')
.data(pie(d.health))
.enter()
.append('path')
.attr('d', arc)
.attr('fill', function(d, i) {
return color(i);
});
}
update(root);
function update(source) {
var duration = d3.event && d3.event.altKey ? 5000 : 500;
// Compute the new tree layout.
var nodes = tree.nodes(root).reverse();
// Update the nodes…
var node = vis.selectAll("g.node")
.data(nodes, function(d) {
return d.id || (d.id = ++i);
});
// Enter any new nodes at the parent's previous position.
var nodeEnter = node.enter().append("svg:g")
.attr("class", "node")
.attr("transform", function(d) {
return "translate(" + source.x0 + "," + source.y0 + ")";
});
nodeEnter
.each(drawPie);
// Transition nodes to their new position.
var nodeUpdate = node.transition()
.duration(duration)
.attr("transform", function(d) {
return "translate(" + d.x + "," + d.y + ")";
});
// Update the links…
var link = vis.selectAll("path.link")
.data(tree.links(nodes), function(d) {
return d.target.id;
});
// Enter any new links at the parent's previous position.
link.enter().insert("svg:path", "g")
.attr("class", "link")
.style("stroke-opacity", 0.4)
.style("stroke", function(d) {
return d.target.color;
})
.attr("d", function(d) {
var o = {
x: source.x0,
y: source.y0
};
return diagonal({
source: o,
target: o
});
})
.transition()
.duration(duration)
.attr("d", diagonal);
// Stash the old positions for transition.
nodes.forEach(function(d) {
d.x0 = d.x;
d.y0 = d.y;
});
}
</script>
</body>
</html>