I have created a d3 visualization that takes an json data, create a rect for each data point, and then displays the text in the rect. However, drag
works only for the 1st rect.
I am wondering how to acts a natural drag action for each rect.
my codepen project: https://codepen.io/moriakijp/project/editor/ZRnVwr
here is the code:
drawNumbers = layout => {
const width = innerWidth;
const height = width * 0.5;
const margin = {
top: height * 0.05,
bottom: height * 0.05,
left: width * 0.05,
right: width * 0.05
};
d3.json(layout).then(data => {
const colsize = data[data.length-1].col;
const rowsize = data[data.length-1].row;
const blocksize = colsize < rowsize ?
(width - margin.left - margin.right) / colsize:
(height - margin.left - margin.right) / rowsize;
function dragstarted(d) {
}
function dragged(d) {
d3
.select(this)
.select("rect")
.attr("x", (d.x = d3.event.x))
.attr("y", (d.y = d3.event.y));
d3
.select(this)
.select("text")
.attr("x", (d.x = d3.event.x))
.attr("y", (d.y = d3.event.y));
}
const dragended = (d) => {
}
const drag = d3
.drag()
.on("start", dragstarted)
.on("drag", dragged)
.on("end", dragended);
const svg = d3
.select("#heatmap")
.append("svg")
.attr("width", width)
.attr("height", height)
.attr("transform", `translate(${margin.left}, ${margin.top})`)
.selectAll("g")
.data(data)
.enter()
.append("g")
.call(drag)
svg
.selectAll("g")
.data(data)
.enter()
.append("rect")
.attr("id", "block")
.attr("class", "block")
.attr("x", (d, i) => blocksize * (i % colsize)) // relative to 'svg'
.attr("y", (d, i) => blocksize * (data[i].row - 1)) // relative to 'svg'
.attr("width", blocksize)
.attr("height", blocksize)
.attr("fill", "#d00a")
.style("opacity", 0.5)
.attr("stroke", "#000")
.attr("stroke-width", "2")
svg
.selectAll("g")
.data(data)
.enter()
.append("text")
.attr("id", "text")
.attr("class", "text")
.text(d => `${d.char}`)
.attr("x", (d, i) => blocksize * (i % colsize))
.attr("y", (d, i) => blocksize * (data[i].row - 1))
.attr("text-anchor", "middle")
.attr("dominant-baseline", "middle")
.attr("fill", "#333")
.attr("dx", blocksize / 2)
.attr("dy", blocksize / 2)
.style("font-size", blocksize / 2 );
});
};
drawNumbers('number.json');
You aren't quite using the enter pattern correctly if you want to take "data, create a rect for each data point, and then displays the text in the rect."
Let's break down what you have:
const svg = d3
.select("#heatmap")
.append("svg")
.attr("width", width)
.attr("height", height)
.attr("transform", `translate(${margin.left}, ${margin.top})`)
.selectAll("g")
.data(data)
.enter()
.append("g")
.call(drag)
Here you select the element with id heatmap append an svg, and then enter a g for each item in your data array. As such, svg is a selection of three g elements, and you call the drag on these g elements.
Next you take this selection of three g elements and select child g elements. As there are no child g elements (this is an empty selection), entering and appending (rects), creates three child rectangles for each g in the selection svg:
svg
.selectAll("g")
.data(data)
.enter()
.append("rect")
....
You do the same thing with the text. Now we have 9 rectangles and 9 texts, three each in each of the parent g elements (held the selection svg). Each of those parent g elements has a drag function that positions the first rectangle in it:
d3
.select(this)
.select("rect") // select first matching element
.attr("x", (d.x = d3.event.x))
.attr("y", (d.y = d3.event.y));
As each g has three rectangles, only the first one will be moved.
One solution would be to not do an enter cycle for each g in svg: your data is not nested, we already have a g for each item in the data array. So we just need to append a single text element and a single rect element to each g:
svg.append("rect").attr("x", function(d) {...
The data bound originally to the g is also bound to this child element, no need to rebind data. Though, I'd rename svg to something else so that it is more reflective of its role and contents though.
Overall this might look something like:
const g = d3
.select("#heatmap")
.append("svg")
.attr("width", width)
.attr("height", height)
.attr("transform", `translate(${margin.left}, ${margin.top})`)
.selectAll("g")
.data(data)
.enter() // create a g for each item in the data array
.append("g")
.call(drag)
// add a rect to each g
g.append("rect")
.attr("id", "block")
.attr("class", "block")
.attr("x", (d, i) => blocksize * (i % colsize)) // relative to 'svg'
.attr("y", (d, i) => blocksize * (data[i].row - 1)) // relative to 'svg'
.attr("width", blocksize)
.attr("height", blocksize)
.attr("fill", "#d00a")
.style("opacity", 0.5)
.attr("stroke", "#000")
.attr("stroke-width", "2")
// add text to each g
g.append("text")
.attr("id", "text")
.attr("class", "text")
.text(d => `${d.char}`)
.attr("x", (d, i) => blocksize * (i % colsize))
.attr("y", (d, i) => blocksize * (data[i].row - 1))
.attr("text-anchor", "middle")
.attr("dominant-baseline", "middle")
.attr("fill", "#333")
.attr("dx", blocksize / 2)
.attr("dy", blocksize / 2)
.style("font-size", blocksize / 2 );
Here's a running example with the above modification.
Related
I am trying to update the color of the graph, on updating, all the previous graphs are also visible
Here is my code:-
class BarChart extends Component {
state = {
color: "green",
};
componentDidUpdate = (prevProps, prevState) => {
if (prevState.color != this.props.color) {
this.drawChart();
}
};
drawChart() {
const data = [12, 5, 6, 6];
const svg = d3
.select("body")
.append("svg")
.attr("width", 400)
.attr("height", 400)
svg
.selectAll("rect")
.data(data)
.enter()
.append("rect")
.attr("x", (d, i) => i * 70)
.attr("y", (d, i) => 400 - 10 * d)
.attr("width", 65)
.attr("height", (d, i) => d * 10)
.attr("fill", this.props.color);
svg
.selectAll("text")
.data(data)
.enter()
.append("text")
.text((d) => d)
.attr("x", (d, i) => i * 70)
.attr("y", (d, i) => 400 - 10 * d - 3);
svg.data(data).exit().remove();
}
render() {
return <div>{this.drawChart}</div>
);
}
}
I've figured that I need to change the selectAll part, but don't know exactly how to change it?
You need to include an .exit declaration
svg
.selectAll("text")
.data(data)
.exit()
.remove()
svg
.selectAll("rect")
.data(data)
.exit()
.remove()
http://bl.ocks.org/alansmithy/e984477a741bc56db5a5
You may want to instead use a .selectAll("g") element/container to avoid having to maintain 'text' and 'rect' selections separately.
I've got a sunburst chart drawn and this works fine. I've tried to add a circle that displays an image within each segment (path) and I can't get them to show up. Weirdly if I inspect the DOM the circles are exactly where I want them to be in the tree but they simply don't display. Is anybody able to help?
.create("svg")
.attr("viewBox", [0, 0, width, width])
.style("font-size", ".6rem");
const g = svg
.append("g")
.attr("transform", `translate(${width / 2},${width / 2})`);
const path = g
.append("g")
.selectAll("path")
.data(root.descendants().slice(1))
.join("path")
.attr("fill", (d) => {
while (d.depth > 1) d = d.parent;
return color(d.data.name);
})
.attr("fill-opacity", (d) =>
arcVisible(d.current) ? (d.children ? 0.7 : 0.5) : 0
)
.attr("d", (d) => arc(d.current));
path
.filter((d) => d.children)
.style("cursor", "pointer")
.on("click", clicked);
const defs = path.append("defs").attr("id", "imgdefs");
const iconPattern = defs
.append("pattern")
.attr("id", "iconPattern")
.attr("height", 1)
.attr("width", 1)
.attr("x", "0")
.attr("y", "0");
iconPattern
.append("image")
.attr("xlink:href", function (d) {
return d.data.img;
})
.attr("height", 15)
.attr("width", 15);
path
.append("circle")
.attr("r", 5)
.attr("cy", 0)
.attr("cx", 0)
.attr("fill", "url(#iconPattern)");
path
.append("title")
.attr("pointer-events", "none")
.attr("color", "white")
.on("click", clicked)
.text((d) => d.data.name);```
Here is your code sample refactored to add g instead of path and add path, circle, and text under g:
const segments = g
.selectAll("g.segment")
.data(root.descendants().slice(1))
.join("g")
.classed("segment", true)
segments.append("path)
.attr("fill", ...)
.attr("fill-opacity", ...);
segments.filter((d) => d.children)
.style("cursor", "pointer")
.on("click", clicked);
const defs = segments.append("defs").attr("id", "imgdefs");
const iconPattern = defs
.append("pattern")
...
iconPattern
.append("image")
...
segments.append("circle")
.attr("r", 5)
.attr("cy", 0)
.attr("cx", 0)
.attr("fill", "url(#iconPattern)");
segments.append("title")
.attr("pointer-events", "none")
.attr("color", "white")
.on("click", clicked)
.text((d) => d.data.name);
This question already has answers here:
Adding text to d3 circle
(2 answers)
Closed 3 years ago.
I am writing a text element (x axis measure value) for each circle but even after showing text element in inspect in browser its not showing
I have appended the text under circle given same x and y for the circle but its coming through
!DOCTYPE html>
<meta charset="utf-8">
<!-- Load d3.js -->
<script src="https://d3js.org/d3.v4.js"></script>
<!-- Create a div where the graph will take place -->
<div id="my_dataviz"></div>
<script>
// set the dimensions and margins of the graph
var margin = {top: 10, right: 30, bottom: 40, left: 100},
width = 460 - margin.left - margin.right,
height = 500 - margin.top - margin.bottom;
// append the svg object to the body of the page
var svg = d3.select("#my_dataviz")
.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("https://raw.githubusercontent.com/holtzy/data_to_viz/master/Example_dataset/7_OneCatOneNum_header.csv", function(data) {
// sort data
data.sort(function(b, a) {
return a.Value - b.Value;
});
// Add X axis
var x = d3.scaleLinear()
.domain([0, 13000])
.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))
// Lines
svg.selectAll("myline")
.data(data)
.enter()
.append("line")
.attr("x1", x(0))
.attr("x2", x(0))
.attr("y1", function(d) { return y(d.Country); })
.attr("y2", function(d) { return y(d.Country); })
.attr("stroke", "grey")
// Circles -> start at X=0
svg.selectAll("mycircle")
.data(data)
.enter()
.append("circle")
.attr("cx", x(0) )
.attr("cy", function(d) { return y(d.Country); })
.attr("r", "7")
.style("fill", "#69b3a2")
.attr("stroke", "black")
// Change the X coordinates of line and circle
svg.selectAll("circle")
.transition()
.duration(2000)
.attr("cx", function(d) { return x(d.Value); })
svg.selectAll("line")
.transition()
.duration(2000)
.attr("x1", function(d) { return x(d.Value); })
// this is the line i have added at my end and it showing as well while i do the inspect element.
svg.selectAll("circle")
.append(Text)
.attr("x", function (d) { return x(d.Value); })
.attr("y", function (d) { return y(d.Country); })
.text(function (d) { return d.Value})
.attr("font-family", "sans-serif")
.attr("font-size", "6px")
.attr("fill", "black")
.style("text-anchor", "middle")
})
</script>
Would like to show measure value under circle so user dont have to guess the x axis. circle is at 13000 so it should show as 13 in circle divided by 1000
From what I can see there's a couple of things going on.
Firstly, instead of:
...
.append(Text)
which is trying to pass in a variable called Text to the append function, it should be:
...
.append('text')
i.e. append an svg text element.
However, this is still appending text elements to circle elements. If you look at the elements via Chrome Devtools, you can see that there will be a text element inside each circle element, which doesn't actually display anything.
Instead, the label text needs to be rendered separately from the circles using something like.
svg.selectAll("mytext")
.data(data)
.enter()
.append('text')
.attr("x", function (d) { return x(d.Value) + 10; })
.attr("y", function (d) { return y(d.Country) + 4; })
.text(function (d) { return d.Value})
.attr("font-family", "sans-serif")
.attr("font-size", "10px")
.attr("fill", "black")
.style("text-anchor", "start")
.style('opacity', 0)
.transition()
.delay(1500)
.duration(500)
.style('opacity', 1);
I've made the font a bit bigger, and adjusted the x and y values and used text-anchor: start so that now the text appears just off the right of the circles. I've also put in a transition based on opacity with a delay so that the text only appears at the end of the circles' animation.
Data
MP,Party,Constituency,Status
"Abbott, Diane",Labour,Hackney North and Stoke Newington,Remain
"Abrahams, Debbie",Labour,Oldham East and Saddleworth,Remain
"Adams, Nigel",Conservative,Selby and Ainsty,Leave
I have created a svg group for each 'Party' populated with many circles to represent each 'MP' belonging to that Party.
The problem I have is that some of the parties are so large that they run right off the screen. Ideally I would like to set the width at about 10 circles before they return to the next 'line'.
I have found examples for setting the width of SVG text but not SVG shapes. Is it possible to create multiple lines of SVG shapes using D3?
Plunker
var svg = d3.select('body')
.append('svg')
.attr('width', width)
.attr('height', height)
var chart = svg
.append('g')
.attr('transform', 'translate(50,100)')
var party = chart.selectAll(".party")
.data(mps)
.enter()
.append('g')
.attr('transform', function(d, i) {
return "translate(0," + (i * (height / mps.length) + ")")
})
party
.append('text')
.text(function(d) {
return d.key
})
party.selectAll('.members')
.data(function(d) {
return d.values
})
.enter()
.append('circle')
.attr('class', 'chart')
.attr('r', 5)
.attr('cy', '10')
.style('fill', function(d) {
return '#' + d.Color
})
.attr("transform", function(d, i) {
return "translate(" + i * 13 + ",20)"
});
Something like this perhaps? You'll likely have to adjust the values to suit.
.attr('transform', function(d, i) {
return "translate(" + ((i / 10) * 20)) + "," + ((i % 10) * (height / mps.length)) + ")")
I am trying to create the image above using d3
http://jsfiddle.net/Spwizard/LBzx7/1/
var dataset = {
hddrives: [20301672, 9408258, 2147483, 21474836, 35622,32210000],
};
var width = 460,
height = 300,
radius = Math.min(width, height) / 2;
var color = d3.scale.ordinal()
.range(["#2DA7E2"]);
var pie = d3.layout.pie()
.sort(null);
var arc = d3.svg.arc()
.innerRadius(radius - 100)
.outerRadius(radius - 70);
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height)
.append("g")
.attr("transform", "translate(" + width / 2 + "," + height / 2 + ")");
var path = svg.selectAll("path")
.data(pie(dataset.hddrives))
.enter().append("path")
.attr("class", "arc")
.attr("fill", function(d, i) { return color(i); })
.attr("d", arc);
svg.append("text")
.attr("dy", ".35em")
.style("text-anchor", "middle")
.attr("class", "inside")
.text(function(d) { return '56%'; });
svg.append("text")
.attr("dy", "2em")
.style("text-anchor", "middle")
.attr("class", "data")
.text(function(d) { return 'some text'; });
Im struggling to see how to deal with the background color of the inner circle and dealing with the space left for storage
Thanks
To get a "background", you can add another circle with the respective fill colour. To deal with the free space, you can selectively set the opacity of one of the segments to 0. In your example, I've done that for the last slice:
.style("opacity", function(d, i) { return i == dataset.hddrives.length - 1 ? 0 : 1; })
Complete example (provided by OP) here.
Just append text:
svg.append("text")
.attr("text-anchor", "middle")
.attr('font-size', '20px')
.attr('y', '5')
.text(dataset.hddrives + "%");