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
Related
I have a List of items that are inside a circle. I am using hardcoded values for the alignment. I need it to be based off the central point of the circle and by the length of the array.
Need to get rid of these "yAxis: -40, yAxis: -40, yAxis: 0, yAxis: 20";
And also have some space between line items.
const w = 500,
h = 400,
r = 160;
const STREAMS = [{
label: 'Emissions',
isSelected: true,
yAxis: -40
}, {
label: 'Energy Produced',
isSelected: false,
yAxis: -20
}, {
label: 'Energy Consumed',
isSelected: false,
yAxis: 0
}, {
label: 'Intensity',
isSelected: false,
yAxis: 20
}]
const SUB_STREAMS = [{
value: 0.15,
label: 'Total',
isSelected: true
}, {
value: 0.2,
label: 'CO2',
isSelected: false
}, {
value: 0.25,
label: 'Methane',
isSelected: false
}, {
value: 0.30,
label: 'N2O',
isSelected: false
}, {
value: 0.35,
label: 'Other',
isSelected: false
}];
const svg = d3.select("#foo")
.append("svg")
.attr("width", w)
.attr("height", h);
const g = svg.append("g")
.attr("transform", "translate(" + [w / 2, h / 2] + ")");
g.append("circle")
.attr("r", r)
.style("fill", "none")
.style("stroke", "black");
const points = g.selectAll(null)
.data(SUB_STREAMS)
.enter()
.append("circle")
.attr('stroke', 'dodgerblue')
.attr('stroke-width', 1)
.style("fill", function(d) {
return d.isSelected ? 'dodgerblue' : 'white'
})
.attr("r", 12)
.attr("cx", function(d) {
return r * Math.cos(d.value * Math.PI * 2 - Math.PI / 2)
})
.attr("cy", function(d) {
return r * Math.sin(d.value * Math.PI * 2 - Math.PI / 2)
})
points.on("click", function(d) {
console.log(d)
})
g.selectAll(null)
.data(SUB_STREAMS)
.enter()
.append('text')
.style('cursor', 'pointer')
.style('fill', 'black')
.attr('text-anchor', 'right')
.attr('font-size', '1.3em')
.attr('dx', (d) => 14 + r * Math.cos(d.value * Math.PI * 2 - Math.PI / 2))
.attr('dy', (d) => r * Math.sin(d.value * Math.PI * 2 - Math.PI / 2))
.text((d) => d.label)
const text = g
.selectAll('path')
.data(STREAMS)
.enter()
.append("text")
.attr("text-anchor", "left")
.attr('font-size', '1em')
.attr("y", function(d, a) {
return d.yAxis - 5
})
.text((d) => d.label);
text.on("click", function(d) {
console.log(d)
})
var arc = d3.symbol().type(d3.symbolTriangle)
var line = g.selectAll('path')
.data(STREAMS)
.enter()
.append('path')
.attr('d', arc)
.attr('fill', 'red')
.attr('stroke', '#000')
.attr('stroke-width', 1)
.attr('transform', function(d) {
return `translate(-10,${d.yAxis - 5}) rotate(210)`;
});
text {
dominant-baseline: central;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<div id="foo" />
One solution out of many is setting a padding...
const padding = 20
...and translating the container groups by their indices times that padding:
const groups = g
.selectAll('path')
.data(STREAMS)
.enter()
.append("g")
.attr("transform", (_, i) => "translate(0," +
(-padding * (STREAMS.length - 1) / 2 + i * padding) + ")");
Then, you append both texts and paths to those groups.
Here is your code with those changes:
const w = 500,
h = 400,
r = 160,
padding = 20;
const STREAMS = [{
label: 'Emissions',
isSelected: true
}, {
label: 'Energy Produced',
isSelected: false
}, {
label: 'Energy Consumed',
isSelected: false
}, {
label: 'Intensity',
isSelected: false
}]
const SUB_STREAMS = [{
value: 0.15,
label: 'Total',
isSelected: true
}, {
value: 0.2,
label: 'CO2',
isSelected: false
}, {
value: 0.25,
label: 'Methane',
isSelected: false
}, {
value: 0.30,
label: 'N2O',
isSelected: false
}, {
value: 0.35,
label: 'Other',
isSelected: false
}];
const svg = d3.select("#foo")
.append("svg")
.attr("width", w)
.attr("height", h);
const g = svg.append("g")
.attr("transform", "translate(" + [w / 2, h / 2] + ")");
g.append("circle")
.attr("r", r)
.style("fill", "none")
.style("stroke", "black");
const points = g.selectAll(null)
.data(SUB_STREAMS)
.enter()
.append("circle")
.attr('stroke', 'dodgerblue')
.attr('stroke-width', 1)
.style("fill", function(d) {
return d.isSelected ? 'dodgerblue' : 'white'
})
.attr("r", 12)
.attr("cx", function(d) {
return r * Math.cos(d.value * Math.PI * 2 - Math.PI / 2)
})
.attr("cy", function(d) {
return r * Math.sin(d.value * Math.PI * 2 - Math.PI / 2)
})
points.on("click", function(d) {
console.log(d)
})
g.selectAll(null)
.data(SUB_STREAMS)
.enter()
.append('text')
.style('cursor', 'pointer')
.style('fill', 'black')
.attr('text-anchor', 'right')
.attr('font-size', '1.3em')
.attr('dx', (d) => 14 + r * Math.cos(d.value * Math.PI * 2 - Math.PI / 2))
.attr('dy', (d) => r * Math.sin(d.value * Math.PI * 2 - Math.PI / 2))
.text((d) => d.label)
const groups = g
.selectAll('path')
.data(STREAMS)
.enter()
.append("g")
.attr("transform", (_, i) => "translate(0," + (-padding * (STREAMS.length - 1) / 2 + i * padding) + ")");
groups.append("text")
.attr('font-size', '1em')
.text((d) => d.label)
.on("click", function(d) {
console.log(d)
})
var arc = d3.symbol().type(d3.symbolTriangle)
groups.append('path')
.attr('d', arc)
.attr('fill', 'red')
.attr('stroke', '#000')
.attr('stroke-width', 1)
.attr('transform', function(d) {
return "translate(-10,0) rotate(210)";
});
text {
dominant-baseline: central;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<div id="foo" />
And here the same code, with a bigger data array, so you can see that it dynamically sets the positions according to the number of elements:
const w = 500,
h = 400,
r = 160,
padding = 20;
const STREAMS = [{
label: 'Emissions',
isSelected: true
}, {
label: 'Energy Produced',
isSelected: false
}, {
label: 'Energy Consumed',
isSelected: false
}, {
label: 'Intensity',
isSelected: false
}, {
label: 'Foo',
isSelected: false
}, {
label: 'Bar',
isSelected: false
}, {
label: 'Baz',
isSelected: false
}]
const SUB_STREAMS = [{
value: 0.15,
label: 'Total',
isSelected: true
}, {
value: 0.2,
label: 'CO2',
isSelected: false
}, {
value: 0.25,
label: 'Methane',
isSelected: false
}, {
value: 0.30,
label: 'N2O',
isSelected: false
}, {
value: 0.35,
label: 'Other',
isSelected: false
}];
const svg = d3.select("#foo")
.append("svg")
.attr("width", w)
.attr("height", h);
const g = svg.append("g")
.attr("transform", "translate(" + [w / 2, h / 2] + ")");
g.append("circle")
.attr("r", r)
.style("fill", "none")
.style("stroke", "black");
const points = g.selectAll(null)
.data(SUB_STREAMS)
.enter()
.append("circle")
.attr('stroke', 'dodgerblue')
.attr('stroke-width', 1)
.style("fill", function(d) {
return d.isSelected ? 'dodgerblue' : 'white'
})
.attr("r", 12)
.attr("cx", function(d) {
return r * Math.cos(d.value * Math.PI * 2 - Math.PI / 2)
})
.attr("cy", function(d) {
return r * Math.sin(d.value * Math.PI * 2 - Math.PI / 2)
})
points.on("click", function(d) {
console.log(d)
})
g.selectAll(null)
.data(SUB_STREAMS)
.enter()
.append('text')
.style('cursor', 'pointer')
.style('fill', 'black')
.attr('text-anchor', 'right')
.attr('font-size', '1.3em')
.attr('dx', (d) => 14 + r * Math.cos(d.value * Math.PI * 2 - Math.PI / 2))
.attr('dy', (d) => r * Math.sin(d.value * Math.PI * 2 - Math.PI / 2))
.text((d) => d.label)
const groups = g
.selectAll('path')
.data(STREAMS)
.enter()
.append("g")
.attr("transform", (_, i) => "translate(0," + (-padding * (STREAMS.length - 1) / 2 + i * padding) + ")");
groups.append("text")
.attr('font-size', '1em')
.text((d) => d.label)
.on("click", function(d) {
console.log(d)
})
var arc = d3.symbol().type(d3.symbolTriangle)
groups.append('path')
.attr('d', arc)
.attr('fill', 'red')
.attr('stroke', '#000')
.attr('stroke-width', 1)
.attr('transform', function(d) {
return "translate(-10,0) rotate(210)";
});
text {
dominant-baseline: central;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<div id="foo" />
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.
This code works perfectly with a JSON file where the source and index are in the form of indices. However, when I switch to a format with the source and target as strings, it throws up TypeError: e[u.source.index] is undefined. How do I overcome this?
<!DOCTYPE html>
<meta charset="utf-8">
<body>
<script src="https://d3js.org/d3.v3.min.js"></script>
<script>
var width = 960,
height = 500,
active = d3.select(null);
var zoom = d3.behavior.zoom()
.scaleExtent([1, 8])
.on("zoom", zoomed);
var force = d3.layout.force()
.size([width, height])
.charge(-400)
.linkDistance(40)
.on("tick", tick);
var drag = force.drag()
.on("dragstart", dragstart);
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height)
// .on("click", reset);
var g = svg.append("g");
var link = g.selectAll(".link"),
node = g.selectAll(".node");
svg
.call(zoom) // delete this line to disable free zooming
.call(zoom.event);
d3.json("data/miserables.json", function(error, graph) {
if (error) throw error;
force
.nodes(graph.nodes)
.links(graph.links)
.start();
link = link.data(graph.links)
.enter().append("line")
.attr("class", "links")
.style("stroke", "#999");
node = node.data(graph.nodes)
.enter().append("circle")
.attr("class", "node")
.attr("r", 12)
.on("click", clicked)
//.call(drag);
});
function tick() {
link.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; });
node.attr("cx", function(d) { return d.x; })
.attr("cy", function(d) { return d.y; });
}
function clicked(d){
if (active.node() === this) return reset();
active.classed("active", false);
active = d3.select(this).classed("active", true);
var bbox = active.node().getBBox(),
bounds = [[bbox.x, bbox.y],[bbox.x + bbox.width, bbox.y + bbox.height]];
var dx = bounds[1][0] - bounds[0][0],
dy = bounds[1][1] - bounds[0][1],
x = (bounds[0][0] + bounds[1][0]) / 2,
y = (bounds[0][1] + bounds[1][1]) / 2,
scale = Math.max(1, Math.min(8, 0.9 / Math.max(dx / width, dy / height))),
translate = [width / 2 - scale * x, height / 2 - scale * y];
svg.transition()
.duration(750)
.call(zoom.translate(translate).scale(scale).event);
}
function reset() {
active.classed("active", false);
active = d3.select(null);
svg.transition()
.duration(750)
.call(zoom.translate([0, 0]).scale(1).event);
}
function dragstart(d) {
d3.select(this).classed("fixed", d.fixed = true);
}
function zoomed() {
console.log(d3.event)
g.style("stroke-width", 1.5 / d3.event.scale + "px");
g.attr("transform", "translate(" + d3.event.translate + ")scale(" + d3.event.scale + ")");
}
</script>
You're using D3 v3.x. While linking by name is an easy task in D3 v4.x, it seems that it's not possible in D3 v3.x. See this issue in D3 v3.x, and this explanation in the API:
Note: the values of the source and target attributes may be initially specified as indexes into the nodes array; these will be replaced by references after the call to start.
Thus, the snippet below won't work (the code is not mine, I just found it online and changed the links array from indices to names):
var nodes = [{
name: "node1"
}, {
name: "node2"
}, {
name: "node3"
}, {
name: "node4"
}, {
name: "node5"
}, {
name: "node6"
}, {
name: "node7"
}];
var edges = [{
source: "node1",
target: "node3"
}, {
source: "node1",
target: "node2"
}, {
source: "node1",
target: "node4"
}, {
source: "node2",
target: "node3"
}, {
source: "node2",
target: "node5"
}, {
source: "node2",
target: "node6"
}, {
source: "node3",
target: "node"
}];
var width = 400;
var height = 400;
var svg = d3.select("body")
.append("svg")
.attr("width", width)
.attr("height", height);
var force = d3.layout.force()
.nodes(nodes)
.links(edges)
.size([width, height])
.linkDistance(150)
.charge(-400);
force.start();
var svg_edges = svg.selectAll("line")
.data(edges)
.enter()
.append("line")
.style("stroke", "#ccc")
.style("stroke-width", 1);
var color = d3.scale.category20();
var svg_nodes = svg.selectAll("circle")
.data(nodes)
.enter()
.append("circle")
.attr("r", 20)
.style("fill", function(d, i) {
return color(i);
})
.call(force.drag);
var svg_texts = svg.selectAll("text")
.data(nodes)
.enter()
.append("text")
.style("fill", "black")
.attr("dx", 20)
.attr("dy", 8)
.text(function(d) {
return d.name;
});
force.on("tick", function() {
svg_edges.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;
});
svg_nodes.attr("cx", function(d) {
return d.x;
})
.attr("cy", function(d) {
return d.y;
});
svg_texts.attr("x", function(d) {
return d.x;
})
.attr("y", function(d) {
return d.y;
});
});
<script src="https://d3js.org/d3.v4.min.js"></script>
If you click "run snippet", you'll only see an error:
Uncaught TypeError: Cannot read property 'force' of undefined
Solution: Keep your links array with indices. However, if you already have/receive an array with names, you can change it to indices:
var nodeByName = d3.map(nodes, function(d) {
return d.name;
});
edges.forEach(function(d) {
d.source = nodeByName.get(d.source);
d.target = nodeByName.get(d.target);
});
Here is the same code of the first snippet with the above-mentioned changes. Now it works:
var nodes = [{
name: "node1"
}, {
name: "node2"
}, {
name: "node3"
}, {
name: "node4"
}, {
name: "node5"
}, {
name: "node6"
}, {
name: "node7"
}];
var edges = [{
source: "node1",
target: "node3"
}, {
source: "node1",
target: "node2"
}, {
source: "node1",
target: "node4"
}, {
source: "node2",
target: "node3"
}, {
source: "node2",
target: "node5"
}, {
source: "node2",
target: "node6"
}, {
source: "node2",
target: "node7"
}];
var nodeByName = d3.map(nodes, function(d) {
return d.name;
});
edges.forEach(function(d) {
d.source = nodeByName.get(d.source);
d.target = nodeByName.get(d.target);
});
var width = 400;
var height = 400;
var svg = d3.select("body")
.append("svg")
.attr("width", width)
.attr("height", height);
var force = d3.layout.force()
.nodes(nodes)
.links(edges)
.size([width, height])
.linkDistance(150)
.charge(-400);
force.start();
var svg_edges = svg.selectAll("line")
.data(edges)
.enter()
.append("line")
.style("stroke", "#ccc")
.style("stroke-width", 1);
var color = d3.scale.category20();
var svg_nodes = svg.selectAll("circle")
.data(nodes)
.enter()
.append("circle")
.attr("r", 20)
.style("fill", function(d, i) {
return color(i);
})
.call(force.drag);
var svg_texts = svg.selectAll("text")
.data(nodes)
.enter()
.append("text")
.style("fill", "black")
.attr("dx", 20)
.attr("dy", 8)
.text(function(d) {
return d.name;
});
force.on("tick", function() {
svg_edges.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;
});
svg_nodes.attr("cx", function(d) {
return d.x;
})
.attr("cy", function(d) {
return d.y;
});
svg_texts.attr("x", function(d) {
return d.x;
})
.attr("y", function(d) {
return d.y;
});
});
<script src="https://d3js.org/d3.v3.min.js"></script>
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.