How to group g elements in D3? - d3.js

Say for example I have a g element that contains a rect and text :
var cells = innercanvas
.selectAll(".newcell")
.data(treemap)
.enter()
.append("g")
.attr("class", "newcell");
cells
.append("rect")
.attr("x", function (d) {
return d.x;
})
.attr("y", function (d) {
return d.y;
})
.attr("id", "rectangle")
.attr("width", function (d) {
return d.dx;
})
.attr("height", function (d) {
return d.dy;
})
.style("fill", function (d) {
return d.children ? cfg.color(d.name) : 'none';
})
.attr("stroke", "#000000")
.attr('pointer-events', 'all')
cells
.append("text")
.attr("x", function (d) {
return d.x + d.dx / 2;
})
.attr("y", function (d) {
return d.y + d.dy / 2;
})
.attr('dy', '.95em')
.attr("text-anchor", "middle")
.text(function (d) { return d.children ? d.name : null })
I want to add a parent g element that groups cells based on their common name. For example ,
var parent = d3.selectAll("cells").attr("groupBy",function(d){ return d.children? d.name : null;})
This is so that I can display the parent name as a header for these common cells.

Nodes don't have to be grouped together physically to be able to group them to apply a label or highlight them. At present, the code you are using produces a g element for each parent node, with a rect that provides the colour fill and a blank text element. The child nodes are then overlaid on this parent node with a black stroke along the edges and a text node with the country name.
If you want to be able to highlight all the elements that have the same parent and put a header on the element, you can do it a couple of ways:
alter existing elements to show a header
create an overlay with the same dimensions as the parent node to display a header
Altering existing elements
To be able to do this, we first need to be able to identify parent nodes and their children. One way to do this is by giving nodes classes according to their position in the hierarchy and their data content. Here's one possible way to do this:
var cells = innercanvas
.selectAll(".newcell")
.data(treemap)
.enter()
.append("g")
.attr("class", function (d,i) {
return 'newcell _' + i // i provides a unique identifier for each node
+ ' cell-level-' + d.depth // cell-level-0 for root, cell-level-1, cell-level-2, etc
+ ( d.name ? ' ' + safe_name(d.name) : '' ) // if d.name exists, use the 'safe' version
+ ( ! d.children
? ' leaf' // d has no children => it's a leaf node
: (d.depth === 0
? ' root' // d.depth = 0 => it's the root node
: ' internal ')); // has children, depth > 0 => internal node
})
// strips non-alphanumeric characters out of `name` strings, replaces with _
function safe_name (txt) {
return txt.replace(/\W/g, '_');
}
The SVG g elements now look like this:
Now we can easily access the parent node for any country name c.name by using
d3.select('.internal.' + safe_name(c.name))
or (e.g.) the text elements of the leaf nodes for the country using
d3.selectAll('.leaf.' + safe_name(c.name) + ' text')
We can then use CSS to show or hide text elements, rect strokes, etc.; e.g.
var bool = false;
var toggle = d3.select('#toggle')
.on('click', toggleSvg);
function toggleSvg() {
bool = !bool;
d3.selectAll('.Cyprus')
.classed('highlightAll', bool);
}
.newcell text {
font-family: Arial, sans-serif;
font-size: 10px;
}
.newcell.leaf rect {
stroke: #000;
stroke-width: 0.5px;
}
.oversize {
display: none;
}
.internal text {
opacity: 0
}
.internal.highlightAll text {
opacity: 1
}
.highlightAll.leaf rect {
opacity: 0.1
}
.highlightAll.leaf text {
opacity: 0
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.17/d3.min.js"></script>
<p><a id="toggle" href='#'>Toggle Cyprus highlighting</a></p>
<svg width="186" height="77">
<g transform="translate(-30.1234, -258.33)">
<g class="newcell _2331 cell-level-1 Cyprus internal ">
<rect x="30.123480134121516" y="258.33086334171067" width="185.81893466750355" height="76.6094615363257" style="fill: rgb(100, 200, 75);"></rect>
<title>Cyprus</title>
<text x="123.0329474678733" y="296.63559410987354" dy=".35em" text-anchor="middle">Cyprus</text>
</g>
<g class="newcell _2332 cell-level-2 Cyprus leaf">
<rect x="40.12348013412152" y="268.33086334171067" width="31.51365155392795" height="23.97574841366901" style="fill: none;"></rect>
<title>Cyprus</title>
<text x="55.88030591108549" y="280.3187375485452" dy=".35em" text-anchor="middle" class="oversize">Cyprus</text>
</g>
<g class="newcell _2333 cell-level-2 Cyprus leaf">
<rect x="40.12348013412152" y="292.30661175537968" width="31.51365155392795" height="32.633713122656687" style="fill: none;"></rect>
<title>Cyprus</title>
<text x="55.88030591108549" y="308.62346831670802" dy=".35em" text-anchor="middle" class="oversize">Cyprus</text>
</g>
<g class="newcell _2334 cell-level-2 Cyprus leaf">
<rect x="71.637131688049465" y="268.33086334171067" width="55.48181226963859" height="56.60946153632569" style="fill: none;"></rect>
<title>Cyprus</title>
<text x="99.37803782286876" y="296.63559410987354" dy=".35em" text-anchor="middle">Cyprus</text>
</g>
<g class="newcell _2335 cell-level-2 Cyprus leaf">
<rect x="127.11894395768805" y="268.33086334171067" width="78.823470843937" height="56.60946153632569" style="fill: none;"></rect>
<title>Cyprus</title>
<text x="166.53067937965655" y="296.63559410987354" dy=".35em" text-anchor="middle">Cyprus</text>
</g>
</g>
</svg>
Creating an overlay
This will duplicate an existing parent node on top of the treemap.
Add a new g element to the treemap with rect and text nodes as children:
var highlightG = canvas.append('g')
.attr("transform", "translate(" + cfg.margin.left + "," + cfg.margin.top + ")")
.append('g')
.classed('highlighter', true)
.attr('opacity',0);
highlightG.append('rect');
highlightG.append('text');
We can use the data from an existing parent node to set the appropriate position and size for the rect element and the content for the text element.
var d = d3.select('.internal.Cyprus').datum();
highlightG.select('rect')
.attr("x", d.x)
.attr("y", d.y)
.attr("width", d.dx)
.attr("height", d.dy)
highlightG
.select("text")
.attr("x", d.x + d.dx / 2 )
.attr("y", d.y + d.dy / 2 )
.attr('dy', '.35em')
.attr("text-anchor", "middle")
.text(function () {
return d.name;
})
d3.select('.internal.Cyprus').property('__data__', {
depth: 1,
dx: 185.81893466750355,
dy: 76.6094615363257,
name: "Cyprus",
value: 446770,
x: 30.123480134121516,
y: 258.33086334171067
});
var opacity = 1;
var highlightG = d3.select('svg')
.append('g')
.attr("transform", "translate(-30.1234, -258.33)")
.append('g')
.classed('highlighter', true)
.attr('opacity', 0);
highlightG.append('rect');
highlightG.append('text');
d3.select('#toggle')
.on('click', function() {
var d = d3.select('.internal.Cyprus').datum();
highlightG.attr('opacity', opacity);
highlightG.select('rect')
.attr("x", d.x)
.attr("y", d.y)
.attr("width", d.dx)
.attr("height", d.dy)
highlightG
.select("text")
.attr("x", d.x + d.dx / 2)
.attr("y", d.y + d.dy / 2)
.attr('dy', '.35em')
.attr("text-anchor", "middle")
.text(function() {
return d.name;
})
opacity = opacity ? 0 : 1;
});
.newcell text {
font-family: Arial, sans-serif;
font-size: 10px;
}
.newcell.leaf rect {
stroke: #000;
stroke-width: 0.5px;
}
.oversize {
display: none;
}
.internal text {
opacity: 0
}
.highlighter rect {
fill: #000;
fill-opacity: 0.7;
stroke: deepskyblue;
stroke-width: 5px;
}
.highlighter text {
fill: deepskyblue;
opacity: 1
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.17/d3.min.js"></script>
<p><a id="toggle" href='#'>Activate Cyprus overlay</a></p>
<svg width="186" height="77">
<g transform="translate(-30.1234, -258.33)">
<g class="newcell _2331 cell-level-1 Cyprus internal ">
<rect x="30.123480134121516" y="258.33086334171067" width="185.81893466750355" height="76.6094615363257" style="fill: rgb(100, 200, 75);"></rect>
<title>Cyprus</title>
<text x="123.0329474678733" y="296.63559410987354" dy=".35em" text-anchor="middle">Cyprus</text>
</g>
<g class="newcell _2332 cell-level-2 Cyprus leaf">
<rect x="40.12348013412152" y="268.33086334171067" width="31.51365155392795" height="23.97574841366901" style="fill: none;"></rect>
<title>Cyprus</title>
<text x="55.88030591108549" y="280.3187375485452" dy=".35em" text-anchor="middle" class="oversize">Cyprus</text>
</g>
<g class="newcell _2333 cell-level-2 Cyprus leaf">
<rect x="40.12348013412152" y="292.30661175537968" width="31.51365155392795" height="32.633713122656687" style="fill: none;"></rect>
<title>Cyprus</title>
<text x="55.88030591108549" y="308.62346831670802" dy=".35em" text-anchor="middle" class="oversize">Cyprus</text>
</g>
<g class="newcell _2334 cell-level-2 Cyprus leaf">
<rect x="71.637131688049465" y="268.33086334171067" width="55.48181226963859" height="56.60946153632569" style="fill: none;"></rect>
<title>Cyprus</title>
<text x="99.37803782286876" y="296.63559410987354" dy=".35em" text-anchor="middle">Cyprus</text>
</g>
<g class="newcell _2335 cell-level-2 Cyprus leaf">
<rect x="127.11894395768805" y="268.33086334171067" width="78.823470843937" height="56.60946153632569" style="fill: none;"></rect>
<title>Cyprus</title>
<text x="166.53067937965655" y="296.63559410987354" dy=".35em" text-anchor="middle">Cyprus</text>
</g>
</g>
</svg>
I've created a .block with more comprehensive versions of these options.
You should be able to find some combination of these that you can use for your purposes.

Related

How to create vertical continuous legend without rotating the legend

I have created the horizontal continous legend using linear gradient. But my legend can be either horizonatl or vertical based on the flag. How can i make same legend vertical but i do not want to rotate it as it takes too much space.
const svgLegend = select(divRef.current).append("svg")
const defs = svgLegend.append("defs")
const linearGradient = defs
.append("linearGradient")
.attr("id", "linear-gradient")
.attr("x1", "0%")
.attr("x2", "100%") //since it's a horizontal linear gradient
.attr("y2", "0%");
// other code to add color
svgLegend
.append("rect")
.attr("x", 25)
.attr("y", 30)
.attr("width", 250)
.attr("height", 25)
.style("fill", "url(#linear-gradient)");
const xLeg = scaleLinear().domain([min, max]).range([10, 258]);
const axisLeg = axisBottom(xLeg).tickValues(colorScale.domain());
svgLegend
.attr("class", "axis")
.append("g")
.attr("transform", "translate(15, 55)")
.style("font-size", "10px")
.style("font-weight", "700")
.call(axisLeg);
Fiddle link : https://jsfiddle.net/r1t60ges/3/
Desired Output :
<div id="container">
<svg class="axis" style="background-color: rgba(255, 255, 255, 0.8); border-radius: 5px;">
<defs>
<linearGradient id="linear-gradient" x1="0%" x2="0%" y1="0%" y2="100%">
<stop offset="0%" stop-color="#ff0000"></stop>
<stop offset="25%" stop-color="#ffff00"></stop><stop offset="50%" stop-color="#00ff00"></stop>
<stop offset="75%" stop-color="#00ffff"></stop><stop offset="100%" stop-color="#0000ff"></stop>
</linearGradient></defs>
<text class="legendTitle" x="25" y="10">Legend</text>
<rect x="25" y="20" width="25" height="150" style="fill: url("#linear-gradient");"></rect>
<g transform="translate(15, 55)" fill="none" font-size="10" font-family="sans-serif" text-anchor="middle" style="font-size: 10px; font-weight: 700;">
</svg>
</div>

How to give same classes

I have svg element of <circle> inside <g>, and I want to give each circle tag a class like this:
<g class="parent">
<circle class="circle_0"></circle>
<circle class="circle_0"></circle>
<circle class="circle_0"></circle>
<circle class="circle_0"></circle>
</g>
<g class="parent">
<circle class="circle_1"></circle>
<circle class="circle_1"></circle>
<circle class="circle_1"></circle>
<circle class="circle_1"></circle>
</g>
<g class="parent">
<circle class="circle_2"></circle>
<circle class="circle_2"></circle>
<circle class="circle_2"></circle>
<circle class="circle_2"></circle>
</g>
the number after circle_ comes from the order of its parent <g>, but each parent has the same class name.
I am creating them like this and right now, every <circle> has the same class:
var g = svg.append("g")
var parentElement = g.selectAll(".parent")
.data(data)
.enter().append("g")
.attr("class", "parent");
parentElement.selectAll(".circle")
.data(function(d,i) { return d; })
.enter().append("circle")
.attr("class", "circle")
How can I achieve this?
Since you're using D3 v3 you can use the third argument, which is the parent's index, to set your classes:
.attr("class", function(d,i,p){
return "class_" + p;
});
Check the snippet:
var data = [[1,1,1,1],[1,1,1,1],[1,1,1,1]];
var svg = d3.select("body");
var g = svg.append("g")
var parentElement = g.selectAll(".parent")
.data(data)
.enter().append("g")
.attr("class", "parent");
parentElement.selectAll(".circle")
.data(function(d,i) { return d; })
.enter().append("circle")
.attr("class", function(d,i,p){
console.log("circle_" + p);
return "circle_" + p;
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
PS: this will not work on D3 v4 (see here).

D3 re drawing adds elements again

My html after the first time I called drawLegend looks like this:
<svg>
<g class="legend">
<rect class="active-dimension-rect"></rect>
<text class="dimension-text">Some text</text>
</g>
<g class="legend">
<rect class="active-dimension-rect"></rect>
<text class="dimension-text">Some text</text>
</g>
<svg>
On window resize I call the function again, but now my html looks like this:
<svg>
<g class="legend">
<rect class="active-dimension-rect"></rect>
<text class="dimension-text">Some text</text>
<rect class="active-dimension-rect"></rect>
<text class="dimension-text">Some text</text>
</g>
<g class="legend">
<rect class="active-dimension-rect"></rect>
<text class="dimension-text">Some text</text>
<rect class="active-dimension-rect"></rect>
<text class="dimension-text">Some text</text>
</g>
<svg>
How do I only append the rect and text elements on enter? At the moment the only way I get it to work, is to actually remove the elements (rect's and text's) from the dom when I resize.
d3.select(window).on('resize', drawLegend);
function drawLegend() {
var legendScale = d3.scale.ordinal()
.domain(dimensions)
.rangeRoundBands([0, legendWidth], .1);
var legendGroups = legendSVG.selectAll(".legend").data(dimensions);
legendGroups.enter().append("g")
.attr("class", "legend");
legendGroups.attr("transform", function (d, i) {
return "translate(" + legendScale(d) + ",0)";
});
legendGroups.exit().remove();
var legendRects = legendGroups.append("rect")
.attr("width", 18)
.attr("height", 18)
.attr('class', 'active-dimension-rect')
.attr('id', function(d){
return 'dimension-rect-' + d.replace(/\s+/g, '');
})
.style({"fill": colourScale});
var legendText = legendGroups.append("text")
.attr("dy", "1em")
.attr("dx", 20)
.attr('dimension', function(d) { return d })
.attr('class', 'dimension-text')
.attr('rectId', function(d) { return'dimension-rect-' + d.replace(/\s+/g, '') })
.style({"text-anchor": "start", 'font-size': '14px'})
.text(function(d) {
visibleDimensions.push(d);
return d;
});
}

How to drag element after cloning

How to drag a cloned element after appending to parent node. I have tried with the drag method but it isn't working. I am new to d3 js and trying to go basics and examples on fiddle.
Demo: https://jsfiddle.net/pmczny6k/
code:
<html>
<head>
<title>Editor</title>
<meta http-equiv="x-ua-compatible" content="ie=9"/>
<meta http-equiv="content-type" content="text/html; charset=UTF-8"/>
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.5/d3.min.js"></script>
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/jquery/2.1.4/jquery.min.js"></script>
<script type="text/javascript">
window.onload = function ()
{
var svgContainer = d3.select("body").append("svg")
.attr("width", 200)
.attr("height", 200);
var circle = svgContainer.append("circle")
.attr("cx", 30)
.attr("cy", 30)
.attr("r", 20);
var rectangle = svgContainer.append("rect")
.attr("x", 100)
.attr("y", 100)
.attr("width", 50)
.attr("height", 50);
//clonning
function move() {
d3.select(rectangle)
.attr('x', d3.event.x - parseInt(d3.select(this).attr("width")) / 2)
.attr('y', d3.event.y - parseInt(d3.select(this).attr("height")) / 2);
this.parentNode.appendChild(this);
}
;
d3.selectAll(".drg").style("fill", "red")
.call(
d3.behavior.drag()
.on('drag', move).origin(function () {
var t = d3.select(this);
return {x: t.attr("x"), y: t.attr("y")};
})
.on('dragend', function (d) {
var elem = d3.select(this);
elem.attr("x", elem.attr("initial-x"));
elem.attr("y", elem.attr("initial-y"));
console.log(elem.attr("x"));
var mouseCoordinates = d3.mouse(this);
if (mouseCoordinates[0] > 70) {
//Append new element
d3.select("svg").append("rect")
.classed("drg", true)
.attr("width", 50)
.attr("height", 50)
.attr("x", mouseCoordinates[0])
.attr("y", mouseCoordinates[1])
.style("fill", "green");
}
})
)
};
</script>
</head>
<body>
<svg width="1024" height="768" style="background-color: #204d74">
<!--<g>-->
<rect x="10" y="20" height="250" width="300" style="fill: #080808"></rect>
<rect class="drg" x="12" y="22" initial-x="12" initial-y="22" height="50" width="50" style="fill: #f0ad4e"></rect>
<!--</g>-->
<rect x="10" y="280" height="250" width="300" style="fill: #080808"></rect>
<rect class="drg" x="12" y="282" initial-x="12" initial-y="282" height="50" width="50" style="fill: #f0ad4e"></rect>
<rect x="320" y="20" height="510" width="690" style="fill: #080808"></rect>
</svg>
</body>
</html>
Just added another call to the new created rect object:
.call(
d3.behavior.drag()
.on('drag', move).origin(function() {
var t = d3.select(this);
return {x: t.attr("x"), y: t.attr("y")};
}));
Updated fiddle.
You also have some syntax errors. Please check them on a better syntax highlighting tool as fiddle isn't a good way to do this. Hope it works now as expected.

How to apply a pattern for a D3 bar chart?

I am trying to apply a pattern to a D3 bar chart, but what I get is this:
the chart should stop exactly at 100,000
the pattern should be "fluid"
I am using a green and red pattern defined as follows:
var defs = this.svg.append("g:defs");
defs.append("g:pattern")
.attr("id", "red-fill")
.attr("patternUnits", "userSpaceOnUse")
.attr("width", "85")
.attr("height", "10")
.append("g:image")
.attr("xlink:href", "../10px-barchart-red.png")
.attr("x", 0)
.attr("y", 0)
.attr("width", 85)
.attr("height", 10);
var defs = this.svg.append("g:defs");
defs.append("g:pattern")
.attr("id", "green-fill")
.attr("patternUnits", "userSpaceOnUse")
.attr("width", "85")
.attr("height", "10")
.append("g:image")
.attr("xlink:href", "../10px-barchart-green.png")
.attr("x", 0)
.attr("y", 0)
.attr("width", 85)
.attr("height", 10);
And the plot is made with:
this.svg.selectAll("rect")
.data(dataset, getKeys)
.enter()
.append("rect")
.attr('class', 'bar')
.attr("x", function(d, i) {
return x(i) + 44;
})
.attr("y", function(d, i) {
return y(d.value);
})
.attr("width", x.rangeBand())
.attr("height", function(d, i) {
return height + padding - y(d.value);
})
.attr("fill", function(d) {
if (d.key == 0) {
return "url(#green-fill)";
} else {
return "url(#red-fill)";
}
})
This block by John Schulz successfully makes patterned bar charts that look fantastic (https://bl.ocks.org/jfsiii/7772281).
Copied the code in the below snippet for convenience and perpetuity (also added a little animation to show that it transitions well also).
var first = true;
setInterval( function(){
if(first){
d3.select('.thing-2').transition()
.delay(500)
.duration(1000)
.attr('height',20)
.attr('y',80)
}else{
d3.select('.thing-2').transition()
.delay(500)
.duration(1000)
.attr('height',100)
.attr('y',0)
}
first = !first;
},2500)
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
<!DOCTYPE html>
<html>
<head>
<meta charset=utf-8 />
<title>SVG colored patterns via mask</title>
<style>
/* FF seems to need explicit dimensions */
svg {
width: 500px;
height: 500px;
}
rect.hbar {
mask: url(#mask-stripe)
}
.thing-1 {
fill: blue;
}
.thing-2 {
fill: green;
}
</style>
</head>
<body>
<svg>
<defs>
<pattern id="pattern-stripe"
width="4" height="4"
patternUnits="userSpaceOnUse"
patternTransform="rotate(45)">
<rect width="2" height="4" transform="translate(0,0)" fill="white"></rect>
</pattern>
<mask id="mask-stripe">
<rect x="0" y="0" width="100%" height="100%" fill="url(#pattern-stripe)" />
</mask>
</defs>
<!-- bar chart -->
<rect class="hbar thing-2" x="0" y="0" width="50" height="100"></rect>
<rect class="hbar thing-2" x="51" y="50" width="50" height="50"></rect>
<rect class="hbar thing-2" x="102" y="25" width="50" height="75"></rect>
<!-- horizontal bar chart -->
<rect class="hbar thing-1" x="0" y="200" width="10" height="50"></rect>
<rect class="hbar thing-1" x="0" y="251" width="123" height="50"></rect>
<rect class="hbar thing-1" x="0" y="302" width="41" height="50"></rect>
</svg>
</body>
</html>

Resources