Text and Rectangle not align - d3.js

The following is the code of rectangle with text within.
The problem is that it can't seem to align. How to make them align more easily?
Here's the code I use:
I know I can adjust the x and y for rectangle and text, but a more organized way to make them align is probably to have one g for each rectangle and related text, and adjust their positions within the g? How to achieve that?
<!DOCTYPE html>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.2.8/d3.min.js" type="text/JavaScript"></script>
<!--script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.9/d3.js" type="text/JavaScript"></script-->
<style>
rect {
stroke: #9A8B7A;
stroke-width: 1px;
fill: #CF7D1C;
opacity:
}
</style>
<body></body>
<script>
var dataset = [[1,3,3,5,6,7],[3,5,8,3,2,6],[9,0,6,3,6,3],[3,4,4,5,6,8],[3,4,5,2,1,8]];
var svg = d3.select("body")
.append("svg")
.attr("width", 500)
.attr("height", 500);
var local = d3.local();
svg.append("g")
.selectAll("g")
.data(dataset)
.enter()
.append("g")
.selectAll("text")
.data(function(d, i) {
local.set(this, i)
return d;
})
.enter()
.append("text")
.text(function(d, i, j) {
return d;
})
.attr("x", function(d, i, j) {
return (i * 20) + 40;
})
.attr("y", function(d) {
return (local.get(this) * 20) + 40;
})
.attr("font-family", "sans-serif")
.attr("font-size", "20px");
svg.append("g")
.selectAll("g")
.data(dataset)//use top-level data to join g
.enter()
.append("g")
.selectAll("rect")
.data(function(d, i) {//for each <g>, use the second-level data (return d) to join rect
console.log(this);
local.set(this, i);//this is the <g> parent
return d;
})
.enter()
.append("rect")
.attr("x", function(d, i, j) {
return (i * 20) + 40;
})
.attr("y", function(d) {
return (local.get(this) * 20) + 40;
})
.attr("width",20)
.attr("height",20)
.attr("fill-opacity",0.1)
</script>

An easy solution is just setting the dominant-baseline. This is a nice image with the possible values:
Source: http://bl.ocks.org/eweitnauer/7325338
So, in your case, just do:
.attr("dominant-baseline", "text-before-edge")
Here is your code with that change:
var dataset = [
[1, 3, 3, 5, 6, 7],
[3, 5, 8, 3, 2, 6],
[9, 0, 6, 3, 6, 3],
[3, 4, 4, 5, 6, 8],
[3, 4, 5, 2, 1, 8]
];
var svg = d3.select("body")
.append("svg")
.attr("width", 500)
.attr("height", 500);
var local = d3.local();
svg.append("g")
.selectAll("g")
.data(dataset) //use top-level data to join g
.enter()
.append("g")
.selectAll("rect")
.data(function(d, i) {
local.set(this, i); //this is the <g> parent
return d;
})
.enter()
.append("rect")
.attr("x", function(d, i, j) {
return (i * 20) + 40;
})
.attr("y", function(d) {
return (local.get(this) * 20) + 40;
})
.attr("width", 20)
.attr("height", 20)
.attr("fill", "tan")
.attr("stroke", "dimgray")
.attr("fill-opacity", 0.4);
svg.append("g")
.selectAll("g")
.data(dataset)
.enter()
.append("g")
.selectAll("text")
.data(function(d, i) {
local.set(this, i)
return d;
})
.enter()
.append("text")
.text(function(d, i, j) {
return d;
})
.attr("x", function(d, i, j) {
return (i * 20) + 40;
})
.attr("y", function(d) {
return (local.get(this) * 20) + 40;
})
.attr("dominant-baseline", "text-before-edge")
.attr("font-family", "sans-serif")
.attr("font-size", "20px");
<script src="https://d3js.org/d3.v4.min.js"></script>

Related

Target this->sibling with mouse event

I'm following the book Interactive Data Visualization for the Web (2nd Ed). While learning about adding interactivity to a bar chart, the text states:
Throw an invisible rect with a fill of none and pointer-events value of all on the top of each group. Even though the rect is invisible, it will still trigger mouse events, so you could have the rect span the whole height of the chart. The net effect is that mousing anywhere in that column—even in “empty” whitespace above a short blue bar—would trigger the highlight effect.
I believe I've successfully created the invisible rect in the proper place (at the end, so as to not be behind the visible rects). I can mouse anywhere in the column, even in the empty whitespace above the short blue bar. However, I cannot figure out how to only highlight the blue bar and not the entire container rect.
Fiddle
//Width and height
var w = 600;
var h = 250;
var dataset = [5, 10, 13, 19, 21, 25, 22, 18, 15, 13,
11, 12, 15, 20, 18, 17, 16, 18, 23, 25];
var xScale = d3.scaleBand()
.domain(d3.range(dataset.length))
.rangeRound([0, w])
.paddingInner(0.05);
var yScale = d3.scaleLinear()
.domain([0, d3.max(dataset)])
.range([0, h]);
//Create SVG element
var svg = d3.select("body")
.append("svg")
.attr("width", w)
.attr("height", h);
//Create groups to hold the bars and text for each data point
var groups = svg.selectAll(".groups")
.data(dataset)
.enter()
.append("g")
.attr("class", "gbar");
//Create bars
groups.append("rect")
.attr("class", "actualRect")
.attr("x", function (d, i) {
return xScale(i);
})
.attr("y", function (d) {
return h - yScale(d);
})
.attr("width", xScale.bandwidth())
.attr("height", function (d) {
return yScale(d);
})
.attr("fill", function (d) {
return "rgb(0, 0, " + Math.round(d * 10) + ")";
});
//Create labels
groups.append("text")
.text(function (d) {
return d;
})
/*.style("pointer-events", "none")*/
.attr("text-anchor", "middle")
.attr("x", function (d, i) {
return xScale(i) + xScale.bandwidth() / 2;
})
.attr("y", function (d) {
return h - yScale(d) + 14;
})
.attr("font-family", "sans-serif")
.attr("font-size", "11px")
.attr("fill", "white");
// Create container rect
// The goal is to be able to hover *above* a bar, but only highlight the visible blue bar to orange.
// I don't understand how to select (this).('actualRect'), instead of (this).("containerRect")
groups.append("rect")
.attr("class", "containerRect")
.attr("x", function (d, i) {
return xScale(i);
})
.attr("y", 0)
.attr("width", xScale.bandwidth())
.attr("height", h)
.attr("fill", "none")
.style("pointer-events", "all")
.on("mouseover", function () {
d3.select(this) // trying to target (this) -> .actualBar
.attr("fill", "orange");
});
You can select the sibling rect by first selecting this.parentNode from within your event callback function, and then making the desired selection.
d3.select(this.parentNode).select('.actualRect').attr("fill", "orange");
//Width and height
var w = 600;
var h = 250;
var dataset = [5, 10, 13, 19, 21, 25, 22, 18, 15, 13,
11, 12, 15, 20, 18, 17, 16, 18, 23, 25];
var xScale = d3.scaleBand()
.domain(d3.range(dataset.length))
.rangeRound([0, w])
.paddingInner(0.05);
var yScale = d3.scaleLinear()
.domain([0, d3.max(dataset)])
.range([0, h]);
//Create SVG element
var svg = d3.select("body")
.append("svg")
.attr("width", w)
.attr("height", h);
//Create groups to hold the bars and text for each data point
var groups = svg.selectAll(".groups")
.data(dataset)
.enter()
.append("g")
.attr("class", "gbar");
//Create bars
groups.append("rect")
.attr("class", "actualRect")
.attr("x", function (d, i) {
return xScale(i);
})
.attr("y", function (d) {
return h - yScale(d);
})
.attr("width", xScale.bandwidth())
.attr("height", function (d) {
return yScale(d);
})
.attr("fill", function (d) {
return "rgb(0, 0, " + Math.round(d * 10) + ")";
});
//Create labels
groups.append("text")
.text(function (d) {
return d;
})
/*.style("pointer-events", "none")*/
.attr("text-anchor", "middle")
.attr("x", function (d, i) {
return xScale(i) + xScale.bandwidth() / 2;
})
.attr("y", function (d) {
return h - yScale(d) + 14;
})
.attr("font-family", "sans-serif")
.attr("font-size", "11px")
.attr("fill", "white");
// Create container rect
// The goal is to be able to hover *above* a bar, but only highlight the visible blue bar to orange.
// I don't understand how to select (this).('actualRect'), instead of (this).("containerRect")
groups.append("rect")
.attr("class", "containerRect")
.attr("x", function (d, i) {
return xScale(i);
})
.attr("y", 0)
.attr("width", xScale.bandwidth())
.attr("height", h)
.attr("fill", "none")
.style("pointer-events", "all")
.on("mouseover", function () {
d3.select(this.parentNode).select('.actualRect').attr("fill", "orange");
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
Fiddle

must it need a full-fill rectangle element in a g(group) element to activate the whole elements within the g element?

when g tag is bonded to a mouseover event whether elements within the g tag response in concert to the mouseover and don't need another rectangle to fill the g tag?
I am following a d3.js book, it refers to a situation that one visible element (here is a text tag) overlaps another (here is a rectangle), and if I bind an event (here is mouseover) to the deeper element, it won't call the event when your mouse over the upper element (the text tag) .
and it gives a solution that you can group the rect and text tags together, and bind the event to the g element so that both rect and text could work.
but it also needs a CSS code: pointer-events: all and another rectangle to overlap the whole the rectangle and text tags inside the g tag, in this case, it will work.
<head>
<meta charset="utf-8">
<title>D3: Smoother highlight transitions</title>
<script type="text/javascript" src="../d3.js"></script>
<style type="text/css">
/* No style rules here yet */
</style>
</head>
<body>
<script type="text/javascript">
//Width and height
var w = 600;
var h = 250;
var dataset = [5, 10, 13, 19, 21, 25, 22, 18, 15, 13,
11, 12, 15, 20, 18, 17, 16, 18, 23, 25
];
var xScale = d3.scaleBand()
.domain(d3.range(dataset.length))
.rangeRound([0, w])
.paddingInner(0.05);
var yScale = d3.scaleLinear()
.domain([0, d3.max(dataset)])
.range([0, h]);
//Create SVG element
var svg = d3.select("body")
.append("svg")
.attr("width", w)
.attr("height", h);
//Create bars
svg.selectAll("g")
.data(dataset)
.enter()
.append("g")
.append("rect")
.attr("x", function(d, i) {
return xScale(i);
})
.attr("y", function(d) {
return h - yScale(d);
})
.attr("width", xScale.bandwidth())
.attr("height", function(d) {
return yScale(d);
})
.attr("fill", function(d) {
return "rgb(0, 0, " + Math.round(d * 10) + ")";
});
//Create labels
svg.selectAll("g")
.append("text")
.text(function(d) {
return d;
})
.attr("text-anchor", "middle")
.attr("x", function(d, i) {
return xScale(i) + xScale.bandwidth() / 2;
})
.attr("y", function(d) {
return h - yScale(d) + 14;
})
.attr("font-family", "sans-serif")
.attr("font-size", "11px")
.attr("fill", "white");
//*****************to add a mouseover event to g tag and overlap the rectangle and text tags above with a rectangle***************************
svg.selectAll("g")
.style("pointer-events", "all")
.on("mouseover", function() {
d3.select(this)
.select("rect")
.attr("fill", "orange");
})
.on("mouseout", function(d) {
d3.select(this)
.select("rect")
.transition()
.duration(250)
.attr("fill", "rgb(0, 0, " + (d * 10) + ")");
})
.append("rect")
.attr("x", function(d, i) {
return xScale(i);
})
.attr("y", function(d) {
return h - yScale(d);
})
.attr("width", xScale.bandwidth())
.attr("height", function(d) {
return yScale(d);
})
//*****************make it invisible*********************************
.attr("fill", "none");
</script>
</body>
you could copy it into an empty HTML file, and load it, it's a bar chart.
the only question is if a rectangle is needed to fill the unempty g tag to make the bar rectangles and text response together.

Resize the SVGs according to windows size

Here's how's the D3.js look currently
What I want to achieve is that, when I resize the windows four tables inside needs resize accordingly. No new tables are added.
Currently it just keep adding new tables inside. How to correct this behavior?
The content of 1.json
[
[[1,3,3,5,6,7],[3,5,8,3,2,6],[9,0,6,3,6,3],[3,4,4,5,6,8],[3,4,5,2,1,8]],
[[1,3,3,5,6,7],[3,5,8,3,2,6],[9,0,6,3,6,3],[3,4,4,5,6,8],[3,4,5,2,1,8]],
[[1,3,3,5,6,7],[3,5,8,3,2,6],[9,0,6,3,6,3],[3,4,4,5,6,8],[3,4,5,2,1,8]],
[[1,3,3,5,6,7],[3,5,8,3,2,6],[9,0,6,3,6,3],[3,4,4,5,6,8],[3,4,5,2,1,8]]
]
The content of D3.js:
<!DOCTYPE html>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.2.8/d3.min.js" type="text/JavaScript"></script>
<!--script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.9/d3.js" type="text/JavaScript"></script-->
<style>
rect {
stroke: #9A8B7A;
stroke-width: 1px;
fill: #CF7D1C;
}
svg{
width: 50%;
height: 50%;
}
</style>
<body>
</body>
<script>
function draw(){
d3.json("array_data/1.json", function(data){
for (i=0; i<data.length; ++i) {
main(data[i]);
}
})
}
function main(dataset){
var local = d3.local();
var svg = d3.select("body").append("svg"),
bBox = svg.node().getBoundingClientRect(),
margin = {top: 20, right: 20, bottom: 30, left: 40},
width = bBox.width - margin.left - margin.right,
height = bBox.height - margin.top - margin.bottom;
var x = d3.scaleBand().rangeRound([0, width]);
var y = d3.scaleBand().rangeRound([0, height]);
y.domain(dataset.map(function(d,i) { return i; }));
var maxChildLength= d3.max(dataset, function(d) { return d.length; });
var xArr=Array.apply(null, {length: maxChildLength}).map(Function.call, Number);
x.domain(xArr);
var maxNum = d3.max(dataset, function(array) {
return d3.max(array);
});
var color=d3.scaleLinear().domain([0,maxNum]).range([0,1]);
svg.append("g")
.selectAll("g")
.data(dataset)//use top-level data to join g
.enter()
.append("g")
.selectAll("rect")
.data(function(d, i) {//for each <g>, use the second-level data (return d) to join rect
local.set(this, i);//this is the <g> parent
return d;
})
.enter()
.append("rect")
.attr("x", function(d, i, j) {
// return (i * 20) + 40;
return x(i);
})
.attr("y", function(d) {
// return (local.get(this) * 20) + 40;
return y(local.get(this));
})
//.attr("width",20)
.attr("width", x.bandwidth())
.attr("height", y.bandwidth())
.attr("fill-opacity",function(d){console.log(color(+d));return color(+d);})
svg.append("g")
.selectAll("g")
.data(dataset)
.enter()
.append("g")
.selectAll("text")
.data(function(d, i) {
local.set(this, i)
return d;
})
.enter()
.append("text")
.text(function(d, i, j) {
return d;
})
.attr("x", function(d, i, j) {
// return (i * 20) + 40;
return x(i);
})
.attr("y", function(d) {
return y(local.get(this));
//return (local.get(this) * 20) + 40;
})
.attr("dx", x.bandwidth()/2)
.attr("dy", y.bandwidth()/2)
.attr("dominant-baseline", "central")//vertical - http://bl.ocks.org/eweitnauer/7325338
.attr("text-anchor", "middle")//horizontal - https://bl.ocks.org/emmasaunders/0016ee0a2cab25a643ee9bd4855d3464
.attr("font-family", "sans-serif")
.attr("font-size", "20px");
svg.append("g")
.append("text")
.attr("x", width/2)
.attr("y", height)
.attr("dominant-baseline", "text-before-edge")
.style("text-anchor", "middle")
//.attr("transform", "translate("+width/2+"," + height+ ")")
.text("Units sold");
}
draw();
window.addEventListener("resize", draw);
</script>
Using your method, you'll need to clear out the HTML first, then redraw. So, at the beginning of your main() function:
var element = d3.select('body');
element.innerHTML = '';
svg = d3.select(element).append('svg');
There are other methods for resizing (viewport, having a resize function), but this fits your code as it exists now.

Multidimensional array for D3

The following code works for D3 V3, but not D3 V4, how to rectify it?
<!DOCTYPE html>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.2.8/d3.min.js" type="text/JavaScript"></script>
<!--script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.9/d3.js" type="text/JavaScript"></script-->
<body></body>
<script>
var dataset = [[1,3,3,5,6,7],[3,5,8,3,2,6],[9,0,6,3,6,3],[3,4,4,5,6,8],[3,4,5,2,1,8]];
var svg = d3.select("body")
.append("svg")
.attr("width", 500)
.attr("height", 500);
svg.append("g")
.selectAll("g")
.data(dataset)
.enter()
.append("g") //removing
.selectAll("text") // these
.data( function(d,i,j) { return d; } ) //lines
.enter() //text displays normally
.append("text")
.text( function(d,i,j) { return d; } )
.attr("x", function(d,i,j) { console.log(j);return (i * 20) + 40; })
.attr("y", function(d,i,j) { return (j * 20) + 40; })
.attr("font-family", "sans-serif")
.attr("font-size", "20px")
</script>
The explanation here is simple, and it was the subject of this answer of mine: the third argument has changed from D3 v3 to D3 v4.
This is the problematic line:
.attr("y", function(d,i,j) { return (j * 20) + 40; })
//the 3rd argum. -------^ ^--- using the 3rd argument
In D3 v3, the third argument is the index of the parent group. However, in v4, the third argument is the current group. The changelog is clear:
The arguments passed to callback functions has changed slightly in 4.0 to be more consistent. The standard arguments are the element’s datum (d), the element’s index (i), and the element’s group (nodes), with this as the element.
But there is a way to achieve what you want in v4! The same changelog says:
The slight exception to this convention is selection.data, which is evaluated for each group rather than each element; it is passed the group’s parent datum (d), the group index (i), and the selection’s parents (parents), with this as the group’s parent. (emphasis mine)
Thus, we can use the data() method to get the group's index.
Here, I'm using a local variable...
var local = d3.local();
... to get the index inside data():
.data( function(d,i) {
local.set(this, i);
return d;
})
Then, using it to set the y position:
.attr("y", function(d) {
return (local.get(this) * 20) + 40;
})
Here is your code with that change:
var dataset = [
[1, 3, 3, 5, 6, 7],
[3, 5, 8, 3, 2, 6],
[9, 0, 6, 3, 6, 3],
[3, 4, 4, 5, 6, 8],
[3, 4, 5, 2, 1, 8]
];
var svg = d3.select("body")
.append("svg")
.attr("width", 500)
.attr("height", 500);
var local = d3.local();
svg.append("g")
.selectAll("g")
.data(dataset)
.enter()
.append("g")
.selectAll("text")
.data(function(d, i) {
local.set(this, i)
return d;
})
.enter()
.append("text")
.text(function(d, i, j) {
return d;
})
.attr("x", function(d, i, j) {
return (i * 20) + 40;
})
.attr("y", function(d) {
return (local.get(this) * 20) + 40;
})
.attr("font-family", "sans-serif")
.attr("font-size", "20px")
<script src="https://d3js.org/d3.v4.min.js"></script>

Creating a border around your D3 graph

So I've just started my D3 journey, and wanted to ask about how one would create a small 1px border around the chart.
I created the variables "border" and "bordercolor" and then I added .attr("border",border) to the var svg = d3.select("body") portion of my code. It doesn't crash, but I get no border either.
I guess the question is how do i add this border, and if someone could explain why what i did is wrong.
<script type="text/javascript">
//Width and height
var w = 800;
var h = 400;
var padding = 20;
var border=1;
var bordercolor='black';
var dataset = [
[5, 20], [480, 90], [250, 50], [100, 33], [330, 95],[-50,-100],[50,-45],
[410, 12], [475, 44], [25, 67], [85, 21], [220, 88],[-480, 90], [3,-90]
];
// create scale functions
var xScale = d3.scale.linear()
.domain([d3.min(dataset, function(d) { return d[0]; }), d3.max(dataset, function(d) { return d[0]; })])
.range([padding, w - padding * 2]);
var yScale = d3.scale.linear()
.domain([d3.min(dataset, function(d) { return d[0]; }), d3.max(dataset, function(d) { return d[1]; })])
.range([h - padding, padding]);
var rScale = d3.scale.linear()
.domain( [-100, d3.max(dataset, function(d) { return d[1]; })] )
.range([2,5]);
//Create SVG element
var svg = d3.select("body")
.append("svg")
.attr("width", w)
.attr("height", h)
.attr("border",border)
;
svg.selectAll("circle")
.data(dataset)
.enter()
.append("circle")
.attr("cx", function(d) {
return xScale(d[0]);
})
.attr("cy", function(d) {
return yScale(d[1]);
})
.attr("r", 3);
svg.selectAll("text")
.data(dataset)
.enter()
.append("text")
.text(function(d) {
return d[0] + "," + d[1];
})
.attr("x", function(d) {
return xScale(d[0]);
})
.attr("y", function(d) {
return yScale(d[1]);
})
.attr("font-family", "sans-serif")
.attr("font-size", "11px")
.attr("fill", "red");
</script>
Use the style attribute to place an outline around the svg:
//Create SVG element
var svg = d3.select("body")
.append("svg")
.attr("style", "outline: thin solid red;") //This will do the job
.attr("width", w)
.attr("height", h);
The svg var is just a container. You need to add a path or element to the container and then give it the stroke color and width you want for your border. There is more than one way to do this. In this gist I did it by adding a rect with the following values:
var borderPath = svg.append("rect")
.attr("x", 0)
.attr("y", 0)
.attr("height", h)
.attr("width", w)
.style("stroke", bordercolor)
.style("fill", "none")
.style("stroke-width", border);
IMHO it's better to keep separated shape from style instructions:
.append("rect")
.attr("x", 5)
.attr("y", 5)
.attr("height", 40)
.attr("width", 50)
.attr("class","foo")
...
CSS:
svg rect.foo {
fill: white;
stroke-width: 0.5;
stroke: grey;
}
Simply use css:
svg {
border:1px solid black;
}
If X and Y Axis are used, other option is to use tickSizeOuter()
Example:
var yAxis = d3.axisLeft(y).ticks(5).tickSizeOuter(-width);
var xAxis = d3.axisBottom(x).ticks(5).tickSizeOuter(-height);

Resources