Multidimensional array for D3 - d3.js

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>

Related

Error when referencing dictionary value in d3.js

I'm trying to display circles by accessing the values for the radius by referencing a dictionary. I can already do it when my vaues are in an array buw when I represent them as a dictionary I get a syntax error, even though d.keyName seems to be the recommended way to do it.
Section of code that works
%%javascript
require.config({
paths: {
d3: 'https://d3js.org/d3.v5.min'
}
});
(function(element) {
require(['d3'], function(d3) {
var data = [4, 8, 4]
var svg = d3.select(element.get(0)).append('svg')
.attr('width', 400)
.attr('height', 200);
svg.selectAll('circle')
.data(data)
.enter()
.append('circle')
.style('fill', 'orange')
.attr("r", function(d) {return 2*d;})
.attr("cx", function(d, i) {return 30 * (i + 1);})
.attr("cy", function(d, i) {return 100 + 30 * (i % 3 - 1);})
;
})
})(element);
And this is what doesn't work:
%%javascript
require.config({
paths: {
d3: 'https://d3js.org/d3.v5.min'
}
});
(function(element) {
require(['d3'], function(d3) {
//var data = [4, 8, 4]
//var words = ["milk", "but", "hey"]
var data = [{'num': 4, 'word': 'milk'}, {'num': 8, 'word': 'but'}, {'num': 4, 'word': 'hey'}];
var svg = d3.select(element.get(0)).append('svg')
.attr('width', 400)
.attr('height', 200);
svg.selectAll('circle')
.data(data)
.enter()
.append('circle')
.style('fill', 'orange')
.attr("r", function(d.num) {return 2*d.num; })
.attr("cx", function(d.num, i) {return 30 * (i + 1); })
.attr("cy", function(d.num, i) {return 100 + 30 * (i % 3 - 1); })
//.attr("r", function(d) {return 2*d;})
//.attr("cx", function(d, i) {return 30 * (i + 1);})
//.attr("cy", function(d, i) {return 100 + 30 * (i % 3 - 1);})
;
})
})(element);
Javascript error adding output!
SyntaxError: Unexpected token .
See your browser Javascript console for more details.
2outputarea.js:764 SyntaxError: Unexpected token .
at OutputArea.append_javascript (outputarea.js:762)
at OutputArea.append_mime_type (outputarea.js:696)
at OutputArea.append_display_data (outputarea.js:659)
at OutputArea.append_output (outputarea.js:346)
at OutputArea.handle_output (outputarea.js:257)
at output (codecell.js:395)
at Kernel._handle_output_message (kernel.js:1196)
at i (jquery.min.js:2)
at Kernel._handle_iopub_message (kernel.js:1223)
at Kernel._finish_ws_message (kernel.js:1015)
Syntax error. Because you want to pass object.property as parameter to attribute callback
function(d.num) {return 2*d.num; }
Use function(d) {return 2*d.num; }.
d is your current datum in array (or data[i])
Check this information and how d3 selection works

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.

Text and Rectangle not align

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>

Filter data in d3 to draw either circle or square

I'm having a bit of trouble understand selections and filtering in d3. Let's say I have a simple array:
data = [1, 2, 6, 3, 4]
I want to draw circles if the value < 5 and squares if it's >= 5. My code right now only draws circles and looks like this:
var svg = d3.select("body").append("svg")
svg.selectAll("shapes")
.data(data)
.enter()
.append("circle")
and other attributes for circles. I need to use the .filter() method, but I don't know where to put it. I tried doing something like:
var svg = d3.select("body").append("svg")
svg.selectAll("shapes")
.data(data)
.enter()
.filter(function(d){if (d>5){console.log('working');})
.append("circle")
but then I get an error with the append method. Can someone point me in the right direction on how I'd accomplish this?
The problem is that after you .enter() you are returning a nested array, hence your error:
Uncaught TypeError: Object [object Array] has no method 'append'
To use .filter(), you need to apply it after the .append():
var data = d3.range(10);
var svg = d3.select("body").append("svg");
var shapes = svg.selectAll(".shapes")
.data(data).enter();
shapes.append("circle")
.filter(function(d){ return d < 5; })
.attr("cx", function(d, i){ return (i+1) * 25; })
.attr("cy", 10)
.attr("r", 10);
shapes.append("rect")
.filter(function(d){ return d >= 5; })
.attr("x", function(d, i){ return (i+1) * 25; })
.attr("y", 25)
.attr("width", 10)
.attr("height", 10);
Using the code above (also in this fiddle), I get the following output:
Note that you can also achieve the same effect using Array's filter method, e.g.
var shapes = svg.selectAll(".shapes")
.data(data.filter(function(d){ return d < 5; })).enter()
.append("circle")
.attr("cx", function(d, i){ return (i+1) * 25; })
.attr("cy", 10)
.attr("r", 10);
It is also possible use the data to conditionally create circles or rectangles by providing a function argument to the append function
.append(function(d, i){
if (something to do with d) {
... return an SVG circle element
} else {
... return an SVG rectangle element
}
})
e.g. like this
var data = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
var svg = d3.select("body").append("svg");
function createSvgEl(name) {
return document.createElementNS('http://www.w3.org/2000/svg', name);
}
svg
.selectAll(".shapes")
.data(data)
.enter()
.append(function(d, i){
if (d <= 4) {
return createSvgEl("circle");
} else {
return createSvgEl("rect");
}
});
svg.selectAll("circle")
.attr("cx", function(d, i){ return (i+1) * 25; })
.attr("cy", 10)
.attr("r", 10);
svg.selectAll("rect")
.attr("x", function(d, i){ return (i+1) * 25; })
.attr("y", 25)
.attr("width", 10)
.attr("height", 10);

Resources