I'm trying to make my map look this way
Unfortunately, my code looks this way, and I don't understand why my text nodes are so gigantic not the way I want it
this is the code that I have going or check my fiddle
This code specifically doesn't seem to produce human readable labels
grp
.append("text")
.attr("fill", "#000")
.style("text-anchor", "middle")
.attr("font-family", "Verdana")
.attr("x", 0)
.attr("y", 0)
.attr("font-size", "10")
.text(function (d, i) {
return name;
});
Here's my full code:
var width = 500,
height = 275,
centered;
var projection = d3.geo
.conicConformal()
.rotate([103, 0])
.center([0, 63])
.parallels([49, 77])
.scale(500)
.translate([width / 2.5, height / 2])
.precision(0.1);
var path = d3.geo.path().projection(projection);
var svg = d3
.select("#map-selector-app")
.append("svg")
.attr("viewBox", `0 0 ${width} ${height}`);
// .attr("width", width)
// .attr("height", height);
svg
.append("rect")
.attr("class", "background-svg-map")
.attr("width", width)
.attr("height", height)
.on("click", clicked);
var g = svg.append("g");
var json = null;
var subregions = {
Western: { centroid: null },
Prairies: { centroid: null },
"Northern Territories": { centroid: null },
Ontario: { centroid: null },
Québec: { centroid: null },
Atlantic: { centroid: null },
};
d3.json(
"https://gist.githubusercontent.com/KatFishSnake/7f3dc88b0a2fa0e8c806111f983dfa60/raw/7fff9e40932feb6c0181b8f3f983edbdc80bf748/canadaprovtopo.json",
function (error, canada) {
if (error) throw error;
json = topojson.feature(canada, canada.objects.canadaprov);
g.append("g")
.attr("id", "provinces")
.selectAll("path")
.data(json.features)
.enter()
.append("path")
.attr("d", path)
.on("click", clicked);
g.append("g")
.attr("id", "province-borders")
.append("path")
.datum(
topojson.mesh(canada, canada.objects.canadaprov, function (
a,
b
) {
return a !== b;
})
)
.attr("id", "province-borders-path")
.attr("d", path);
// g.select("g")
// .selectAll("path")
// .each(function (d, i) {
// var centroid = path.centroid(d);
// });
Object.keys(subregions).forEach((rkey) => {
var p = "";
json.features.forEach(function (f, i) {
if (f.properties.subregion === rkey) {
p += path(f);
}
});
var tmp = svg.append("path").attr("d", p);
subregions[rkey].centroid = getCentroid(tmp.node());
subregions[rkey].name = rkey;
tmp.remove();
});
Object.values(subregions).forEach(({ centroid, name }) => {
var w = 80;
var h = 30;
var grp = g
.append("svg")
// .attr("width", w)
// .attr("height", h)
.attr("viewBox", `0 0 ${w} ${h}`)
.attr("x", centroid[0] - w / 2)
.attr("y", centroid[1] - h / 2);
// grp
// .append("rect")
// .attr("width", 80)
// .attr("height", 27)
// .attr("rx", 10)
// .attr("fill", "rgb(230, 230, 230)")
// .attr("stroke-width", "2")
// .attr("stroke", "#FFF");
grp
.append("text")
.attr("fill", "#000")
.style("text-anchor", "middle")
.attr("font-family", "Verdana")
.attr("x", 0)
.attr("y", 0)
.attr("font-size", "10")
.text(function (d, i) {
return name;
});
// var group = g.append("g");
// group
// .append("rect")
// .attr("x", centroid[0] - w / 2)
// .attr("y", centroid[1] - h / 2)
// .attr("width", 80)
// .attr("height", 27)
// .attr("rx", 10)
// .attr("fill", "rgb(230, 230, 230)")
// .attr("stroke-width", "2")
// .attr("stroke", "#FFF");
// group
// .append("text")
// .attr("x", centroid[0] - w / 2)
// .attr("y", centroid[1] - h / 2)
// .text(function (d, i) {
// return name;
// });
});
// g.append("button")
// .attr("class", "wrap")
// .text((d) => d.properties.name);
}
);
function getCentroid(element) {
var bbox = element.getBBox();
return [bbox.x + bbox.width / 2, bbox.y + bbox.height / 2];
}
function clicked(d) {
var x, y, k;
if (d && centered !== d) {
// CENTROIDS for subregion provinces
var p = "";
json.features.forEach(function (f, i) {
if (f.properties.subregion === d.properties.subregion) {
p += path(f);
}
});
var tmp = svg.append("path");
tmp.attr("d", p);
var centroid = getCentroid(tmp.node());
tmp.remove();
// var centroid = path.centroid(p);
x = centroid[0];
y = centroid[1];
k = 2;
if (d.properties.subregion === "Northern Territories") {
k = 1.5;
}
centered = d;
} else {
x = width / 2;
y = height / 2;
k = 1;
centered = null;
}
g.selectAll("path").classed(
"active",
centered &&
function (d) {
return (
d.properties &&
d.properties.subregion === centered.properties.subregion
);
// return d === centered;
}
);
g.transition()
.duration(650)
.attr(
"transform",
"translate(" +
width / 2 +
"," +
height / 2 +
")scale(" +
k +
")translate(" +
-x +
"," +
-y +
")"
)
.style("stroke-width", 1.5 / k + "px");
}
.background-svg-map {
fill: none;
pointer-events: all;
}
#provinces {
fill: rgb(230, 230, 230);
}
#provinces > path:hover {
fill: #0630a6;
}
#provinces .active {
fill: #0630a6;
}
#province-borders-path {
fill: none;
stroke: #fff;
stroke-width: 1.5px;
stroke-linejoin: round;
stroke-linecap: round;
pointer-events: none;
}
<script src="https://d3js.org/d3.v3.min.js"></script>
<script src="https://d3js.org/topojson.v1.min.js"></script>
<div id="map-selector-app"></div>
The basic workflow for labels with a background is:
Position a parent g
Add the text
Add the rectangle and set it's size based on the bounding box of the parent g and then move it behind the text.
For d3v3, I'm going to actually add the rectangle before the text, but not style it until the text is added and the required size is known. If we append it after it will be infront of the text. In d3v4 there's a handy .lower() method that would move it backwards for us, in d3v3, there are ways to do this, but for simplicity, to ensure the rectangle is behind the text, I'll add it first
I'm going to deviate from your code at a more foundational level in my example. I'm not going to append child SVGs as this is introducing some sizing issues for you. Also, instead of using a loop to append the labels, I'm going to use a selectAll()/enter() cycle. This means I need a data array not an object ultimately. In order to help build that array I'll use an object though - by going through your json once, we can build a list of regions and create a geojson feature for each. The geojson feature is nice as it allows us to use path.centroid() which allows us to find the centroid of a feature without additional code.
So first, I need to create the data array:
var subregions = {};
json.features.forEach(function(feature) {
var subregion = feature.properties.subregion;
// Have we already encountered this subregion? If not, add it.
if(!(subregion in subregions)) {
subregions[subregion] = {"type":"FeatureCollection", features: [] };
}
// For every feature, add it to the subregion featureCollection:
subregions[subregion].features.push(feature);
})
// Convert to an array:
subregions = Object.keys(subregions).map(function(key) {
return { name: key, geojson: subregions[key] };
})
Now we can append the parent g with a standard d3 selectAll/enter statement:
// Create a parent g for each label:
var subregionsParent = g.selectAll(null)
.data(subregions)
.enter()
.append("g")
.attr("transform", function(d) {
// position the parent, so we don't need to position each child based on geographic location:
return "translate("+path.centroid(d.geojson)+")";
})
Now we can add the text and rectangle:
// add a rectangle to each parent `g`
var boxes = subregionsParent.append("rect");
// add text to each parent `g`
subregionsParent.append("text")
.text(function(d) { return d.name; })
.attr("text-anchor","middle");
// style the boxes based on the parent `g`'s bbox
boxes
.each(function() {
var bbox = this.parentNode.getBBox();
d3.select(this)
.attr("width", bbox.width + 10)
.attr("height", bbox.height +10)
.attr("x", bbox.x - 5)
.attr("y", bbox.y - 5)
.attr("rx", 10)
.attr("ry", 10)
.attr("fill","#ccc")
})
You can see the centroid method (whether using your existing function or path.centroid()) can be a little dumb when it comes to placement given some of the overlap on the map. There are ways you could modify that - perhaps adding offsets to the data, or manual exceptions when adding the text. Though on a larger SVG there shouldn't be overlap. Annotations are notoriously difficult to do.
Here's my result with the above:
And a snippet to demonstrate (I've removed a fair amount of unnecessary code to make a simpler example, but it should retain the functionality of your example):
var width = 500,
height = 275,
centered;
var projection = d3.geo.conicConformal()
.rotate([103, 0])
.center([0, 63])
.parallels([49, 77])
.scale(500)
.translate([width / 2.5, height / 2])
var path = d3.geo.path().projection(projection);
var svg = d3
.select("#map-selector-app")
.append("svg")
.attr("viewBox", `0 0 ${width} ${height}`);
var g = svg.append("g");
d3.json("https://gist.githubusercontent.com/KatFishSnake/7f3dc88b0a2fa0e8c806111f983dfa60/raw/7fff9e40932feb6c0181b8f3f983edbdc80bf748/canadaprovtopo.json",
function (error, canada) {
if (error) throw error;
json = topojson.feature(canada, canada.objects.canadaprov);
var provinces = g.append("g")
.attr("id", "provinces")
.selectAll("path")
.data(json.features)
.enter()
.append("path")
.attr("d", path)
.on("click", clicked);
g.append("g")
.attr("id", "province-borders")
.append("path")
.datum(topojson.mesh(canada, canada.objects.canadaprov, function (a,b) { return a !== b; }))
.attr("id", "province-borders-path")
.attr("d", path);
// Add labels:
// Get the data:
var subregions = {};
json.features.forEach(function(feature) {
var subregion = feature.properties.subregion;
if(!(subregion in subregions)) {
subregions[subregion] = {"type":"FeatureCollection", features: [] };
}
subregions[subregion].features.push(feature);
})
// Convert to an array:
subregions = Object.keys(subregions).map(function(key) {
return { name: key, geojson: subregions[key] };
})
// Create a parent g for each label:
var subregionsParent = g.selectAll(null)
.data(subregions)
.enter()
.append("g")
.attr("transform", function(d) {
return "translate("+path.centroid(d.geojson)+")";
})
var boxes = subregionsParent.append("rect");
subregionsParent.append("text")
.text(function(d) { return d.name; })
.attr("text-anchor","middle");
boxes
.each(function() {
var bbox = this.parentNode.getBBox();
d3.select(this)
.attr("width", bbox.width + 10)
.attr("height", bbox.height +10)
.attr("x", bbox.x - 5)
.attr("y", bbox.y - 5)
.attr("rx", 10)
.attr("ry", 10)
.attr("fill","#ccc")
})
// End labels.
function getCentroid(element) {
var bbox = element.getBBox();
return [bbox.x + bbox.width / 2, bbox.y + bbox.height / 2];
}
function clicked(d) {
var x, y, k;
if (d && centered !== d) {
// CENTROIDS for subregion provinces
var p = "";
json.features.forEach(function (f, i) {
if (f.properties.subregion === d.properties.subregion) {
p += path(f);
}
});
var tmp = svg.append("path");
tmp.attr("d", p);
var centroid = getCentroid(tmp.node());
tmp.remove();
x = centroid[0];
y = centroid[1];
k = 2;
if (d.properties.subregion === "Northern Territories") {
k = 1.5;
}
centered = d;
} else {
x = width / 2;
y = height / 2;
k = 1;
centered = null;
}
g.selectAll("path").classed(
"active",
centered &&
function (d) {
return (
d.properties &&
d.properties.subregion === centered.properties.subregion
);
}
);
g.transition()
.duration(650)
.attr("transform","translate(" + width / 2 + "," + height / 2 +")scale(" +
k +")translate(" + -x + "," + -y + ")"
)
.style("stroke-width", 1.5 / k + "px");
}
})
.background-svg-map {
fill: none;
pointer-events: all;
}
#provinces {
fill: rgb(230, 230, 230);
}
#provinces > path:hover {
fill: #0630a6;
}
#provinces .active {
fill: #0630a6;
}
#province-borders-path {
fill: none;
stroke: #fff;
stroke-width: 1.5px;
stroke-linejoin: round;
stroke-linecap: round;
pointer-events: none;
}
<script src="https://d3js.org/d3.v3.min.js"></script>
<script src="https://d3js.org/topojson.v1.min.js"></script>
<div id="map-selector-app"></div>
Related
I am creating a choropleth for Australian food waste in D3 v5, and I am intending to create tooltip that is displayed when users hover in each state, and multiple values could be seen such as waste used for energy recovery, recycling and disposal, taken from this CSV file:
States,Energy Recovery,Disposal,Recycling,Total
ACT,53,70,0,123
New South Wales,28,80,48,156
Northern Territory,34,203,0,237
Queensland,50,143,10,203
South Australia,36,75,7,118
Tasmania,47,138,0,185
Victoria,51,108,14,173
Western Australia,29,163,29,221
Here is my code I used to create such type of tooltip. When the users hover in, the total amount, as well as the amount on columns of Energy_Recovery, Disposal and Recycling should be displayed:
const second_width = 1000;
const second_height = 850;
const projection = d3.geoMercator().center([132, -28])
.translate([second_width/2, second_height/2])
.scale(1000);
const second_color = d3.scaleQuantize().range(['#fef0d9','#fdd49e','#fdbb84','#fc8d59','#e34a33','#b30000']);
const path = d3.geoPath().projection(projection);
const second_svg = d3.select("#chart2")
.append("svg")
.attr("width", second_width)
.attr("height", second_height);
d3.csv("data/Waste_Per_State_Per_Capita(1).csv").then(function(data) {
//Set input domain for color scale
second_color.domain([
d3.min(data, function(d) { return d.Total; }),
d3.max(data, function(d) { return d.Total; })
]);
d3.json("data/aust.json").then(function(json) {
for (var i = 0; i < data.length; i++) {
var data_state = data[i].States;
//Grab data value, and convert from string to float
var dataTotal = parseFloat(data[i].Total);
var dataEnergy = parseFloat(data[i].Energy_Recovery);
var dataDisposal = parseFloat(data[i].Disposal);
var dataRecycling = parseFloat(data[i].Recycling);
for (var j = 0; j < json.features.length; j++) {
var json_state = json.features[j].properties.STATE_NAME;
if (data_state == json_state) {
//Copy the data value into the JSON
json.features[j].properties.value = dataTotal;
//Stop looking through the JSON
break;
}
}
}
// create tooltip
var second_tooltip = second_svg.append("g")
.attr("class", "tooltip")
.style("display", "none");
second_tooltip.append("rect")
.attr("width", 30)
.attr("height", 20)
.attr("fill", "white")
.style("opacity", 0.5);
second_tooltip.append("text")
.attr("dy", "1.2em")
.style("text-anchor", "middle")
.attr("font-size", "12px");
second_svg.selectAll("path")
.data(json.features)
.enter()
.append("path")
.attr("d", path)
.attr("class", "state")
.style("fill", function(d) {
//Get data value
var value = d.properties.value;
if (value) {
//If value exists…
return second_color(value);
} else {
//If value is undefined…
return "#ccc";
}
})
.on('mouseover', function(d) {
var second_xPosition = parseFloat(d3.select(this).attr("x"));
var second_yPosition = parseFloat(d3.select(this).attr("y"));
second_tooltip.attr("x", second_xPosition)
.attr("y", second_yPosition);
second_tooltip.style("display", "block");
second_tooltip.select("text").text(d.properties.STATE_NAME + " "
+ "Amount of food waste per capita: " + d.properties.value + "(kg/year)");
})
.on('mouseout', function(d) {
second_tooltip.style('display', 'none');
});
})
(my JSON file is taken from here:https://gist.github.com/GerardoFurtado/02aa65e5522104cb692e)
However, when I tried to run that in the server, not only my tooltip displayed in a wrong position (in the upper-left corner of the svg file rather than in the same place when I hover the mouse in), it could only shows the total value of waste per capita, as in this image:
I would like to ask that, are there any method that I could use to fix this code, so that I could display tooltip in the place that I hover my mouse in, with all necessary data that I cited earlier?
Thank you!
Try this (see the snippet in full-page):
const w = 850;
const h = 700;
//Define map projection // geoEqualEarth
const projection = d3.geoMercator()
.center([ 132, -28 ])
.translate([ w/2, h/2 ])
.scale(1000);
//Define path generator
const path = d3.geoPath()
.projection(projection);
const color = ['#8dd3c7','#ffffb3','#bebada','#fb8072','#80b1d3','#fdb462','#b3de69','#fccde5','#d9d9d9'];
//Create SVG
const svg = d3.select("svg")
.attr('width', w)
.attr('height', h)
//Load in GeoJSON data
d3.json('https://gist.githubusercontent.com/GerardoFurtado/02aa65e5522104cb692e/raw/8108fbd4103a827e67444381ff594f7df8450411/aust.json')
.then(json => onGeoJsonLoaded(json))
.catch(err => console.log('ERROR: ', err));
const onGeoJsonLoaded = json => {
//Bind data and create one path per GeoJSON feature
const states = svg.selectAll('g.state')
.data(json.features)
.enter()
.append('g')
.classed('state', true);
states.append('path')
.attr("d", path)
.attr("stroke", 'white')
.attr("fill", (d, i) => color[i]);
//States
states.append("text")
.attr("fill", "darkslategray")
.attr("transform", function(d) { return "translate(" + path.centroid(d) + ")"; })
.attr("text-anchor", "middle")
.attr("dy", 15)
.text(function(d) {
return d.properties.STATE_NAME;
});
//Append the name
/*
svg.append("text")
.attr("x", 446)
.attr("y", 340)
.attr("font-size", 90)
.attr("font-weight", "bold")
.attr("font-family", "Roboto")
.attr("text-anchor", "middle")
.attr("opacity", 0.10)
.text("AUSTRALIA");
*/
d3.json('https://api.jsonbin.io/b/60af2dc3d0f4985540524d62')
.then(dataJson => onDataJsonLoaded(dataJson))
.catch(err => console.log('ERR: ', err));
}
const tooltipPath = (width, height, offset, radius) => {
const left = -width / 2
const right = width / 2
const top = -offset - height
const bottom = -offset
return `M 0,0
L ${-offset},${bottom}
H ${left + radius}
Q ${left},${bottom} ${left},${bottom - radius}
V ${top + radius}
Q ${left},${top} ${left + radius},${top}
H ${right - radius}
Q ${right},${top} ${right},${top + radius}
V ${bottom - radius}
Q ${right},${bottom} ${right - radius},${bottom}
H ${offset}
L 0,0 z`
}
const onDataJsonLoaded = json => {
console.log('DATA: ', json);
const rows = Object.keys(json[0]).filter(n => n !== 'State');
const tooltip = svg.append('g')
.classed('tooltip', true)
.style('visibility', 'hidden');
tooltip.append('path')
.attr('d', tooltipPath(150, 80, 5, 5))
rows.forEach((row, index) => {
tooltip.append('text')
.text(`${row} :`)
.attr('x', -70)
.attr('y', -68 + index * 18);
tooltip.append('text')
.classed(row.replace(' ', '_'), true)
.attr('x', 40)
.attr('y', -68 + index * 18)
}
);
svg.selectAll('g.state')
.on('mouseenter', d => {
const stateData = json.find(s => s.State === d.properties.STATE_NAME);
rows.forEach(row => tooltip.select(`.${row.replace(' ', '_')}`).text(stateData[row]));
tooltip.attr('transform', `translate(${path.centroid(d)})`);
tooltip.style('visibility', 'visible');
})
.on('mouseleave', () => tooltip.style('visibility', 'hidden'));
};
.tooltip > path {
fill: white;
stroke: black;
}
.tooltip > text {
font-family: "Ubuntu";
font-size: 12px;
fill: black;
stroke: none;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<svg/>
I am trying to add a delete node feature in this jsfiddle
The refresh method is adding on("mousedown", mousedownNode) event to each circle, but when I click the node GW2 it deletes the DB node. I figured out that mousedownNode method deletes the correct node but, node = node.data(nodes); in the refresh method is messing up. I am not sure how to fix this problem.
function mousedownNode(d, i) {
nodes.splice(i, 1);
links = links.filter(function(l) {
return l.source !== d && l.target !== d;
});
d3.event.stopPropagation();
refresh();
}
EDIT
I have a map which is indexing the node name to index mapping :
var map = {}
nodes.forEach(function(d,i){
map[d.id] = i;
})
links.forEach(function(d) {
d.source = map[d.source];
d.target = map[d.target];
})
As you said, the correct nodes are getting deleted and the data bound to the <g class="node"></g> also seems correct BUT you're not UPDATING the node texts.
This is what you have:
nodeEnter
.append("text")
.attr("dx", 12)
.attr("dy", ".35em")
.text(function(d) {
return d.id;
});
This is what I've changed it to:
nodeEnter
.append("text")
.attr("dx", 12)
.attr("dy", ".35em");
node.select('text')
.text(function(d) {
return d.id;
});
You can also change this by assigning the nodeText to a variable like this var nodeText = ...
But here's the point, when you call the refresh after say deleting a node, the exit() selection works BUT the existing nodes DO NOT update by themselves. Hope this makes sense. Read How Selections Work
JSFIDDLE and here's the snippet:
EDIT: Removing a draggable SVG element results in an error. To fix, change the exit selection line to:
node.exit().on('mousedown.drag', null).remove();
that removes the drag event bound before removing the element.
rect {
fill: none;
pointer-events: all;
}
.node {
fill: #000;
}
.cursor {
fill: green;
stroke: brown;
pointer-events: none;
}
.link {
stroke: #999;
}
.node text {
pointer-events: none;
font: 10px sans-serif;
}
path.link {
fill: none;
stroke: #666;
stroke-width: 1.5px;
}
<script src="https://d3js.org/d3.v3.min.js"></script>
<button id="ref" onclick="refresh()">refresh </button>
<script>
//setInterval(refresh, 15000);
function addNodeCanvas(nodeName,g) {
var node = { x: 900, y: 900, id: nodeName, grp:g };
var n = nodes.push(node);
console.log(node);
refresh();
}
function addLinkCanvas(idSrc, idTarget) {
if (idSrc != idTarget) {
var s = {}, t = {};
nodes.forEach(function(curNode) {
if (typeof curNode.id != "undefined") {
if (curNode.id == idSrc) { s = curNode; }
if (curNode.id == idTarget) { t = curNode; }
}
});
//console.log( { s,t});
links.push({ source: s, target: t });
};
refresh();
}
var fill = d3.scale.category20();
var links = [{ source: "FH", target: "TP" }];
var nodes = [
{ id: "FH", x: 100, y: 110 },
{ id: "TP", x: 200, y: 110 },
{ id: "GW1", x: 200, y: 110 },
{ id: "GW", x: 200, y: 110 },
{ id: "GW2", x: 200, y: 110 },
{ id: "DB", x: 100, y: 110 }
]
var width = 600,
height = 400,
radius = 8;
var map = {}
nodes.forEach(function(d,i){
map[d.id] = i;
})
links.forEach(function(d) {
d.source = map[d.source];
d.target = map[d.target];
})
var force = d3.layout
.force()
.size([width, height])
.nodes(nodes) .links(links)
.linkDistance(50)
.charge(-100)
.on("tick", tick);
var svg = d3
.select("body")
.append("svg")
.attr("width", width)
.attr("height", height);
// build the arrow.
var arrows = svg
.append("svg:defs")
.selectAll("marker")
.data(["arrow"]) // Different link/path types can be defined here
.enter()
.append("svg:marker") // This section adds in the arrows
.attr("id", String)
.attr("viewBox", "0 -5 10 10")
.attr("refX", 10)
.attr("refY", -1)
.attr("markerWidth", 4)
.attr("markerHeight", 4)
.attr("orient", "auto")
.append("svg:path")
.attr("d", "M0,-5L10,0L0,5");
svg
.append("rect")
.attr("width", width)
.attr("height", height);
var nodes = force.nodes(),
links = force.links(),
node = svg.selectAll(".node"),
link = svg.selectAll(".link");
function tick() {
link.attr("d", function(d) {
var dx = d.target.x - d.source.x,
dy = d.target.y - d.source.y;
var angle = Math.atan2(dy, dx);
var offsetX = radius * Math.cos(angle);
var offsetY = radius * Math.sin(angle);
dr = Math.sqrt(dx * dx + dy * dy);
return (
"M" +
(d.source.x + offsetX) +
"," +
(d.source.y + offsetY) +
"A" +
dr +
"," +
dr +
" 0 0,1 " +
(d.target.x - offsetX) +
"," +
(d.target.y - offsetY)
);
});
node.attr("transform", function(d) {
return "translate(" + d.x + "," + d.y + ")";
});
}
function mousedownNode(d, i) {
nodes.splice(i, 1);
links = links.filter(function(l) {
return l.source !== d && l.target !== d;
});
d3.event.stopPropagation();
refresh();
}
refresh();
function refresh() {
link = link.data(links);
link
.enter()
.append("path")
.attr("class", "link")
.attr("marker-end", "url(#arrow)");
link.exit().remove();
node = node.data(nodes);
var nodeEnter = node
.enter()
.insert("g")
.attr("class", "node")
.on("mousedown", mousedownNode)
.call(force.drag);
nodeEnter
.append("image")
.attr("xlink:href", "https://github.com/favicon.ico")
.attr("x", -8)
.attr("y", -8)
.attr("width", 16)
.attr("height", 16);
nodeEnter
.append("text")
.attr("dx", 12)
.attr("dy", ".35em");
node.select('text')
.text(function(d) {
return d.id;
});
node.exit().on('mousedown.drag', null).remove();
force.start();
}
</script>
I didn't dig into this much, as I felt like the problem was quite obvious to me. I can't exactly explain why the GW node returns but it's something to do with your data-binding being incorrect I believe.
When you're doing your data join you're basically telling D3 to key on an index. Now the original GW node in the DOM is going to be re-used if it weren't the last in the index because the indexes will be offset by 1.
node = node.data(nodes);
If you change this to key on a field
node = node.data(nodes, d => d.id);
You'll find your problem goes away.
Here is a parallel coordinate with some sample data
http://plnkr.co/edit/dCNuBsaDNBwr7CrAJUBe?p=preview
Here the numerical axis like column1 , column3 have small number of data so it looks ok. But in practice, I have a big data (something like 600 entries), so numerical ticks in axis become clustered , overlapped.
Is there a way so that it looks good. Like Setting the number of ticks in parallel coordinate. For instance, if I want to change the number of ticks in column1 of this example from 10 to 5.
Any help would be appreciated.
Thanks
<!DOCTYPE html>
<meta charset="utf-8">
<style>
svg {
font: 10px sans-serif;
}
.background path {
fill: none;
stroke: #ddd;
stroke-opacity: .4;
shape-rendering: crispEdges;
}
.foreground path {
fill: none;
stroke: steelblue;
stroke-opacity: .7;
}
.brush .extent {
fill-opacity: .3;
stroke: #fff;
shape-rendering: crispEdges;
}
.axis line,
.axis path {
fill: none;
stroke: #000;
shape-rendering: crispEdges;
}
.axis text {
text-shadow: 0 1px 0 #fff, 1px 0 0 #fff, 0 -1px 0 #fff, -1px 0 0 #fff;
cursor: move;
}
</style>
<body>
<script src="http://d3js.org/d3.v4.min.js"></script>
<script>
var margin = {top: 30, right: 10, bottom: 10, left: 10},
width = 600 - margin.left - margin.right,
height = 200 - margin.top - margin.bottom;
var x = d3.scalePoint().rangeRound([0, width]).padding(1),
y = {},
dragging = {};
var line = d3.line(),
//axis = d3.axisLeft(x),
background,
foreground,
extents;
var container = d3.select("body").append("div")
.attr("class", "parcoords")
.style("width", width + margin.left + margin.right + "px")
.style("height", height + margin.top + margin.bottom + "px");
var svg = container.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 + ")");
var quant_p = function(v){return (parseFloat(v) == v) || (v == "")};
d3.json("convertcsv.json", function(error, cars) {
dimensions = d3.keys(cars[0]);
x.domain(dimensions);
dimensions.forEach(function(d) {
var vals = cars.map(function(p) {return p[d];});
if (vals.every(quant_p)){
y[d] = d3.scaleLinear()
.domain(d3.extent(cars, function(p) {
return +p[d]; }))
.range([height, 0])
console.log(y[d]);
}
else{
vals.sort();
y[d] = d3.scalePoint()
.domain(vals.filter(function(v, i) {return vals.indexOf(v) == i;}))
.range([height, 0],1);
}
})
extents = dimensions.map(function(p) { return [0,0]; });
// Add grey background lines for context.
background = svg.append("g")
.attr("class", "background")
.selectAll("path")
.data(cars)
.enter().append("path")
.attr("d", path);
// Add blue foreground lines for focus.
foreground = svg.append("g")
.attr("class", "foreground")
.selectAll("path")
.data(cars)
.enter().append("path")
.attr("d", path);
// Add a group element for each dimension.
var g = svg.selectAll(".dimension")
.data(dimensions)
.enter().append("g")
.attr("class", "dimension")
.attr("transform", function(d) { return "translate(" + x(d) + ")"; })
.call(d3.drag()
.subject(function(d) { return {x: x(d)}; })
.on("start", function(d) {
dragging[d] = x(d);
background.attr("visibility", "hidden");
})
.on("drag", function(d) {
dragging[d] = Math.min(width, Math.max(0, d3.event.x));
foreground.attr("d", path);
dimensions.sort(function(a, b) { return position(a) - position(b); });
x.domain(dimensions);
g.attr("transform", function(d) { return "translate(" + position(d) + ")"; })
})
.on("end", function(d) {
delete dragging[d];
transition(d3.select(this)).attr("transform", "translate(" + x(d) + ")");
transition(foreground).attr("d", path);
background
.attr("d", path)
.transition()
.delay(500)
.duration(0)
.attr("visibility", null);
}));
// Add an axis and title.
var g = svg.selectAll(".dimension");
g.append("g")
.attr("class", "axis")
.each(function(d) { d3.select(this).call(d3.axisLeft(y[d]));})
//text does not show up because previous line breaks somehow
.append("text")
.attr("fill", "black")
.style("text-anchor", "middle")
.attr("y", -9)
.text(function(d) { return d; });
// Add and store a brush for each axis.
g.append("g")
.attr("class", "brush")
.each(function(d) {
if(y[d].name == 'r'){
// console.log(this);
d3.select(this).call(y[d].brush = d3.brushY().extent([[-8, 0], [8,height]]).on("brush start", brushstart).on("brush", go_brush).on("brush", brush_parallel_chart).on("end", brush_end));
}
else if(y[d].name == 'n')
d3.select(this).call(y[d].brush = d3.brushY().extent([[-8, 0], [15,height]]).on("brush start", brushstart).on("brush", go_brush).on("brush", brush_parallel).on("end", brush_end_ordinal));
})
.selectAll("rect")
.attr("x", -8)
.attr("width", 16);
}); // closing
function position(d) {
var v = dragging[d];
return v == null ? x(d) : v;
}
function transition(g) {
return g.transition().duration(500);
}
// Returns the path for a given data point.
function path(d) {
return line(dimensions.map(function(p) { return [position(p), y[p](d[p])]; }));
}
function go_brush() {
d3.event.sourceEvent.stopPropagation();
}
invertExtent = function(y) {
return domain.filter(function(d, i) { return y === range[i]; });
};
function brushstart(selectionName) {
foreground.style("display", "none")
//console.log(selectionName);
var dimensionsIndex = dimensions.indexOf(selectionName);
//console.log(dimensionsIndex);
extents[dimensionsIndex] = [0, 0];
foreground.style("display", function(d) {
return dimensions.every(function(p, i) {
if(extents[i][0]==0 && extents[i][0]==0) {
return true;
}
return extents[i][1] <= d[p] && d[p] <= extents[i][0];
}) ? null : "none";
});
}
// Handles a brush event, toggling the display of foreground lines.
function brush_parallel_chart() {
for(var i=0;i<dimensions.length;++i){
if(d3.event.target==y[dimensions[i]].brush) {
//if (d3.event.sourceEvent.type === "brush") return;
extents[i]=d3.event.selection.map(y[dimensions[i]].invert,y[dimensions[i]]);
}
}
foreground.style("display", function(d) {
return dimensions.every(function(p, i) {
if(extents[i][0]==0 && extents[i][0]==0) {
return true;
}
return extents[i][1] <= d[p] && d[p] <= extents[i][0];
}) ? null : "none";
});
}
function brush_end(){
if (!d3.event.sourceEvent) return; // Only transition after input.
if (!d3.event.selection) return; // Ignore empty selections.
for(var i=0;i<dimensions.length;++i){
if(d3.event.target==y[dimensions[i]].brush) {
extents[i]=d3.event.selection.map(y[dimensions[i]].invert,y[dimensions[i]]);
extents[i][0] = Math.round( extents[i][0] * 10 ) / 10;
extents[i][1] = Math.round( extents[i][1] * 10 ) / 10;
d3.select(this).transition().call(d3.event.target.move, extents[i].map(y[dimensions[i]]));
}
}
}
// brush for ordinal cases
function brush_parallel() {
for(var i=0;i<dimensions.length;++i){
if(d3.event.target==y[dimensions[i]].brush) {
var yScale = y[dimensions[i]];
var selected = yScale.domain().filter(function(d){
// var s = d3.event.target.extent();
var s = d3.event.selection;
return (s[0] <= yScale(d)) && (yScale(d) <= s[1])
});
var temp = selected.sort();
extents[i] = [temp[temp.length-1], temp[0]];
}
}
foreground.style("display", function(d) {
return dimensions.every(function(p, i) {
if(extents[i][0]==0 && extents[i][0]==0) {
return true;
}
//var p_new = (y[p].ticks)?d[p]:y[p](d[p]);
//return extents[i][1] <= p_new && p_new <= extents[i][0];
return extents[i][1] <= d[p] && d[p] <= extents[i][0];
}) ? null : "none";
});
}
function brush_end_ordinal(){
console.log("hhhhh");
if (!d3.event.sourceEvent) return; // Only transition after input.
if (!d3.event.selection) return; // Ignore empty selections.
for(var i=0;i<dimensions.length;++i){
if(d3.event.target==y[dimensions[i]].brush) {
var yScale = y[dimensions[i]];
var selected = yScale.domain().filter(function(d){
// var s = d3.event.target.extent();
var s = d3.event.selection;
return (s[0] <= yScale(d)) && (yScale(d) <= s[1])
});
var temp = selected.sort();
extents[i] = [temp[temp.length-1], temp[0]];
if(selected.length >1)
d3.select(this).transition().call(d3.event.target.move, extents[i].map(y[dimensions[i]]));
}
}
}
</script>
You can either use d3-axis.ticks() or d3-axis.tickValues([]) to specify custom number of ticks/tick values.
For example, for column1 using tickValues and column3 using ticks:
var g = svg.selectAll(".dimension");
g.append("g")
.attr("class", "axis")
.each(function(d) {
if(d === 'column1') {
d3.select(this).call(d3.axisLeft(y[d]).tickValues([.1, .2, .3, .5,.8]));
} else if(d === 'column3') {
d3.select(this).call(d3.axisLeft(y[d]).ticks(4));
} else {
d3.select(this).call(d3.axisLeft(y[d]));
}
})
A fork of your plunkr: TickValues and ticks DEMO
Hope this helps. Read the docs for more options.
I want to develop "drill down" "pie chart" using "D3.JS". I found the below sample, and perfect to me to use.
D3 Js sample drill down pie chart
The above sample is perfect fit for me.
Additionally from the sample drill down pie chart, I want to place TEXT LABEL in each section of pie chart divided.
I followed many samples,
Sample 1 - pie chart with section text label
Sample 2 - JSFiddle - pie chart with section text label
Based on the above samples for placing text label in pie chart sections, I followed the below code, I tried adding sample "var dataSet", "var arcs = svg.selectAll("g.slice")" code.
But, when I execute the program, it doesn't display any text label in centre of each section in pie chart.
Could someone guide me to fix this please?
<!doctype html>
<html>
<head>
<head>
<meta charset="utf-8">
<title>Drill down pie chart test</title>
<script type="text/javascript" src="http://d3js.org/d3.v2.min.js?2.9.6"></script>
<style type="text/css">
body {
text-align: center;
padding: 50px;
font-family: "Helvetica Neue",Arial,Sans-serif;
font-weight: 200;
color: #333;
}
.header {
font-size: 20px;
}
.sector {
cursor: pointer;
}
.slice text {
font-size: 16pt;
font-family: Arial;
}
</style>
</head>
<body>
<script type="text/javascript">
// Globals
var width = 500,
height = 400,
margin = 50,
radius = Math.min(width - margin, height - margin) / 2,
// Pie layout will use the "val" property of each data object entry
pieChart = d3.layout.pie().sort(null).value(function(d){return d.val;}),
arc = d3.svg.arc().outerRadius(radius),
MAX_SECTORS = 15, // Less than 20 please
colors = d3.scale.category20();
var dataSet = [
{"legendLabel":"One", "magnitude":20},
{"legendLabel":"Two", "magnitude":40},
{"legendLabel":"Three", "magnitude":50},
{"legendLabel":"Four", "magnitude":16},
{"legendLabel":"Five", "magnitude":50},
{"legendLabel":"Six", "magnitude":8},
{"legendLabel":"Seven", "magnitude":30}];
//mydata = {"Medical", "Agriculture", "Security"};
var st = {};
st.data = [{"label":"less than a week","value":169,"pos":0},{"label":"1 week - 30 days","value":1,"pos":1},{"label":"30 - 90 days","value":22,"pos":2},{"label":"90 - 180 days","value":35,"pos":3},{"label":"180 days - 1 year","value":47,"pos":4},{"label":"more than 1 year","value":783,"pos":5}] ;
// Synthetic data generation ------------------------------------------------
var data = [];
var numSectors = 8; //Math.ceil(Math.random()*MAX_SECTORS);
for(i = -1; i++ < numSectors; ) {
var children = [];
var numChildSectors = Math.ceil(Math.random()*MAX_SECTORS);
var color = colors(i);
for( j=-1; j++ < numChildSectors; ){
// Add children categories with shades of the parent color
children.push(
{ cat: "cat"+((i+1)*100+j),
val: Math.random(),
color: d3.rgb(color).darker(1/(j+1))
});
}
data.push({
cat: "cat"+i,
val: Math.random(),
color: color,
children: children});
}
// --------------------------------------------------------------------------
// SVG elements init
var svg = d3.select("body").append("svg").data([dataSet]).attr("width", width).attr("height", height),
defs = svg.append("svg:defs"),
// .data(pieChart)
// Declare a main gradient with the dimensions for all gradient entries to refer
mainGrad = defs.append("svg:radialGradient")
.attr("gradientUnits", "userSpaceOnUse")
.attr("cx", 0).attr("cy", 0).attr("r", radius).attr("fx", 0).attr("fy", 0)
.attr("id", "master"),
// The pie sectors container
arcGroup = svg.append("svg:g")
.attr("class", "arcGroup")
.attr("filter", "url(#shadow)")
.attr("transform", "translate(" + (width / 2) + "," + (height / 2) + ")"),
// Header text
header = svg.append("text").text("Biotechnology")
.attr("transform", "translate(10, 20)").attr("class", "header");
//svg.append("text").attr("text-anchor", "middle").text("$" + "sample"),
//svg.append("text").text("sample").attr("text-anchor", "middle")
/*svg.append("text")
.attr("transform", "translate(" + arcGroup.centroid(d) + ")")
.attr("dy", ".35em")
.attr("text-anchor", "middle")
.text("sample");
*/
// Declare shadow filter
var shadow = defs.append("filter").attr("id", "shadow")
.attr("filterUnits", "userSpaceOnUse")
.attr("x", -1*(width / 2)).attr("y", -1*(height / 2))
.attr("width", width).attr("height", height);
shadow.append("feGaussianBlur")
.attr("in", "SourceAlpha")
.attr("stdDeviation", "4")
.attr("result", "blur");
shadow.append("feOffset")
.attr("in", "blur")
.attr("dx", "4").attr("dy", "4")
.attr("result", "offsetBlur");
shadow.append("feBlend")
.attr("in", "SourceGraphic")
.attr("in2", "offsetBlur")
.attr("mode", "normal");
/* var arcs = svg.selectAll("g.slice")
arcs.append("text")
.attr("transform", function(d) { return "translate(" + arc.centroid(d) + ")"; })
.attr("dy", ".35em")
.attr("text-anchor", "middle")
.text(function(d) { return d.value; });
*/
// Redraw the graph given a certain level of data
function updateGraph(cat){
var currData = data;
// Simple header text
if(cat != undefined){
currData = findChildenByCat(cat);
d3.select(".header").text("Biotechnology → "+cat);
} else {
d3.select(".header").text("Biotechnology");
}
// Create a gradient for each entry (each entry identified by its unique category)
var gradients = defs.selectAll(".gradient").data(currData, function(d){return d.cat;});
gradients.enter().append("svg:radialGradient")
.attr("id", function(d, i) { return "gradient" + d.cat; })
.attr("class", "gradient")
.attr("xlink:href", "#master");
gradients.append("svg:stop").attr("offset", "0%").attr("stop-color", getColor );
gradients.append("svg:stop").attr("offset", "90%").attr("stop-color", getColor );
gradients.append("svg:stop").attr("offset", "100%").attr("stop-color", getDarkerColor );
/*var arcs = defs.selectAll("g.slice").data(pie).enter().append("svg:g").attr("class","slice");
arcs.append("svg:text").attr("transform", function(d){
d.innerRadius = 0;
d.outerRadius = r;
return "translate(" + arc.centroid(d) + ")";}).attr("text-anchor", "middle").text( function(d, i) {
return (data[i].value / tot ) * 100 > 10 ? ((data[i].value / tot ) * 100).toFixed(1) + "%" : "";
}
).attr("fill","#fff")
.classed("slice-label",true);
*/
// Create a sector for each entry in the enter selection
var paths = arcGroup.selectAll("path")
.data(pieChart(currData), function(d) {return d.data.cat;} );
paths.enter().append("svg:path").attr("class", "sector");
// Each sector will refer to its gradient fill
paths.attr("fill", function(d, i) { return "url(#gradient"+d.data.cat+")"; })
.transition().duration(1000).attrTween("d", tweenIn).each("end", function(){
this._listenToEvents = true;
});
// Mouse interaction handling
paths.on("click", function(d){
if(this._listenToEvents){
// Reset inmediatelly
d3.select(this).attr("transform", "translate(0,0)")
// Change level on click if no transition has started
paths.each(function(){
this._listenToEvents = false;
});
updateGraph(d.data.children? d.data.cat : undefined);
}
})
.on("mouseover", function(d){
// Mouseover effect if no transition has started
if(this._listenToEvents){
// Calculate angle bisector
var ang = d.startAngle + (d.endAngle - d.startAngle)/2;
// Transformate to SVG space
ang = (ang - (Math.PI / 2) ) * -1;
// Calculate a 10% radius displacement
var x = Math.cos(ang) * radius * 0.1;
var y = Math.sin(ang) * radius * -0.1;
d3.select(this).transition()
.duration(250).attr("transform", "translate("+x+","+y+")");
}
})
.on("mouseout", function(d){
// Mouseout effect if no transition has started
if(this._listenToEvents){
d3.select(this).transition()
.duration(150).attr("transform", "translate(0,0)");
}
});
// Collapse sectors for the exit selection
paths.exit().transition()
.duration(1000)
.attrTween("d", tweenOut).remove();
// NEWLY ADDED START
// Select all <g> elements with class slice (there aren't any yet)
var arcs = svg.selectAll("g.slice")
// Associate the generated pie data (an array of arcs, each having startAngle,
// endAngle and value properties)
.data(pie)
// This will create <g> elements for every "extra" data element that should be associated
// with a selection. The result is creating a <g> for every object in the data array
.enter()
// Create a group to hold each slice (we will have a <path> and a <text>
// element associated with each slice)
.append("svg:g")
.attr("class", "slice"); //allow us to style things in the slices (like text)
arcs.append("svg:path")
//set the color for each slice to be chosen from the color function defined above
.attr("fill", function(d, i) { return color(i); } )
//this creates the actual SVG path using the associated data (pie) with the arc drawing function
.attr("d", arc);
// Add a legendLabel to each arc slice...
arcs.append("svg:text")
.attr("transform", function(d) { //set the label's origin to the center of the arc
//we have to make sure to set these before calling arc.centroid
d.outerRadius = outerRadius + 50; // Set Outer Coordinate
d.innerRadius = outerRadius + 45; // Set Inner Coordinate
return "translate(" + arc.centroid(d) + ")";
})
.attr("text-anchor", "middle") //center the text on it's origin
.style("fill", "Purple")
.style("font", "bold 12px Arial")
<!-- .text(function(d, i) { return dataSet[i].legendLabel; }); //get the label from our original dat -->
.text(function(d, i) { return "Test"; }); //get the label from our original dat
// Add a magnitude value to the larger arcs, translated to the arc centroid and rotated.
arcs.filter(function(d) { return d.endAngle - d.startAngle > .2; }).append("svg:text")
.attr("dy", ".35em")
.attr("text-anchor", "middle")
//.attr("transform", function(d) { return "translate(" + arc.centroid(d) + ")rotate(" + angle(d) + ")"; })
.attr("transform", function(d) { //set the label's origin to the center of the arc
//we have to make sure to set these before calling arc.centroid
d.outerRadius = outerRadius; // Set Outer Coordinate
d.innerRadius = outerRadius/2; // Set Inner Coordinate
return "translate(" + arc.centroid(d) + ")rotate(" + angle(d) + ")";
})
.style("fill", "White")
.style("font", "bold 12px Arial")
.text(function(d) { return d.data.magnitude; });
function angle(d) {
var a = (d.startAngle + d.endAngle) * 90 / Math.PI - 90;
return a > 90 ? a - 180 : a;
}
// END
}
// "Fold" pie sectors by tweening its current start/end angles
// into 2*PI
function tweenOut(data) {
data.startAngle = data.endAngle = (2 * Math.PI);
var interpolation = d3.interpolate(this._current, data);
this._current = interpolation(0);
return function(t) {
return arc(interpolation(t));
};
}
// "Unfold" pie sectors by tweening its start/end angles
// from 0 into their final calculated values
function tweenIn(data) {
var interpolation = d3.interpolate({startAngle: 0, endAngle: 0}, data);
this._current = interpolation(0);
return function(t) {
return arc(interpolation(t));
};
}
// Helper function to extract color from data object
function getColor(data, index){
return data.color;
}
// Helper function to extract a darker version of the color
function getDarkerColor(data, index){
return d3.rgb(getColor(data, index)).darker();
}
function findChildenByCat(cat){
for(i=-1; i++ < data.length - 1; ){
if(data[i].cat == cat){
return data[i].children;
}
}
return data;
}
//.text(function(d, i) { return categorydata[i].label; });
// Start by updating graph at root level
updateGraph();
</script>
<!-- <p>Drill down pie chart test by Marc Baiges Camprubí (marcbc#gmail.com) in D3.js -->
</body>
</html>
Instead of doing this:
var arcs = svg.selectAll("g.slice")
do this
var arcs = arcGroup.selectAll("g.slice")
reason so that the text label and the path of the pie all are in same group.
Give proper inner and outer radius for placing the labels in the center (so that the centroid is calculated on basis of the new inner outer radius of the arc)
arcs.append("svg:text")
.attr("transform", function(d) { //set the label's origin to the center of the arc
//we have to make sure to set these before calling arc.centroid
d.outerRadius = radius - 20; // Set Outer Coordinate
d.innerRadius = radius - 100; // Set Inner Coordinate
return "translate(" + arc.centroid(d) + ")";
})
Next give proper data in the text:
.text(function(d, i) { return "Test"; }); //get the label from our original data
do this
.text(function(d, i) { return d.data.cat; }); //get the label from our original data
working code here
Is there a way to add zoomable treemap in RAW. Using the enter-update-exit pattern of d3.js does not make sense in RAW, so without this functionality, is it possible to add d3.js zoomable treemap in RAW.js
The RAW docs in adding new chart says
In this way, D3's enter-update-exit pattern does not make too much
sense within RAW's charts, since the selection is always empty when
passed to the draw function. Since RAW is meant to be a tool for the
production of non-interactive visualizations, to be elaborated using
vector-graphics tools, this should not be perceived as a limitation,
but, at the contrary, as a way to simplify charts' drawing code.
I have been able to add zoomable treemap in RAW. Following is the code.
zoomableTreemap.js
(function(){
var tree = raw.models.tree();
var chart = raw.chart()
.title('Zoomable Treemap')
.description(
"A space filling visualization of data hierarchies and proportion between elements. The different hierarchical levels create visual clusters through the subdivision into rectangles proportionally to each element's value. Treemaps are useful to represent the different proportion of nested hierarchical data structures.<br/>Based on <a href='http://bl.ocks.org/mbostock/4063582'>http://bl.ocks.org/mbostock/4063582</a>")
.thumbnail("/raw/imgs/treemap.png")
.category('Hierarchies')
.model(tree)
var rawWidth = chart.number()
.title('Width')
.defaultValue(100)
.fitToWidth(true)
var rawHeight = chart.number()
.title("Height")
.defaultValue(500)
var padding = chart.number()
.title("Padding")
.defaultValue(0)
var colors = chart.color()
.title("Color scale")
chart.draw(function (selection, root){
root.name = 'ZoomableTree';
var margin = {top: 20, right: 0, bottom: 0, left: 0},
width = +rawWidth(),
height = +rawHeight() - margin.top - margin.bottom,
formatNumber = d3.format(",d"),
transitioning;
var x = d3.scale.linear()
.domain([0, width])
.range([0, width]);
var y = d3.scale.linear()
.domain([0, height])
.range([0, height]);
console.log(padding());
console.log(+padding());
var treemap = d3.layout.treemap()
.padding(+padding())
.children(function(d, depth) { return depth ? null : d._children; })
.sort(function(a, b) { return a.value - b.value; })
.ratio(height / width * 0.5 * (1 + Math.sqrt(5)))
// Values are required in d3 treemap layout
// and our DB table do not have values field in it, so we are going to use 1 for all nodes.
// The value decides the size/area of rectangle in d3 treemap layout so effectively we are going to have
// even sized rectangles
.value(function(d) { return 1; })
.round(false);
var svg = selection
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.bottom + margin.top)
.style("margin-left", -margin.left + "px")
.style("margin.right", -margin.right + "px")
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")")
.style("shape-rendering", "crispEdges")
var grandparent = svg.append("g")
.attr("class", "grandparent");
grandparent.append("rect")
.attr("y", -margin.top)
.attr("width", width)
.attr("height", margin.top)
.style("fill", function (d) { return colors()(d.color); })
.style("stroke","#fff")
grandparent.append("text")
.attr("x", 6)
.attr("y", 6 - margin.top)
.attr("dy", ".75em");
initialize(root);
//console.log(root);
//throw '';
accumulate(root);
layout(root);
display(root);
function initialize(root) {
root.x = root.y = 0;
root.dx = width;
root.dy = height;
root.depth = 0;
}
// Aggregate the values for internal nodes. This is normally done by the
// treemap layout, but not here because of our custom implementation.
// We also take a snapshot of the original children (_children) to avoid
// the children being overwritten when when layout is computed.
function accumulate(d) {
return (d._children = d.children)
? d.value = d.children.reduce(function(p, v) { return p + accumulate(v); }, 0)
: d.value;
}
// Compute the treemap layout recursively such that each group of siblings
// uses the same size (1×1) rather than the dimensions of the parent cell.
// This optimizes the layout for the current zoom state. Note that a wrapper
// object is created for the parent node for each group of siblings so that
// the parent’s dimensions are not discarded as we recurse. Since each group
// of sibling was laid out in 1×1, we must rescale to fit using absolute
// coordinates. This lets us use a viewport to zoom.
function layout(d) {
if (d._children) {
//console.log(d);
//throw 'stop';
treemap.nodes({_children: d._children});
d._children.forEach(function(c) {
//console.log(d);
c.x = d.x + c.x * d.dx;
c.y = d.y + c.y * d.dy;
c.dx *= d.dx;
c.dy *= d.dy;
c.parent = d;
//console.log(c);
layout(c);
});
}
}
function display(d) {
grandparent
.datum(d.parent)
.on("click", transition)
.select("text")
.text(name(d));
var g1 = svg.insert("g", ".grandparent")
.datum(d)
.attr("class", "depth");
var g = g1.selectAll("g")
.data(d._children)
.enter().append("g");
g.filter(function(d) { return d._children; })
.classed("children", true)
.on("click", transition);
g.selectAll(".child")
.data(function(d) { return d._children || [d]; })
.enter().append("rect")
.attr("class", "child")
.call(rect);
g.append("rect")
.attr("class", "parent")
.call(rect)
.append("title")
.text(function(d) { return formatNumber(d.value); })
.style("fill", function (d) { return colors()(d.color); })
.style("stroke","#fff")
g.append("text")
.attr("dy", ".75em")
.text(function(d) { return d.name; })
.call(text);
function transition(d) {
if (transitioning || !d) return;
transitioning = true;
var g2 = display(d),
t1 = g1.transition().duration(750),
t2 = g2.transition().duration(750);
// Update the domain only after entering new elements.
x.domain([d.x, d.x + d.dx]);
y.domain([d.y, d.y + d.dy]);
// Enable anti-aliasing during the transition.
svg.style("shape-rendering", null);
// Draw child nodes on top of parent nodes.
svg.selectAll(".depth").sort(function(a, b) { return a.depth - b.depth; });
// Fade-in entering text.
g2.selectAll("text").style("fill-opacity", 0);
// Transition to the new view.
t1.selectAll("text").call(text).style("fill-opacity", 0);
t2.selectAll("text").call(text).style("fill-opacity", 1);
t1.selectAll("rect").call(rect);
t2.selectAll("rect").call(rect);
// Remove the old node when the transition is finished.
t1.remove().each("end", function() {
svg.style("shape-rendering", "crispEdges");
transitioning = false;
});
}
return g;
}
function text(text) {
text.attr("x", function(d) { return x(d.x) + 6; })
.attr("y", function(d) { return y(d.y) + 6; });
}
function rect(rect) {
rect.attr("x", function(d) { return x(d.x); })
.attr("y", function(d) { return y(d.y); })
.attr("width", function(d) { return x(d.x + d.dx) - x(d.x); })
.attr("height", function(d) { return y(d.y + d.dy) - y(d.y); })
.style("fill", function (d) { return colors()(d.color); })
.style("stroke","#fff");
}
function name(d) {
return d.parent
? name(d.parent) + "." + d.name
: d.name;
}
})
})();
chart.css
#chart {
background: #ddd;
}
text {
pointer-events: none;
}
.grandparent text {
font-weight: bold;
}
rect {
fill: none;
stroke: #fff;
}
rect.parent,
.grandparent rect {
stroke-width: 2px;
}
.grandparent rect {
fill: orange;
}
.grandparent:hover rect {
fill: #ee9700;
}
.children rect.parent,
.grandparent rect {
cursor: pointer;
}
.children rect.parent {
fill: #bbb;
fill-opacity: .5;
}
.children:hover rect.child {
fill: #bbb;
}
Index.html
Just include css and js in index page of raw.