My Problem: I have some data points that are bound to circle elements. Now, each time the user clicks on a circle I want to change its color to red. For this, I want to use an update function which is called each time the user clicks on a circle. Since I want to change not only the clicked circle but also other elements based on which circle was clicked, I need to somehow remember which one was clicked. I saw this being done in d3.js v3 by simply saving the datum to a variable (clickedCircle) in the event listener and recalling it later. However, this doesn't seem to work in d3.js v6+ anymore. What would be the best way to do this in v6+?
Quick d3.js v3 Example (Basic idea adapted from this chart):
var data = [
{x: 10, y: 20},
{x: 30, y: 10},
{x: 20, y: 55},
];
svg = d3.select(#div1)
.append("svg")
.attr("width", 100)
.attr("height", 100)
;
var circles;
var clickedCircle;
function update() {
circles = svg.selectAll("circle")
.data(data)
.enter()
.append("circle")
.attr("cx", function(d) { return d.position.x } )
.attr("cy", function(d) { return d.position.y } )
.attr("r", 10)
.on("click", function(e, d) { clickedCircle = d; update(); } )
;
circles
.style("fill", function(d) {
if (d === clickedCircle) {
return "red"
} else {
return "black"
}
})
;
}
Here's an example with D3 v7. If you click a circle, that circle becomes red and its data is shown in a text label.
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<script src="https://d3js.org/d3.v7.js"></script>
</head>
<body>
<div id="chart"></div>
<script>
// margin convention set up
const margin = { top: 10, bottom: 10, left: 10, right: 10 };
const width = 110 - margin.left - margin.right;
const height = 110 - margin.top - margin.bottom;
const svg = d3.select('#chart')
.append('svg')
.attr('width', width + margin.left + margin.right)
.attr('height', height + margin.top + margin.bottom);
const g = svg.append('g')
.attr('transform', `translate(${margin.left},${margin.top})`);
// data
const data = [
{ x: 10, y: 20 },
{ x: 30, y: 10 },
{ x: 20, y: 55 },
];
// draw cirlces
const circles = g.selectAll('circle')
.data(data)
.join('circle')
.attr('cx', d => d.x)
.attr('cy', d => d.y)
.attr('r', 10)
.attr('fill', 'black')
.on('click', update);
// text label
const label = g.append('text')
.attr('y', height);
// update when circle is clicked
function update(event, d) {
// reset all circles to black
circles.attr('fill', 'black');
// color the clicked circle red
d3.select(this).attr('fill', 'red');
// update the label
label.text(`x: ${d.x}, y: ${d.y}`);
}
</script>
</body>
</html>
Related
I have made two separate graph on separate page of Bar and pie chart respectively and now i wanted to combine this two graph in the single page so that I can have a dashboard. but when i start to combine to two graph in the main page its not happening and they overlap of each other.
Code:
https://github.com/Mustafa2911/d3-design/blob/main/combine.html
Combine file contain: Code of both pie and bar chart.
Bar file contain: Code of bar chart.
Pie chart contain: Code of pie chart.
Tried this with your code.
Scroll to see the bar graph axis.
NOTE: The bar graph data will not be available ∵ it is from the demo1.csv file in your repository.
Hope this helps.
<!DOCTYPE html>
<meta charset="utf-8">
<head>
<!-- Load d3.js -->
<script src="https://d3js.org/d3.v4.js"></script>
<script src="https://d3js.org/d3-scale-chromatic.v1.min.js"></script>
<style>
#my_dataviz {
display: inline-block;
width: 50%;
}
</style>
</head>
<body>
<div id="my_dataviz"></div>
<script>
// set the dimensions and margins of the graph
var width = 800
height = 450
margin = 40
// The radius of the pieplot is half the width or half the height (smallest one). I subtract a bit of margin.
var radius = Math.min(width, height) / 2 - margin
// append the svg object to the div called 'my_dataviz'
var svg = d3.select("#my_dataviz")
.append("svg")
.attr("width", width)
.attr("height", height)
.append("g")
.attr("transform", "translate(" + width / 2 + "," + height / 2 + ")");
// Create dummy data
var data = {
Corporation_Tax: 15,
Income_Tax: 15,
Customs: 5,
Union_Excise_Duties: 7,
Good_and_Service_tax: 16,
Non_tax_Revenue: 5,
Non_Dept_Capital_Receipt: 2,
Borrowings_Liabilities: 35
}
// set the color scale
var color = d3.scaleOrdinal()
.domain(["a", "b", "c", "d", "e", "f", "g", "h"])
.range(d3.schemeSet1);
// Compute the position of each group on the pie:
var pie = d3.pie()
.sort(null) // Do not sort group by size
.value(function(d) {
return d.value;
})
var data_ready = pie(d3.entries(data))
// The arc generator
var arc = d3.arc()
.innerRadius(radius * 0.5) // This is the size of the donut hole
.outerRadius(radius * 0.8)
// Another arc that won't be drawn. Just for labels positioning
var outerArc = d3.arc()
.innerRadius(radius * 0.9)
.outerRadius(radius * 0.9)
// Build the pie chart: Basically, each part of the pie is a path that we build using the arc function.
svg
.selectAll('allSlices')
.data(data_ready)
.enter()
.append('path')
.attr('d', arc)
.attr('fill', function(d) {
return (color(d.data.key))
})
.attr("stroke", "white")
.style("stroke-width", "2px")
.style("opacity", 1)
// Add the polylines between chart and labels:
svg
.selectAll('allPolylines')
.data(data_ready)
.enter()
.append('polyline')
.attr("stroke", "black")
.style("fill", "none")
.attr("stroke-width", 1)
.attr('points', function(d) {
var posA = arc.centroid(d) // line insertion in the slice
var posB = outerArc.centroid(d) // line break: we use the other arc generator that has been built only for that
var posC = outerArc.centroid(d); // Label position = almost the same as posB
var midangle = d.startAngle + (d.endAngle - d.startAngle) / 2 // we need the angle to see if the X position will be at the extreme right or extreme left
posC[0] = radius * 0.95 * (midangle < Math.PI ? 1 : -1); // multiply by 1 or -1 to put it on the right or on the left
return [posA, posB, posC]
})
// Add the polylines between chart and labels:
svg
.selectAll('allLabels')
.data(data_ready)
.enter()
.append('text')
.text(function(d) {
console.log(d.data.key);
return d.data.key
})
.attr('transform', function(d) {
var pos = outerArc.centroid(d);
var midangle = d.startAngle + (d.endAngle - d.startAngle) / 2
pos[0] = radius * 0.99 * (midangle < Math.PI ? 1 : -1);
return 'translate(' + pos + ')';
})
.style('text-anchor', function(d) {
var midangle = d.startAngle + (d.endAngle - d.startAngle) / 2
return (midangle < Math.PI ? 'start' : 'end')
})
</script>
<style>
#my_dataviz {
display: inline-block;
width: 50%;
}
</style>
<div id="my_dataviz_es"></div>
<script>
// set the dimensions and margins of the graph
var margin = {
top: 20,
right: 30,
bottom: 40,
left: 160
},
width = 460,
height = 400;
// append the svg object to the body of the page
var svg = d3.select("#my_dataviz_es")
.append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.append("g")
.attr("transform",
"translate(" + margin.left + "," + margin.top + ")");
// Parse the Data
d3.csv("demo1.csv", function(data) {
// Add X axis
var x = d3.scaleLinear()
.domain([0, 550000])
.range([0, width]);
svg.append("g")
.attr("transform", "translate(0," + height + ")")
.call(d3.axisBottom(x))
.selectAll("text")
.attr("transform", "translate(-10,0)rotate(-45)")
.style("text-anchor", "end");
// Y axis
var y = d3.scaleBand()
.range([0, height])
.domain(data.map(function(d) {
return d.Country;
}))
.padding(.1);
svg.append("g")
.call(d3.axisLeft(y))
//Bars
svg.selectAll("myRect")
.data(data)
.enter()
.append("rect")
.attr("x", x(0))
.attr("y", function(d) {
return y(d.Country);
})
.attr("width", function(d) {
return x(d.Value);
})
.attr("height", y.bandwidth())
.attr("fill", "#69b3a2")
// .attr("x", function(d) { return x(d.Country); })
// .attr("y", function(d) { return y(d.Value); })
// .attr("width", x.bandwidth())
// .attr("height", function(d) { return height - y(d.Value); })
// .attr
})
</script>
</body>
</html>
EDIT: See here - https://codepen.io/KZJ/pen/rNpqvdq?editors=1011 - for changes made reg. the below comment
what if I want to have my bar chart at the top and on right side i want to have my pie chart
Changed -
a) Both charts were using the same name 'svg' to d3.select() the divs. This caused the charts to overlap.
b) Modified width, height, transform, and added some border CSS - only for demonstration purposes - It can be removed/edited as required.
FYR this is how it looks now -
I wanted to update my d3 graph when user select the site. But when I select the dropdown menu up top, I get new graph being generated and I cannot seem to find a way to remove it.
this is the code in <script>
<script>
import * as d3 from 'd3'
import { rgbaToHex } from '../utils/color.ts'
export default {
data () {
return {
selectedSite: '',
selectedWhale: '',
groupType: '',
heatmapShow: false
}
},
mounted () {
this.generateChart()
},
methods: {
generateChart () {
// set the dimensions and margins of the graph
const margin = { top: 20, right: 20, bottom: 30, left: 30 }
const width = 1850 - margin.left - margin.right
const height = 200 - margin.top - margin.bottom
// make the area for the graph to stay
const svg = d3.select('#heatmap')
.append('svg') // svg area can include headers and color scales
.attr('width', width + margin.left + margin.right) // set width
.attr('height', height + margin.top + margin.bottom) // set height
.append('g') // new g tag area for graph only
.attr('transform', `translate(${margin.left}, ${margin.bottom})`)
// stick g tag to the bottom
const xLabel = d3.range(259)
const yLabel = d3.range(23, -1, -1)
const x = d3.scaleBand()
.domain(xLabel)
.range([0, width])
.padding(0.05)
svg.append('g')
.attr('transform', `translate(0, ${height})`)
.call(d3.axisBottom(x).tickValues(x.domain().filter((_, i) => !(i % 6))))
const y = d3.scaleBand()
.domain(yLabel)
.range([height, 0])
.padding(0.05)
svg.append('g').call(d3.axisLeft(y).tickValues(y.domain().filter((_, i) => !(i % 5))))
d3.json('../predictions.json').then((data) => {
const u = svg.selectAll().data(data.heatmaps[this.selectedWhale][this.selectedSite])
u.exit().remove()
const uEnter = u.enter().append('rect')
uEnter
.merge(u)
.transition()
.duration(1000)
.attr('x', function (d) {
return x(d[1]) // return cell's position
})
.attr('y', function (d) {
return y(d[0])
})
.attr('cx', 1)
.attr('cy', 1)
.attr('width', x.bandwidth()) // return cell's width
.attr('height', y.bandwidth()) // return cell's height
.style('fill', function (d) {
return rgbaToHex(0, 128, 255, 100 * d[2])
})
.on('mouseover', function () { // box stroke when hover
d3.select(this)
.style('stroke', 'black')
.style('opacity', 1)
})
.on('mouseout', function () { // fade block stroke when mouse leave the cell
d3.select(this)
.style('stroke', 'none')
.style('opacity', 0.8)
})
.on('click', (d) => {
console.log(d)
this.heatmapShow = true
})
uEnter.exit().remove()
})
}
},
watch: {
selectedSite: function () {
this.generateChart()
},
selectedWhale: function () {
this.generateChart()
},
groupType: function (value) {
console.log(value)
}
}
}
</script>
It appears as if you're selecting the ID to paint your graph canvas into it but you append it instead of inserting a new one.
selection.append(type)
If the specified type is a string, appends a new element of this type (tag name) as the last child of each selected element, or before the next following sibling in the update selection if this is an enter selection.
More on he subject is written here.
Try removing the ID before repainting it with selection remove and then try to append/insert again
So what I've done to fix it was, as was suggested before, just add d3.select('svg').remove() before I start creating the svg const again.
const margin = { top: 20, right: 20, bottom: 30, left: 30 }
const width = 1850 - margin.left - margin.right
const height = 200 - margin.top - margin.bottom
d3.select('svg').remove()
// make the area for the graph to stay
const svg = d3.select('#heatmap')
.append('svg') // svg area can include headers and color scales
/* rest of the code */
I know this question has been asked many times but I am not able to solve the problem of updating my pie chart. I am completely lost. Could you please tell me what seems to the problem here ?
I tried the following way but the chart doesn't seem to update. I added a function change that is supposed to update the chart. I am updating the data, redrawing the arcs and changing the labels but it is not working.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<!-- Load D3 -->
<script src="https://d3js.org/d3.v5.min.js"></script>
<title>Document</title>
</head>
<body>
<div id="pie"></div>
<script>
var dataChart = { a: 9, b: 20, c: 30, d: 8, e: 12 };
var dataChart2 = { f: 9, g: 20, h: 30, i: 8 };
console.log(dataChart);
var width = 300,
height = 300,
// Think back to 5th grade. Radius is 1/2 of the diameter. What is the limiting factor on the diameter? Width or height, whichever is smaller
radius = Math.min(width, height) / 2;
var color = d3.scaleOrdinal()
.range(["#2C93E8", "#838690", "#F56C4E"]);
var pie = d3.pie()
.value(function (d) { return d.value; });
data = pie(d3.entries(dataChart));
var arc = d3.arc()
.outerRadius(radius - 10)
.innerRadius(0);
var labelArc = d3.arc()
.outerRadius(radius - 40)
.innerRadius(radius - 40);
var svg = d3.select("#pie")
.append("svg")
.attr("width", width)
.attr("height", height)
.append("g")
.attr("transform", "translate(" + width / 2 + "," + height / 2 + ")"); // Moving the center point. 1/2 the width and 1/2 the height
var g = svg.selectAll("arc")
.data(data)
.enter().append("g")
.attr("class", "arc");
g.append("path")
.attr("d", arc)
.style("fill", function (d) { return color(d.data.key); });
g.append("text")
.attr("transform", function (d) { return "translate(" + labelArc.centroid(d) + ")"; })
.text(function (d) { return d.data.key; })
.style("fill", "#fff");
function change(dataChart) {
var pie = d3.pie()
.value(function (d) { return d.value; });
data = pie(d3.entries(dataChart));
path = d3.select("#pie").selectAll("path").data(data); // Compute the new angles
path.attr("d", arc); // redrawing the path
d3.selectAll("text").data(data).attr("transform", function (d) { return "translate(" + labelArc.centroid(d) + ")"; }); // recomputing the centroid and translating the text accordingly.
}
// calling the update functions
change(dataChart);
change (dataChart2);
</script>
</body>
</html>
What am I doing wrong here please? I want to increase the point size when the mouse enters the associated voronoi cell, however the point goes back to its original size when the mouse is exaclty above that point; I have tried both the mouseover and mousemove events without any luck. Code in snippet, you can zoom in and you will be able to see what I just described.
Many thanks!
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Chart</title>
<!-- Reference minified version of D3 -->
<script src='https://d3js.org/d3.v4.min.js' type='text/javascript'></script>
<script src='https://cdnjs.cloudflare.com/ajax/libs/jquery/3.1.1/jquery.min.js'></script>
</head>
<body>
<style>
.grid line {
stroke: #ddd;
}
</style>
<div id='scatter-plot'>
<svg width="700" height="500">
</svg>
</div>
<script>
var data = [];
for (let i = 0; i < 200; i++) {
data.push({
x: Math.random(),
y: Math.random(),
dotNum: i,
})
}
renderChart(data)
function renderChart(data) {
var totalWidth = 920,
totalHeight = 480;
var margin = {
top: 10,
left: 50,
bottom: 30,
right: 0
}
var width = totalWidth - margin.left - margin.right,
height = totalHeight - margin.top - margin.bottom;
// inner chart dimensions, where the dots are plotted
// var width = width - margin.left - margin.right;
// var height = height - margin.top - margin.bottom;
var tsn = d3.transition().duration(200);
// radius of points in the scatterplot
var pointRadius = 2;
var extent = {
x: d3.extent(data, function (d) {return d.x}),
y: d3.extent(data, function (d) {return d.y}),
};
var scale = {
x: d3.scaleLinear().range([0, width]),
y: d3.scaleLinear().range([height, 0]),
};
var axis = {
x: d3.axisBottom(scale.x).ticks(xTicks).tickSizeOuter(0),
y: d3.axisLeft(scale.y).ticks(yTicks).tickSizeOuter(0),
};
var gridlines = {
x: d3.axisBottom(scale.x).tickFormat("").tickSize(height),
y: d3.axisLeft(scale.y).tickFormat("").tickSize(-width),
}
var colorScale = d3.scaleLinear().domain([0, 1]).range(['#06a', '#06a']);
// select the root container where the chart will be added
var container = d3.select('#scatter-plot');
var zoom = d3.zoom()
.scaleExtent([1, 20])
.on("zoom", zoomed);
var tooltip = d3.select("body").append("div")
.attr("id", "tooltip")
.style("opacity", 0);
// initialize main SVG
var svg = container.select('svg')
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.call(zoom)
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
// Clip path
svg.append("clipPath")
.attr("id", "clip")
.append("rect")
.attr("width", width)
.attr("height", height);
// Heatmap dots
var dotsGroup = svg.append("g")
.attr("clip-path", "url(#clip)")
.append("g");
//Create X axis
var renderXAxis = svg.append("g")
.attr("class", "x axis")
//Create Y axis
var renderYAxis = svg.append("g")
.attr("class", "y axis")
// set up axis generating functions
var xTicks = Math.round(width / 50);
var yTicks = Math.round(height / 50);
function updateScales(data, scale){
scale.x.domain([extent.x[0], extent.x[1]]).nice(),
scale.y.domain([extent.y[0], extent.y[1]]).nice()
}
function zoomed() {
d3.event.transform.x = d3.event.transform.x;
d3.event.transform.y = d3.event.transform.y;
// update: rescale x axis
renderXAxis.call(axis.x.scale(d3.event.transform.rescaleX(scale.x)));
renderYAxis.call(axis.y.scale(d3.event.transform.rescaleX(scale.y)));
dotsGroup.attr("transform", d3.event.transform);
}
// add the overlay on top of everything to take the mouse events
dotsGroup.append('rect')
.attr('class', 'overlay')
.attr('width', width)
.attr('height', height)
.style('fill', 'red')
.style('opacity', 0)
.on('mouseover', mouseMoveHandler)
.on('mouseleave', () => {
// hide the highlight circle when the mouse leaves the chart
highlight(null);
});
renderPlot(data);
function renderPlot(data){
updateScales(data, scale);
svg.select('.y.axis')
.attr("transform", "translate(" + -pointRadius + " 0)" )
.call(axis.y);
var h = height + pointRadius;
svg.select('.x.axis')
.attr("transform", "translate(0, " + h + ")")
.call(axis.x);
svg.append("g")
.attr("class", "grid")
.call(gridlines.x);
svg.append("g")
.attr("class", "grid")
.call(gridlines.y);
//Do the chart
var update = dotsGroup.selectAll("circle").data(data)
update
.enter()
.append('circle')
.attr('r', pointRadius)
.attr('cx', d => scale.x(d.x))
.attr('cy', d => scale.y(d.y))
.attr('fill', d => colorScale(d.y))
};
// create a voronoi diagram
var voronoiDiagram = d3.voronoi()
.x(d => scale.x(d.x))
.y(d => scale.y(d.y))
.size([width, height])(data);
// add a circle for indicating the highlighted point
dotsGroup.append('circle')
.attr('class', 'highlight-circle')
.attr('r', pointRadius*2) // increase the size if highlighted
.style('fill', 'red')
.style('display', 'none');
// callback to highlight a point
function highlight(d) {
// no point to highlight - hide the circle and the tooltip
if (!d) {
d3.select('.highlight-circle').style('display', 'none');
//tooltip.style("opacity",0);
// otherwise, show the highlight circle at the correct position
} else {
d3.select('.highlight-circle')
.style('display', '')
.style('stroke', colorScale(d.y))
.attr('cx', scale.x(d.x))
.attr('cy', scale.y(d.y));
}
}
// callback for when the mouse moves across the overlay
function mouseMoveHandler() {
// get the current mouse position
var [mx, my] = d3.mouse(this);
var site = voronoiDiagram.find(mx, my);
// highlight the point if we found one, otherwise hide the highlight circle
highlight(site && site.data);
for (let i = 0; i < site.data.dotNum; i++) {
//do something....
}
}
}
</script>
</body>
</html>
you have to draw the overlay rect after the circles and the highlight circle. If not then hovering over a circle generates a mouse leave event and you see a flashing of the highlight circle
use the mousemove event not the mouseover, that is kind of a mouse-enter event
I have added logic to only update the highlight when it changes dots
the grid is not updated on zoom and translate (not fixed)
even when moving over the overlay there were still mouseleave events - they where caused by the grid lines. Moved the dots group after the grid line groups
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Chart</title>
<!-- Reference minified version of D3 -->
<script src='https://d3js.org/d3.v4.min.js' type='text/javascript'></script>
<script src='https://cdnjs.cloudflare.com/ajax/libs/jquery/3.1.1/jquery.min.js'></script>
</head>
<body>
<style>
.grid line { stroke: #ddd; }
</style>
<div id='scatter-plot'>
<svg width="700" height="500">
</svg>
</div>
<script>
var data = [];
for (let i = 0; i < 200; i++) {
data.push({
x: Math.random(),
y: Math.random(),
dotNum: i,
})
}
renderChart(data);
function renderChart(data) {
var totalWidth = 920,
totalHeight = 480;
var margin = {
top: 10,
left: 50,
bottom: 30,
right: 0
}
var width = totalWidth - margin.left - margin.right,
height = totalHeight - margin.top - margin.bottom;
// inner chart dimensions, where the dots are plotted
// var width = width - margin.left - margin.right;
// var height = height - margin.top - margin.bottom;
var tsn = d3.transition().duration(200);
// radius of points in the scatterplot
var pointRadius = 2;
var extent = {
x: d3.extent(data, function (d) {return d.x}),
y: d3.extent(data, function (d) {return d.y}),
};
var scale = {
x: d3.scaleLinear().range([0, width]),
y: d3.scaleLinear().range([height, 0]),
};
var axis = {
x: d3.axisBottom(scale.x).ticks(xTicks).tickSizeOuter(0),
y: d3.axisLeft(scale.y).ticks(yTicks).tickSizeOuter(0),
};
var gridlines = {
x: d3.axisBottom(scale.x).tickFormat("").tickSize(height),
y: d3.axisLeft(scale.y).tickFormat("").tickSize(-width),
}
var colorScale = d3.scaleLinear().domain([0, 1]).range(['#06a', '#06a']);
// select the root container where the chart will be added
var container = d3.select('#scatter-plot');
var zoom = d3.zoom()
.scaleExtent([1, 20])
.on("zoom", zoomed);
var tooltip = d3.select("body").append("div")
.attr("id", "tooltip")
.style("opacity", 0);
// initialize main SVG
var svg = container.select('svg')
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.call(zoom)
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
// Clip path
svg.append("clipPath")
.attr("id", "clip")
.append("rect")
.attr("width", width)
.attr("height", height);
//Create X axis
var renderXAxis = svg.append("g")
.attr("class", "x axis")
//Create Y axis
var renderYAxis = svg.append("g")
.attr("class", "y axis")
// set up axis generating functions
var xTicks = Math.round(width / 50);
var yTicks = Math.round(height / 50);
function updateScales(data, scale){
scale.x.domain([extent.x[0], extent.x[1]]).nice(),
scale.y.domain([extent.y[0], extent.y[1]]).nice()
}
function zoomed() {
d3.event.transform.x = d3.event.transform.x;
d3.event.transform.y = d3.event.transform.y;
// update: rescale x axis
renderXAxis.call(axis.x.scale(d3.event.transform.rescaleX(scale.x)));
renderYAxis.call(axis.y.scale(d3.event.transform.rescaleX(scale.y)));
dotsGroup.attr("transform", d3.event.transform);
}
var dotsGroup;
renderPlot(data);
function renderPlot(data){
updateScales(data, scale);
svg.select('.y.axis')
.attr("transform", "translate(" + -pointRadius + " 0)" )
.call(axis.y);
var h = height + pointRadius;
svg.select('.x.axis')
.attr("transform", "translate(0, " + h + ")")
.call(axis.x);
svg.append("g")
.attr("class", "grid")
.call(gridlines.x);
svg.append("g")
.attr("class", "grid")
.call(gridlines.y);
dotsGroup = svg.append("g")
.attr("clip-path", "url(#clip)")
.append("g");
//Do the chart
var update = dotsGroup.selectAll("circle").data(data)
update
.enter()
.append('circle')
.attr('r', pointRadius)
.attr('cx', d => scale.x(d.x))
.attr('cy', d => scale.y(d.y))
.attr('fill', d => colorScale(d.y))
};
// create a voronoi diagram
var voronoiDiagram = d3.voronoi()
.x(d => scale.x(d.x))
.y(d => scale.y(d.y))
.size([width, height])(data);
// add a circle for indicating the highlighted point
dotsGroup.append('circle')
.attr('class', 'highlight-circle')
.attr('r', pointRadius*2) // increase the size if highlighted
.style('fill', 'red')
.style('display', 'none');
// add the overlay on top of everything to take the mouse events
dotsGroup.append('rect')
.attr('class', 'overlay')
.attr('width', width)
.attr('height', height)
.style('fill', 'red')
.style('opacity', 0)
.on('mousemove', mouseMoveHandler)
.on('mouseleave', () => {
// hide the highlight circle when the mouse leaves the chart
console.log('mouse leave');
highlight(null);
});
var prevHighlightDotNum = null;
// callback to highlight a point
function highlight(d) {
// no point to highlight - hide the circle and the tooltip
if (!d) {
d3.select('.highlight-circle').style('display', 'none');
prevHighlightDotNum = null;
//tooltip.style("opacity",0);
// otherwise, show the highlight circle at the correct position
} else {
if (prevHighlightDotNum !== d.dotNum) {
d3.select('.highlight-circle')
.style('display', '')
.style('stroke', colorScale(d.y))
.attr('cx', scale.x(d.x))
.attr('cy', scale.y(d.y));
prevHighlightDotNum = d.dotNum;
}
}
}
// callback for when the mouse moves across the overlay
function mouseMoveHandler() {
// get the current mouse position
var [mx, my] = d3.mouse(this);
var site = voronoiDiagram.find(mx, my);
//console.log('site', site);
// highlight the point if we found one, otherwise hide the highlight circle
highlight(site && site.data);
for (let i = 0; i < site.data.dotNum; i++) {
//do something....
}
}
}
</script>
</body>
</html>
I have a seemingly simple d3.js problem. I am creating a tree from a set of json data. The tree is composed of labels that are composed of a rectangle container that wrap around some text. I would like to change the width of the rectangle according to the length of the text. I understand I should be doing something like this one, but I am struggling to understand how.
Here is my JS code (stripped down of most unnecessary frills):
var rectW = 140, rectH = 40;
// Declare the nodes.
var node = draw.selectAll('g.node')
.data(nodes, function(d) { return d.id; });
// Enter the nodes.
var nodeLabel = node.enter().append('g')
.attr('transform', function(d) { return 'translate(' + source.x0 + ',' + source.y0 + ')'; });
var nodeRect = nodeLabel.append('rect')
.attr('width', rectW)
.attr('height', rectH);
var nodeText = nodeLabel.append('text')
.attr('x', rectW / 2)
.attr('y', rectH / 2)
.text(function (d) { return d.name; });
As you can see, I create an SVG group to which I append both the container rectangle and the contained text.
Now, I would like to retrieve the length of each text element, and use it to change the width of the corresponding rectangle element. How can I do that? I tried with every possible combination of D3 directives I could think of, but my knowledge of the library is not enough advanced to suit my purposes.
UPDATE
Thanks to Geraldo Furtado's answer, I managed to fix this issue by adding the following:
// This arranges the width of the rectangles
nodeRect.attr("width", function() {
return this.nextSibling.getComputedTextLength() + 20;
})
// This repositions texts to be at the center of the rectangle
nodeText.attr('x', function() {
return (this.getComputedTextLength() + 20) /2;
})
This is the current structure of your nodes:
<g>
<rect></rect>
<text></text>
</g>
That being the case, the texts are the nextSiblings of the rectangles. Therefore, all you need to get the length of the texts is using nextSibling in the rectangle selection:
nodeRect.attr("width", function() {
return this.nextSibling.getComputedTextLength() + rectW
})
Here I'm adding rectW to keep the same padding on the left and right, since you're putting the texts to start at rectW / 2.
If you're not sure about the relationship between the texts and rectangles (who is the first child, who is the last child...), you can go up, select the group element and then select the text inside it:
nodeRect.attr("width", function() {
return d3.select(this.parentNode).select("text").node().getComputedTextLength() + rectW
})
Here is a basic demo:
var data = [{
name: "some text",
x: 10,
y: 10
},
{
name: "A very very very long text here",
x: 100,
y: 50
},
{
name: "Another text, this time longer than the previous one",
x: 25,
y: 100
},
{
name: "some short text here",
x: 220,
y: 150
}
];
var svg = d3.select("svg");
var rectW = 140,
rectH = 30;
var node = svg.selectAll(null)
.data(data);
var nodeLabel = node.enter().append('g')
.attr('transform', function(d) {
return 'translate(' + d.x + ',' + d.y + ')';
});
var nodeRect = nodeLabel.append('rect')
.attr('width', rectW)
.attr('height', rectH)
.style("fill", "none")
.style("stroke", "gray")
var nodeText = nodeLabel.append('text')
.attr('x', rectW / 2)
.attr('y', rectH / 2)
.style("dominant-baseline", "central")
.text(function(d) {
return d.name;
});
nodeRect.attr("width", function() {
return this.nextSibling.getComputedTextLength() + rectW
})
<script src="https://d3js.org/d3.v5.min.js"></script>
<svg width="500" height="300"></svg>
For storing the computed width and using it later on, you can set another property. For instance:
nodeRect.attr("width", function(d) {
return d.rectWidth = this.nextSibling.getComputedTextLength() + rectW
});
Here is the demo, look at the console:
var data = [{
name: "some text",
x: 10,
y: 10
},
{
name: "A very very very long text here",
x: 100,
y: 50
},
{
name: "Another text, this time longer than the previous one",
x: 25,
y: 100
},
{
name: "some short text here",
x: 220,
y: 150
}
];
var svg = d3.select("svg");
var rectW = 140,
rectH = 30;
var node = svg.selectAll(null)
.data(data);
var nodeLabel = node.enter().append('g')
.attr('transform', function(d) {
return 'translate(' + d.x + ',' + d.y + ')';
});
var nodeRect = nodeLabel.append('rect')
.attr('width', rectW)
.attr('height', rectH)
.style("fill", "none")
.style("stroke", "gray")
var nodeText = nodeLabel.append('text')
.attr('x', rectW / 2)
.attr('y', rectH / 2)
.style("dominant-baseline", "central")
.text(function(d) {
return d.name;
});
nodeRect.attr("width", function(d) {
return d.rectWidth = this.nextSibling.getComputedTextLength() + rectW
});
nodeLabel.each(function(d) {
console.log(d)
})
<script src="https://d3js.org/d3.v5.min.js"></script>
<svg width="500" height="300"></svg>
You can add the following line at the end of your script, after the text has been set:
nodeRect
.transition()
.attr("width",function(){
return Math.max(rectW,nodeText.node().getComputedTextLength())
}).delay(0).duration(500)
I used Math.max to set it to rectW if its large enough or expand if necessary. You can perhaps adapt that part.