Data Join with Custom Key does not work as expected - d3.js

I am plotting some points using d3. I want to change the shape off all the points based on some condition. The join looks a bit like this:
var data=[{x:10,y:10}, {x:20, y:30}];
var shape = "rect";
...
var point = svg.selectAll(".point")
.data(data, function(d, idx) { return "row_" + idx + "_shape_" + shape;})
;
The d3 enter() and exit() selections do not seem to reflect any changes caused by "shape" changing.
Fiddle is here: http://jsfiddle.net/schmoo2k/jcpctbty/

You need to be aware that the key function is calculated on the selection with this as the SVG element and then on the data with the data array as this.
I think maybe this is what you are trying to do...
var data = [{
x: 10,
y: 10
}, {
x: 20,
y: 30
}];
var svg = d3.select("body").append("svg")
.attr("width", 500)
.attr("height", 500);
function update(data, shape) {
var point = svg.selectAll(".point")
.data(data, function(d, idx) {
var key = "row_" + idx + "_shape_" + (Array.isArray(this) ? "Data: " + shape :
d3.select(this).attr("shape"));
alert(key);
return key;
});
alert("enter selection size: " + point.enter().size());
point.enter().append(shape)
.attr("class", "point")
.style("fill", "red")
.attr("shape", shape);
switch (shape) {
case "rect":
point.attr("x", function(d) {
return d.x;
})
.attr("y", function(d) {
return d.y;
})
.attr("width", 5)
.attr("height", 5);
break;
case "circle":
point.attr("cx", function(d) {
return d.x;
})
.attr("cy", function(d) {
return d.y;
})
.attr("r", 5);
break;
}
point.exit().remove();
}
update(data, "rect");
setTimeout(function() {
update(data, "circle");
}, 5000);
text {
font: bold 48px monospace;
}
.enter {
fill: green;
}
.update {
fill: #333;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.5/d3.js"></script>
Abstracted version
Just to tidy things up here is a more readable and idiomatic version (including fixing a problem with the text element)...
var data = [{
x: 10,
y: 10,
}, {
x: 20,
y: 30,
}];
var svg = d3.select("body").append("svg")
.attr("width", 500)
.attr("height", 500),
marker = Marker();
function update(data, shape) {
var point = svg.selectAll(".point")
.data(data, key("shape", shape)),
enter = point.enter().append("g")
.attr("class", "point")
.attr("transform", function(d) {
return "translate(" + d.x + "," + d.y + ")"
})
.attr("shape", shape);
enter.append(shape)
.style("fill", "red")
.attr(marker.width[shape], 5)
.attr(marker.height[shape], 5);
enter.append("text")
.attr({
"class": "title",
dx: 10,
"text-anchor": "start"
})
.text(shape);
point.exit().remove();
}
update(data, "rect");
setTimeout(function() {
update(data, "circle");
}, 2000);
function Marker() {
return {
width: {
rect: "width",
circle: "r"
},
height: {
rect: "height",
circle: "r"
},
shape: function(d) {
return d.shape
},
};
}
function key(attr, value) {
//join data and elements where value of attr is value
function _phase(that) {
return Array.isArray(that) ? "data" : "element";
}
function _Type(that) {
return {
data: value,
get element() {
return d3.select(that).attr(attr)
}
}
}
return function(d, i, j) {
var _value = _Type(this)
return i + "_" + _value[_phase(this)];
};
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
Generalised, data-driven approach
var data = [{
x: 10,
y: 10,
}, {
x: 20,
y: 30,
}];
var svg = d3.select("body").append("svg")
.attr("width", 500)
.attr("height", 500),
marker = Marker();
function update(data, shape) {
//data-driven approach
data.forEach(function(d, i) {
d.shape = shape[i]
});
var log = [],
point = svg.selectAll(".point")
.data(data, key({
shape: marker.shape,
transform: marker.transform
}, log)),
//UPDATE
update = point.classed("update", true),
updateSize = update.size();
update.selectAll("text").transition().duration(1000).style("fill", "#ccc");
update.selectAll(".shape").transition().duration(1000).style("fill", "#ccc")
//ENTER
var enter = point.enter().append("g")
.classed("point enter", true)
.attr("transform", marker.dock)
.attr("shape", marker.shape),
//UPDATE+ENTER
// ... not required on this occasion
updateAndEnter = point.classed("update-enter", true);
//EXIT
var exit = point.exit().classed("exit", true);
exit.selectAll("text").transition().duration(1000).style("fill", "red");
exit.selectAll(".shape").transition().duration(1000).style("fill", "red");
exit.transition().delay(1000.).duration(1000)
.attr("transform", marker.dock)
.remove();
//ADJUSTMENTS
enter.each(function(d) {
//append the specified shape for each data element
//wrap in each so that attr can be a function of the data
d3.select(this).append(marker.shape(d))
.style("fill", "green")
.classed("shape", true)
.attr(marker.width[marker.shape(d)], 5)
.attr(marker.height[marker.shape(d)], 5)
});
enter.append("text")
.attr({
"class": "title",
dx: 10,
"text-anchor": "start"
})
.text(marker.shape)
.style("fill", "green")
.style("opacity", 1);
enter.transition().delay(1000).duration(2000)
.attr("transform", marker.transform);
}
data = generateData(40, 10)
update(data, data.map(function(d, i) {
return ["rect", "circle"][Math.round(Math.random())]
}));
setInterval(function() {
update(data, data.map(function(d, i) {
return ["rect", "circle"][Math.round(Math.random())]
}));
}, 5000);
function generateData(n, p) {
var values = [];
for (var i = 0; i < n; i++) {
values.push({
x: (i + 1) * p,
y: (i + 1) * p
})
}
return values;
};
function Marker() {
return {
x: {
rect: "x",
circle: "cx"
},
y: {
rect: "y",
circle: "cy"
},
width: {
rect: "width",
circle: "r"
},
height: {
rect: "height",
circle: "r"
},
shape: function(d) {
return d.shape
},
transform: function(d) {
return "translate(" + f(d.x) + "," + f(d.y) + ")"
},
dock: function(d) {
return "translate(" + (d.x + 800) + "," + (d.y + 100) + ")"
}
};
function f(x) {
return d3.format(".0f")(x)
}
}
function key(attr, value, log) {
//join data and elements where value of attr is value
function _phase(that) {
return Array.isArray(that) ? "data" : "element";
}
function _Key(that) {
if (plural) {
return {
data: function(d, i, j) {
var a, key = "";
for (a in attr) {
key += (typeof attr[a] === "function" ? attr[a](d, i, j) : attr[a]);
}
return key;
},
element: function() {
var a, key = "";
for (a in attr) {
key += d3.select(that).attr(a);
}
return key;
}
}
} else {
return {
data: function(d, i, j) {
return typeof value === "function" ? value(d, i, j) : value;
},
element: function() {
return d3.select(that).attr(attr)
}
}
}
}
var plural = typeof attr === "object";
if (plural && arguments.length === 2) log = value;
return function(d, i, j) {
var key = _Key(this)[_phase(this)](d, i, j);
if (log) log.push(i + "_" + _phase(this) + "_" + key);
return key;
};
}
text {
font: bold 12px monospace;
fill: black;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>

Related

D3js horizontal bar chart: how to add numbers from data at the end of every bar?

I have this code, which draw a bar chart. I'm trying to put the corresponding value inside the bars but in the very end of each bar. I haven't been able to accomplish it. Can you please help me with this? Any suggestion will be highly appreciated.
I'll leave a running snippet so that you can see it and tell where to start to place the values at the end of the bars but inside it.
$(window).on('resize', function (event) {
$("#chart").width(window.innerWidth * 0.9);
$("#chart").height(window.innerHeight);
});
function horizontalGroupBarChart(config) {
function setReSizeEvent(data) {
var resizeTimer;
var interval = 500;
window.removeEventListener('resize', function () {
});
window.addEventListener('resize', function (event) {
if (resizeTimer !== false) {
clearTimeout(resizeTimer);
}
resizeTimer = setTimeout(function () {
$(data.mainDiv).empty();
drawHorizontalGroupBarChartChart(data);
clearTimeout(resizeTimer);
}, interval);
});
}
drawHorizontalGroupBarChartChart(config);
setReSizeEvent(config);
}
function createhorizontalGroupBarChartLegend(mainDiv, columnsInfo, colorRange) {
var z = d3.scaleOrdinal()
.range(colorRange);
var mainDivName = mainDiv.substr(1, mainDiv.length);
$(mainDiv).before("<div id='Legend_" + mainDivName + "' class='pmd-card-body' style='margin-top:0; margin-bottom:0;text-align:center'></div>");
var keys = Object.keys(columnsInfo);
keys.forEach(function (d) {
var cloloCode = z(d);
$("#Legend_" + mainDivName).append("<span class='team-graph team1' style='display: inline-block; margin-right:10px;'>\
<span style='background:" + cloloCode + ";width: 10px;height: 10px;display: inline-block;vertical-align: middle;'> </span>\
<span style='padding-top: 0;font-family:Source Sans Pro, sans-serif;font-size: 13px;display: inline;'>" + columnsInfo[d] + " </span>\
</span>");
});
}
function drawHorizontalGroupBarChartChart(config) {
var data = config.data;
var columnsInfo = config.columnsInfo;
var xAxis = config.xAxis;
var yAxis = config.yAxis;
var colorRange = config.colorRange;
var mainDiv = config.mainDiv;
var mainDivName = mainDiv.substr(1, mainDiv.length);
var label = config.label;
var requireLegend = config.requireLegend;
d3.select(mainDiv).append("svg").attr("width", $(mainDiv).width()).attr("height", $(mainDiv).height() * 0.80).attr("class","mainSVG")
var svg = d3.select(mainDiv + " svg"),
margin = { top: 20, right: 20, bottom: 40, left: 40 },
width = +svg.attr("width") - margin.left - margin.right,
height = +svg.attr("height") - margin.top - margin.bottom;
var g = svg.append("g").attr("transform", "translate(" +( margin.left*2.3) + "," + margin.top + ")");
if (requireLegend != null && requireLegend != undefined && requireLegend != false) {
$("#Legend_" + mainDivName).remove();
createhorizontalGroupBarChartLegend(mainDiv, columnsInfo, colorRange);
}
$(".mainSVG").attr("transform","translate(5,10)")
var y0 = d3.scaleBand()
.rangeRound([height, 0])
.paddingInner(0.1);
var y1 = d3.scaleBand()
.padding(0.05);
var x = d3.scaleLinear()
.rangeRound([0, width - margin.left ]);
var z = d3.scaleOrdinal()
.range(colorRange);
var keys = Object.keys(columnsInfo);
y0.domain(data.map(function (d) {
return d[yAxis];
}));
y1.domain(keys).rangeRound([0, y0.bandwidth()]);
x.domain([0, d3.max(data, function (d) {
return d3.max(keys, function (key) {
return d[key];
});
})]).nice();
var maxTicks = d3.max(data, function (d) {
return d3.max(keys, function (key) {
return d[key];
});
});
var element = g.append("g")
.selectAll("g")
.data(data)
.enter().append("g")
.attr("transform", function (d) {
return "translate(0," + y0(d[yAxis]) + ")";
});
var rect = element.selectAll("rect")
.data(function (d, i) {
return keys.map(function (key) {
return { key: key, value: d[key], index: key + "_" + i + "_" + d[yAxis] };
});
})
.enter().append("rect")
.attr("y", function (d) {
return y1(d.key);
})
.attr("width", function (d) {
return x(d.value);
})
.attr("data-index", function (d, i) {
return d.index;
})
.attr("height", y1.bandwidth())
.attr("fill", function (d) {
return z(d.key);
})
var datax = [0,1,2,3,4,5,6,7,8,9,10,11,12];
var tScale= d3.scaleLinear()
.rangeRound([0, width - margin.left ]);
tScale.domain(d3.extent(datax)).nice();
//CBT:add tooltips
var self = {};
self.svg = svg;
self.cssPrefix = "horgroupBar0_";
self.data = data;
self.keys = keys;
self.height = height;
self.width = width;
self.label = label;
self.yAxis = yAxis;
self.xAxis = xAxis;
horBarTooltip.addTooltips(self);
rect.on("mouseover", function () {
var currentEl = d3.select(this);
var index = currentEl.attr("data-index");
horBarTooltip.showTooltip(self, index);
});
rect.on("mouseout", function () {
var currentEl = d3.select(this);
var index = currentEl.attr("data-index");
horBarTooltip.hideTooltip(self, index);
});
rect.on("mousemove", function () {
horBarTooltip.moveTooltip(self);
});
g.append("g")
.attr("class", "axis")
.attr("transform", "translate(0," + height + ")")
.call(d3.axisBottom(x).ticks(maxTicks))
.append("text")
.attr("x", width / 2)
.attr("y", margin.bottom * 0.7)
.attr("dx", "0.32em")
.attr("fill", "#000")
.attr("font-weight", "bold")
.attr("text-anchor", "start")
g.append("g")
.attr("class", "axis")
.call(d3.axisLeft(y0).ticks(null, "s"))
.append("text")
.attr("x", height * 0.4 * -1)
.attr("y", margin.left * 0.8 * -1)//y(y.ticks().pop()) + 0.5)
.attr("dy", "0.71em")
.attr("fill", "#00338D")
.attr("font-weight", "bold")
// .attr("text-anchor", "start")
}
var helpers = {
getDimensions: function (id) {
var el = document.getElementById(id);
var w = 0, h = 0;
if (el) {
var dimensions = el.getBBox();
w = dimensions.width;
h = dimensions.height;
} else {
console.log("error: getDimensions() " + id + " not found.");
}
return { w: w, h: h };
}
}
var horBarTooltip = {
addTooltips: function (pie) {
var keys = pie.keys;
// group the label groups (label, percentage, value) into a single element for simpler positioning
var element = pie.svg.append("g")
.selectAll("g")
.data(pie.data)
.enter().append("g")
.attr("class", function (d, i) {
return pie.cssPrefix + "tooltips" + "_" + i
});
tooltips = element.selectAll("g")
.data(function (d, i) {
return keys.map(function (key) {
return { key: key, value: d[key], index: key + "_" + i + "_" + d[pie.yAxis] };
});
})
.enter()
.append("g")
.attr("class", pie.cssPrefix + "tooltip")
.attr("id", function (d, i) {
return pie.cssPrefix + "tooltip" + d.index;
})
.style("opacity", 0)
.append("rect")
.attr("rx", 2)
.attr("ry", 2)
.attr("x", -2)
.attr("opacity", 0.71)
.style("fill", "#000000");
element.selectAll("g")
.data(function (d, i) {
return keys.map(function (key) {
return { key: key, value: d[key], index: key + "_" + i + "_" + d[pie.yAxis] };
});
})
.append("text")
.attr("fill", function (d) {
return "#efefef"
})
.style("font-size", function (d) {
return 10;
})
.style("font-family", function (d) {
return "arial";
})
.text(function (d, i) {
var caption = "" + pie.label.xAxis + ":{value}";
return horBarTooltip.replacePlaceholders(pie, caption, i, {
value: d.value,
});
});
element.selectAll("g rect")
.attr("width", function (d, i) {
var dims = helpers.getDimensions(pie.cssPrefix + "tooltip" + d.index);
return dims.w + (2 * 4);
})
.attr("height", function (d, i) {
var dims = helpers.getDimensions(pie.cssPrefix + "tooltip" + d.index);
return dims.h + (2 * 4);
})
.attr("y", function (d, i) {
var dims = helpers.getDimensions(pie.cssPrefix + "tooltip" + d.index);
return -(dims.h / 2) + 1;
});
},
showTooltip: function (pie, index) {
var fadeInSpeed = 250;
if (horBarTooltip.currentTooltip === index) {
fadeInSpeed = 1;
}
horBarTooltip.currentTooltip = index;
d3.select("#" + pie.cssPrefix + "tooltip" + index)
.transition()
.duration(fadeInSpeed)
.style("opacity", function () {
return 1;
});
horBarTooltip.moveTooltip(pie);
},
moveTooltip: function (pie) {
d3.selectAll("#" + pie.cssPrefix + "tooltip" + horBarTooltip.currentTooltip)
.attr("transform", function (d) {
var mouseCoords = d3.mouse(this.parentNode);
var x = mouseCoords[0] + 4 + 2;
var y = mouseCoords[1] - (2 * 4) - 2;
return "translate(" + x + "," + y + ")";
});
},
hideTooltip: function (pie, index) {
d3.select("#" + pie.cssPrefix + "tooltip" + index)
.style("opacity", function () {
return 0;
});
// move the tooltip offscreen. This ensures that when the user next mouseovers the segment the hidden
// element won't interfere
d3.select("#" + pie.cssPrefix + "tooltip" + horBarTooltip.currentTooltip)
.attr("transform", function (d, i) {
// klutzy, but it accounts for tooltip padding which could push it onscreen
var x = pie.width + 1000;
var y = pie.height + 1000;
return "translate(" + x + "," + y + ")";
});
},
replacePlaceholders: function (pie, str, index, replacements) {
var replacer = function () {
return function (match) {
var placeholder = arguments[1];
if (replacements.hasOwnProperty(placeholder)) {
return replacements[arguments[1]];
} else {
return arguments[0];
}
};
};
return str.replace(/\{(\w+)\}/g, replacer(replacements));
}
};
var groupChartData = [{ "num": 1, "over": "Singapore" }, { "num": 1.3, "over": "The Netherlands" }, { "num": 2, "over": "United Kingdom" }, { "num": 2.4, "over": "United States"}, { "num": 2.6, "over": "New Zealand" }, { "num": 2.8, "over": "Sweden" }, { "num": 3, "over": "Canada"}, { "num": 3, "over": "UAE" }, { "num": 4, "over": "Australia" }, { "num": 4.4, "over": "France" },{ "num": 5, "over": "South Korea" },{ "num": 5.2, "over": "Germany" },{ "num": 5.5, "over": "Austria" },{ "num": 6, "over": "Austria" },{ "num": 7, "over": "Brazil" },{ "num": 7, "over": "China" },{ "num": 8, "over": "Japan" },{ "num": 10, "over": "Russia" },{ "num": 11, "over": "Mexico" },{ "num": 12, "over": "India" },];
var columnsInfo = { "num": "<span class='mainTitle KPMGWeb-ExtraLight'>Technology & innovation pillar: score by country</span>" };
$("#chart").empty();
var barChartConfig = {
mainDiv: "#chart",
colorRange: ["#0091DA", "#6D2077"],
data: groupChartData,
columnsInfo: columnsInfo,
xAxis: "runs",
yAxis: "over",
label: {
xAxis: "",
yAxis: ""
},
requireLegend: true
};
var groupChart = new horizontalGroupBarChart(barChartConfig);
.mainTitle{
font-size: 3em;
}
<script src="https://code.jquery.com/jquery-latest.min.js"></script>
<script src="https://d3js.org/d3.v4.min.js"></script>
<div id="chart" style="width: 800;height: 600">
Thank you very much!
It sounds like you were almost there with adding the labels. You want to add them to the g element that holds the rect bars, and add them after the rects so they will appear on top of the bars, so:
var element = g.append("g")
.selectAll("g")
.data(data)
.enter().append("g")
.attr("transform", function(d) {
return "translate(0," + y0(d[yAxis]) + ")";
});
var rect = element.selectAll("rect")
.data(function(d, i) {
return keys.map(function(key) {
return {
key: key,
value: d[key],
index: key + "_" + i + "_" + d[yAxis]
};
});
})
.enter().append("rect")
.attr("width", function (d) {
return x(d.value);
})
[ etc. ]
// add the text elements
element.append('text')
The text element needs to be at the end of the bar, and the bar width is x(d.value) using the data bound to the rect elements. d.value translates to d.num in terms of the data you already have attached to the g elements, so we can set the x attribute as x(d.num). If the text-anchor is set to end, that will align the end of the text with the end of the bar; we want a little space between text and end of bar, so add in a small offset:
element.append('text')
.attr('text-anchor', 'end')
.attr('x', d => x(d.num) - 5)
The value to be shown in the bar is also going to be d.num, so we can add that:
element.append('text')
.attr('text-anchor', 'end')
.attr('x', d => x(d.num) - 5)
.text(d => d.num )
If you run the code now, you'll find that the numbers are partially obscured by the bar above, so let's sort out the y offset. The bar width is y1.bandwidth(); to align the baseline of the text with the bottom of the bar, add
element.append('text')
.attr('text-anchor', 'end')
.attr('x', d => x(d.num) - 5)
.attr('y', y1.bandwidth())
.text(d => d.num )
Depending on what size you anticipate your users viewing the chart at, you might want to try to centre the text over the bar -- e.g. try
element.append('text')
.attr('text-anchor', 'end')
.attr('x', d => x(d.num) - 5)
.attr('y', y1.bandwidth()/2)
.attr('dy', '0.25em')
.text(d => d.num )
Here's a working example:
$(window).on('resize', function(event) {
$("#chart").width(window.innerWidth * 0.9);
$("#chart").height(window.innerHeight);
});
function horizontalGroupBarChart(config) {
function setReSizeEvent(data) {
var resizeTimer;
var interval = 500;
window.removeEventListener('resize', function() {});
window.addEventListener('resize', function(event) {
if (resizeTimer !== false) {
clearTimeout(resizeTimer);
}
resizeTimer = setTimeout(function() {
$(data.mainDiv).empty();
drawHorizontalGroupBarChartChart(data);
clearTimeout(resizeTimer);
}, interval);
});
}
drawHorizontalGroupBarChartChart(config);
setReSizeEvent(config);
}
function createhorizontalGroupBarChartLegend(mainDiv, columnsInfo, colorRange) {
var z = d3.scaleOrdinal()
.range(colorRange);
var mainDivName = mainDiv.substr(1, mainDiv.length);
$(mainDiv).before("<div id='Legend_" + mainDivName + "' class='pmd-card-body' style='margin-top:0; margin-bottom:0;text-align:center'></div>");
var keys = Object.keys(columnsInfo);
keys.forEach(function(d) {
var cloloCode = z(d);
$("#Legend_" + mainDivName).append("<span class='team-graph team1' style='display: inline-block; margin-right:10px;'>\
<span style='background:" + cloloCode + ";width: 10px;height: 10px;display: inline-block;vertical-align: middle;'> </span>\
<span style='padding-top: 0;font-family:Source Sans Pro, sans-serif;font-size: 13px;display: inline;'>" + columnsInfo[d] + " </span>\
</span>");
});
}
function drawHorizontalGroupBarChartChart(config) {
var data = config.data;
var columnsInfo = config.columnsInfo;
var xAxis = config.xAxis;
var yAxis = config.yAxis;
var colorRange = config.colorRange;
var mainDiv = config.mainDiv;
var mainDivName = mainDiv.substr(1, mainDiv.length);
var label = config.label;
var requireLegend = config.requireLegend;
d3.select(mainDiv).append("svg").attr("width", $(mainDiv).width()).attr("height", $(mainDiv).height() * 0.80).attr("class", "mainSVG")
var svg = d3.select(mainDiv + " svg"),
margin = {
top: 20,
right: 20,
bottom: 40,
left: 40
},
width = +svg.attr("width") - margin.left - margin.right,
height = +svg.attr("height") - margin.top - margin.bottom;
var g = svg.append("g").attr("transform", "translate(" + (margin.left * 2.3) + "," + margin.top + ")");
if (requireLegend != null && requireLegend != undefined && requireLegend != false) {
$("#Legend_" + mainDivName).remove();
createhorizontalGroupBarChartLegend(mainDiv, columnsInfo, colorRange);
}
$(".mainSVG").attr("transform", "translate(5,10)")
var y0 = d3.scaleBand()
.rangeRound([height, 0])
.paddingInner(0.1);
var y1 = d3.scaleBand()
.padding(0.05);
var x = d3.scaleLinear()
.rangeRound([0, width - margin.left]);
var z = d3.scaleOrdinal()
.range(colorRange);
var keys = Object.keys(columnsInfo);
y0.domain(data.map(function(d) {
return d[yAxis];
}));
y1.domain(keys).rangeRound([0, y0.bandwidth()]);
x.domain([0, d3.max(data, function(d) {
return d3.max(keys, function(key) {
return d[key];
});
})]).nice();
var maxTicks = d3.max(data, function(d) {
return d3.max(keys, function(key) {
return d[key];
});
});
var element = g.append("g")
.selectAll("g")
.data(data)
.enter().append("g")
.attr("transform", function(d) {
return "translate(0," + y0(d[yAxis]) + ")";
});
var rect = element.selectAll("rect")
.data(function(d, i) {
return keys.map(function(key) {
return {
key: key,
value: d[key],
index: key + "_" + i + "_" + d[yAxis]
};
});
})
.enter().append("rect")
.attr("y", function(d) {
return y1(d.key);
})
.attr("width", function(d) {
return x(d.value);
})
.attr("data-index", function(d, i) {
return d.index;
})
.attr("height", y1.bandwidth())
.attr("fill", function(d) {
return z(d.key);
})
element.append('text')
.attr('x', d => x(d.num) - 5)
.attr('y', y1.bandwidth()/2)
.attr('dy', '0.35em')
.attr('text-anchor', 'end')
.text(d => d.num )
var datax = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12];
var tScale = d3.scaleLinear()
.rangeRound([0, width - margin.left]);
tScale.domain(d3.extent(datax)).nice();
//CBT:add tooltips
var self = {};
self.svg = svg;
self.cssPrefix = "horgroupBar0_";
self.data = data;
self.keys = keys;
self.height = height;
self.width = width;
self.label = label;
self.yAxis = yAxis;
self.xAxis = xAxis;
horBarTooltip.addTooltips(self);
rect.on("mouseover", function() {
var currentEl = d3.select(this);
var index = currentEl.attr("data-index");
horBarTooltip.showTooltip(self, index);
});
rect.on("mouseout", function() {
var currentEl = d3.select(this);
var index = currentEl.attr("data-index");
horBarTooltip.hideTooltip(self, index);
});
rect.on("mousemove", function() {
horBarTooltip.moveTooltip(self);
});
g.append("g")
.attr("class", "axis")
.attr("transform", "translate(0," + height + ")")
.call(d3.axisBottom(x).ticks(maxTicks))
.append("text")
.attr("x", width / 2)
.attr("y", margin.bottom * 0.7)
.attr("dx", "0.32em")
.attr("fill", "#000")
.attr("font-weight", "bold")
.attr("text-anchor", "start")
g.append("g")
.attr("class", "axis")
.call(d3.axisLeft(y0).ticks(null, "s"))
.append("text")
.attr("x", height * 0.4 * -1)
.attr("y", margin.left * 0.8 * -1) //y(y.ticks().pop()) + 0.5)
.attr("dy", "0.71em")
.attr("fill", "#00338D")
.attr("font-weight", "bold")
// .attr("text-anchor", "start")
}
var helpers = {
getDimensions: function(id) {
var el = document.getElementById(id);
var w = 0,
h = 0;
if (el) {
var dimensions = el.getBBox();
w = dimensions.width;
h = dimensions.height;
} else {
console.log("error: getDimensions() " + id + " not found.");
}
return {
w: w,
h: h
};
}
}
var horBarTooltip = {
addTooltips: function(pie) {
var keys = pie.keys;
// group the label groups (label, percentage, value) into a single element for simpler positioning
var element = pie.svg.append("g")
.selectAll("g")
.data(pie.data)
.enter().append("g")
.attr("class", function(d, i) {
return pie.cssPrefix + "tooltips" + "_" + i
});
tooltips = element.selectAll("g")
.data(function(d, i) {
return keys.map(function(key) {
return {
key: key,
value: d[key],
index: key + "_" + i + "_" + d[pie.yAxis]
};
});
})
.enter()
.append("g")
.attr("class", pie.cssPrefix + "tooltip")
.attr("id", function(d, i) {
return pie.cssPrefix + "tooltip" + d.index;
})
.style("opacity", 0)
.append("rect")
.attr("rx", 2)
.attr("ry", 2)
.attr("x", -2)
.attr("opacity", 0.71)
.style("fill", "#000000");
element.selectAll("g")
.data(function(d, i) {
return keys.map(function(key) {
return {
key: key,
value: d[key],
index: key + "_" + i + "_" + d[pie.yAxis]
};
});
})
.append("text")
.attr("fill", function(d) {
return "#efefef"
})
.style("font-size", function(d) {
return 10;
})
.style("font-family", function(d) {
return "arial";
})
.text(function(d, i) {
var caption = "" + pie.label.xAxis + ":{value}";
return horBarTooltip.replacePlaceholders(pie, caption, i, {
value: d.value,
});
});
element.selectAll("g rect")
.attr("width", function(d, i) {
var dims = helpers.getDimensions(pie.cssPrefix + "tooltip" + d.index);
return dims.w + (2 * 4);
})
.attr("height", function(d, i) {
var dims = helpers.getDimensions(pie.cssPrefix + "tooltip" + d.index);
return dims.h + (2 * 4);
})
.attr("y", function(d, i) {
var dims = helpers.getDimensions(pie.cssPrefix + "tooltip" + d.index);
return -(dims.h / 2) + 1;
});
},
showTooltip: function(pie, index) {
var fadeInSpeed = 250;
if (horBarTooltip.currentTooltip === index) {
fadeInSpeed = 1;
}
horBarTooltip.currentTooltip = index;
d3.select("#" + pie.cssPrefix + "tooltip" + index)
.transition()
.duration(fadeInSpeed)
.style("opacity", function() {
return 1;
});
horBarTooltip.moveTooltip(pie);
},
moveTooltip: function(pie) {
d3.selectAll("#" + pie.cssPrefix + "tooltip" + horBarTooltip.currentTooltip)
.attr("transform", function(d) {
var mouseCoords = d3.mouse(this.parentNode);
var x = mouseCoords[0] + 4 + 2;
var y = mouseCoords[1] - (2 * 4) - 2;
return "translate(" + x + "," + y + ")";
});
},
hideTooltip: function(pie, index) {
d3.select("#" + pie.cssPrefix + "tooltip" + index)
.style("opacity", function() {
return 0;
});
// move the tooltip offscreen. This ensures that when the user next mouseovers the segment the hidden
// element won't interfere
d3.select("#" + pie.cssPrefix + "tooltip" + horBarTooltip.currentTooltip)
.attr("transform", function(d, i) {
// klutzy, but it accounts for tooltip padding which could push it onscreen
var x = pie.width + 1000;
var y = pie.height + 1000;
return "translate(" + x + "," + y + ")";
});
},
replacePlaceholders: function(pie, str, index, replacements) {
var replacer = function() {
return function(match) {
var placeholder = arguments[1];
if (replacements.hasOwnProperty(placeholder)) {
return replacements[arguments[1]];
} else {
return arguments[0];
}
};
};
return str.replace(/\{(\w+)\}/g, replacer(replacements));
}
};
var groupChartData = [{
"num": 1,
"over": "Singapore"
}, {
"num": 1.3,
"over": "The Netherlands"
}, {
"num": 2,
"over": "United Kingdom"
}, {
"num": 2.4,
"over": "United States"
}, {
"num": 2.6,
"over": "New Zealand"
}, {
"num": 2.8,
"over": "Sweden"
}, {
"num": 3,
"over": "Canada"
}, {
"num": 3,
"over": "UAE"
}, {
"num": 4,
"over": "Australia"
}, {
"num": 4.4,
"over": "France"
}, {
"num": 5,
"over": "South Korea"
}, {
"num": 5.2,
"over": "Germany"
}, {
"num": 5.5,
"over": "Austria"
}, {
"num": 6,
"over": "Austria"
}, {
"num": 7,
"over": "Brazil"
}, {
"num": 7,
"over": "China"
}, {
"num": 8,
"over": "Japan"
}, {
"num": 10,
"over": "Russia"
}, {
"num": 11,
"over": "Mexico"
}, {
"num": 12,
"over": "India"
}, ];
var columnsInfo = {
"num": "<span class='mainTitle KPMGWeb-ExtraLight'>Technology & innovation pillar: score by country</span>"
};
$("#chart").empty();
var barChartConfig = {
mainDiv: "#chart",
colorRange: ["#0091DA", "#6D2077"],
data: groupChartData,
columnsInfo: columnsInfo,
xAxis: "runs",
yAxis: "over",
label: {
xAxis: "",
yAxis: ""
},
requireLegend: true
};
var groupChart = new horizontalGroupBarChart(barChartConfig);
.mainTitle {
font-size: 3em;
}
svg text {
font-size: 10px;
font-family: sans-serif;
}
<script src="https://code.jquery.com/jquery-latest.min.js"></script>
<script src="https://d3js.org/d3.v4.min.js"></script>
<div id="chart" style="width: 800;height: 600">
By the way, I noticed that there seem to be two bars on the same line for Austria - not sure if that is deliberate or not.

Legend that updates at the click of the mouse

I created this sample heatmap: Plunker
Initially I use a certain linear scale colorScale1 to color the heatmap.
When the user clicks on the legend, the color scale is updated and the threshold scale (colorScale2) is used.
This switch works well.
Now I don't know how to change the legend for the colorScale2.
The ticks and the gradient for colorScale2 are wrong. I looked for a linearGradient equivalent for scaleThreshold but I didn't find anything.
This is the code:
var itemSize = 20;
var cellBorderSize = 1;
var cellSize = itemSize - 1 + cellBorderSize;
var margin = {top: 10, right: 10, bottom: 10, left: 10};
var width = 80 - margin.right - margin.left;
var height = 80 - margin.top - margin.bottom;
var svg = d3.select('#heatmap')
.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 domain1 = [0, 80, 90, 95, 100];
var range1 = ['#EC93AB', '#CEB1DE', '#95D3F0', '#77EDD9', '#A9FCAA'];
var colorScale1 = d3.scaleLinear()
.domain(domain1)
.range(range1);
var domain2 = [0, 95, 100];
var range2 = ['white', 'lightgrey', 'grey'];
var colorScale2 = d3.scaleThreshold()
.domain(domain2)
.range(range2);
svg.append('defs')
.append('pattern')
.attr('id', 'pattern-stripes')
.attr('patternUnits', 'userSpaceOnUse')
.attr('patternTransform', 'rotate(45)')
.attr('width', 3)
.attr('height', 3)
.append('rect')
.attr('width', 1)
.attr('height', 3)
.attr('transform', 'translate(0, 0)')
.attr('fill', 'black');
///////////////////////////////////////////////////////////
// Load data files.
///////////////////////////////////////////////////////////
var files = ['./data.csv'];
var promises = [];
promises.push(d3.csv(files[0]));
Promise.all(promises)
.then(makeHeatmap)
.catch(function(err) {
console.log('Error loading files');
throw err;
});
///////////////////////////////////////////////////////////
// Data heatmap
///////////////////////////////////////////////////////////
function makeHeatmap(myData) {
var data = myData[0];
// get each element of data file and creates an object
var data = data.map(function(item) {
var newItem = {};
newItem.name = item.NAME;
newItem.year = item.YEAR;
newItem.val = item.VAL;
return newItem;
});
var names = data.map(function(d) {
return d.name;
});
regionsName = d3.set(names).values();
numRegions = regionsName.length;
var years = data.map(function(d) {
return d.year;
});
yearsName = d3.set(years).values();
numYears = yearsName.length;
///////////////////////////////////////////////////////////
// Draw heatmap
///////////////////////////////////////////////////////////
var cells = svg.selectAll('.cell')
.data(data)
.enter()
.append('g')
.append('rect')
.attr('data-value', function(d) {
return d.val;
})
.attr('data-r', function(d) {
var idr = regionsName.indexOf(d.name);
return idr;
})
.attr('data-c', function(d, i) {
if(regionsName.includes(d.name) & d.year == '1990') var idc = 0;
else if(regionsName.includes(d.name) && d.year == '1991') var idc = 1;
else if(regionsName.includes(d.name) && d.year == '1992') var idc = 2;
return idc;
})
.attr('class', function() {
var idr = d3.select(this).attr('data-r'); // row
var idc = d3.select(this).attr('data-c'); // column
return 'cell cr' + idr + ' cc' + idc;
})
.attr('width', cellSize)
.attr('height', cellSize)
.attr('x', function(d) {
var c = d3.select(this).attr('data-c');
return c * cellSize;
})
.attr('y', function() {
var r = d3.select(this).attr('data-r');
return r * cellSize;
})
.attr('fill', function(d) {
var col;
if(d.name == '') {
col = 'url(#pattern-stripes)';
}
else {
col = colorScale1(d.val);
}
return col;
});
} // end makeHeatmap
///////////////////////////////////////////////////////////
// Legend
///////////////////////////////////////////////////////////
// create tick marks
var xLegend = d3.scaleLinear()
.domain([0, 100])
.range([10, 409]); // larghezza dei tick
var axisLegend = d3.axisBottom(xLegend)
.tickSize(19) // height of ticks
.tickFormat(function(v, i) { // i is index of domain colorScale, v is the corrisponding value (v = domain[i])
if(v == 0) {
return v + '%';
}
else {
return v;
}
})
.tickValues(colorScale1.domain());
var svgLegend = d3.select('#legend').append('svg').attr('width', 600);
// append title
svgLegend.append('text')
.attr('class', 'legendTitle')
.attr('x', 10)
.attr('y', 20)
.style('text-anchor', 'start')
.text('Legend title');
// draw the rectangle and fill with gradient
svgLegend.append('rect')
.attr('class', 'legendRect')
.attr('x', 10) // position
.attr('y', 30)
.attr('width', 400) // larghezza fascia colorata
.attr('height', 15) // altezza fascia colorata
.style('fill', 'url(#linear-gradient1)')
.on('click', function() {
if(currentFill === '1') {
updateColor2();
currentFill = '2';
}
else {
updateColor1();
currentFill = '1';
}
});
svgLegend
.attr('class', 'legendLinAxis')
.append('g')
.attr('class', 'legendLinG')
.attr('transform', 'translate(0, 30)') // 47 è la posizione verticale dei tick (se l'aumenti, scendono) (47 per farli partire sotto, 30 per farli partire da sopra)
.call(axisLegend);
var defs = svgLegend.append('defs');
// horizontal gradient and append multiple color stops by using D3's data/enter step
var linearGradient1 = defs.append('linearGradient')
.attr('id', 'linear-gradient1')
.attr('x1', '0%').attr('y1', '0%')
.attr('x2', '100%').attr('y2', '0%')
.selectAll('stop')
.data(colorScale1.domain())
.enter().append('stop')
.attr('offset', function(d) {
return d + '%';
})
.attr('stop-color', function(d) {
return colorScale1(d);
});
// horizontal gradient and append multiple color stops by using D3's data/enter step
var linearGradient2 = defs.append('linearGradient')
.attr('id', 'linear-gradient2')
.attr('x1', '0%').attr('y1', '0%')
.attr('x2', '100%').attr('y2', '0%')
.selectAll('stop')
.data(colorScale2.domain())
.enter().append('stop')
.attr('offset', function(d) {
return d + '%';
})
.attr('stop-color', function(d) {
return colorScale2(d);
});
// update the colors to a different color scale (colorScale1)
function updateColor1() {
// fill the legend rectangle
svgLegend.select('.legendRect')
.style('fill', 'url(#linear-gradient1)');
// transition the cell colors
svg.selectAll('.cell')
.transition().duration(1000)
.style('fill', function(d, i) {
var col;
if(d.valuePol == '') {
col = 'url(#pattern-stripes)';
}
else {
col = colorScale1(d.val);
}
return col;
});
}
// update the colors to a different color scale (colorScale2)
function updateColor2() {
// fill the legend rectangle
svgLegend.select('.legendRect')
.style('fill', 'url(#linear-gradient2)');
// transition the cell colors
svg.selectAll('.cell')
.transition().duration(1000)
.style('fill', function(d, i) {
var col;
if(d.valuePol == '') {
col = 'url(#pattern-stripes)';
}
else {
col = colorScale2(d.val);
}
return col;
});
}
// start set-up
updateColor1();
var currentFill = '1';
Here is a slightly modified version of your code for which the legend's ticks and color scale are updated when clicking on the legend:
var itemSize = 20;
var cellBorderSize = 1;
var cellSize = itemSize - 1 + cellBorderSize;
var margin = {top: 10, right: 10, bottom: 10, left: 10};
var width = 80 - margin.right - margin.left;
var height = 80 - margin.top - margin.bottom;
var svg = d3.select('#heatmap')
.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 domain1 = [0, 80, 90, 95, 100];
var range1 = ['#EC93AB', '#CEB1DE', '#95D3F0', '#77EDD9', '#A9FCAA'];
var colorScale1 = d3.scaleLinear()
.domain(domain1)
.range(range1);
var domain2 = [0, 95, 100];
var range2 = ['white', 'lightgrey', 'grey'];
var colorScale2 = d3.scaleThreshold()
.domain(domain2)
.range(range2);
svg.append('defs')
.append('pattern')
.attr('id', 'pattern-stripes')
.attr('patternUnits', 'userSpaceOnUse')
.attr('patternTransform', 'rotate(45)')
.attr('width', 3)
.attr('height', 3)
.append('rect')
.attr('width', 1)
.attr('height', 3)
.attr('transform', 'translate(0, 0)')
.attr('fill', 'black');
var data = [
{ "NAME": "ronnie", "YEAR": 1990, "VAL": 90 },
{ "NAME": "ronnie", "YEAR": 1991, "VAL": 95 },
{ "NAME": "ronnie", "YEAR": 1992, "VAL": 98 },
{ "NAME": "bob", "YEAR": 1990, "VAL": 92 },
{ "NAME": "bob", "YEAR": 1991, "VAL": 90 },
{ "NAME": "bob", "YEAR": 1992, "VAL": 99 },
{ "NAME": "carl", "YEAR": 1990, "VAL": 98 },
{ "NAME": "carl", "YEAR": 1991, "VAL": 99 },
{ "NAME": "carl", "YEAR": 1992, "VAL": 995 }
];
makeHeatmap(data);
///////////////////////////////////////////////////////////
// Data heatmap
///////////////////////////////////////////////////////////
function makeHeatmap(data) {
//var data = myData[0];
// get each element of data file and creates an object
var data = data.map(function(item) {
var newItem = {};
newItem.name = item.NAME;
newItem.year = item.YEAR;
newItem.val = item.VAL;
return newItem;
});
var names = data.map(function(d) {
return d.name;
});
regionsName = d3.set(names).values();
numRegions = regionsName.length;
var years = data.map(function(d) {
return d.year;
});
yearsName = d3.set(years).values();
numYears = yearsName.length;
///////////////////////////////////////////////////////////
// Draw heatmap
///////////////////////////////////////////////////////////
var cells = svg.selectAll('.cell')
.data(data)
.enter()
.append('g')
.append('rect')
.attr('data-value', function(d) {
return d.val;
})
.attr('data-r', function(d) {
var idr = regionsName.indexOf(d.name);
return idr;
})
.attr('data-c', function(d, i) {
if(regionsName.includes(d.name) & d.year == '1990') var idc = 0;
else if(regionsName.includes(d.name) && d.year == '1991') var idc = 1;
else if(regionsName.includes(d.name) && d.year == '1992') var idc = 2;
return idc;
})
.attr('class', function() {
var idr = d3.select(this).attr('data-r'); // row
var idc = d3.select(this).attr('data-c'); // column
return 'cell cr' + idr + ' cc' + idc;
})
.attr('width', cellSize)
.attr('height', cellSize)
.attr('x', function(d) {
var c = d3.select(this).attr('data-c');
return c * cellSize;
})
.attr('y', function() {
var r = d3.select(this).attr('data-r');
return r * cellSize;
})
.attr('fill', function(d) {
var col;
if(d.name == '') {
col = 'url(#pattern-stripes)';
}
else {
col = colorScale1(d.val);
}
return col;
});
} // end makeHeatmap
///////////////////////////////////////////////////////////
// Legend
///////////////////////////////////////////////////////////
// create tick marks
var xLegend = d3.scaleLinear()
.domain([0, 100])
.range([10, 409]); // larghezza dei tick
var axisLegend = d3.axisBottom(xLegend)
.tickSize(19) // height of ticks
.tickFormat(function(v, i) { // i is index of domain colorScale, v is the corrisponding value (v = domain[i])
if(v == 0) {
return v + '%';
}
else {
return v;
}
});
var svgLegend = d3.select('#legend').append('svg').attr('width', 600);
// append title
svgLegend.append('text')
.attr('class', 'legendTitle')
.attr('x', 10)
.attr('y', 20)
.style('text-anchor', 'start')
.text('Legend title');
// draw the rectangle and fill with gradient
svgLegend.append('rect')
.attr('class', 'legendRect')
.attr('x', 10) // position
.attr('y', 30)
.attr('width', 400) // larghezza fascia colorata
.attr('height', 15) // altezza fascia colorata
.style('fill', 'url(#linear-gradient1)')
.on('click', function() {
if(currentFill === '1') {
updateColor2();
currentFill = '2';
}
else {
updateColor1();
currentFill = '1';
}
});
var legend = svgLegend
.attr('class', 'legendLinAxis')
.append('g')
.attr('class', 'legendLinG')
.attr('transform', 'translate(0, 30)'); // 47 è la posizione verticale dei tick (se l'aumenti, scendono) (47 per farli partire sotto, 30 per farli partire da sopra)
var defs = svgLegend.append('defs');
// horizontal gradient and append multiple color stops by using D3's data/enter step
var linearGradient1 = defs.append('linearGradient')
.attr('id', 'linear-gradient1')
.attr('x1', '0%').attr('y1', '0%')
.attr('x2', '100%').attr('y2', '0%')
.selectAll('stop')
.data(colorScale1.domain())
.enter().append('stop')
.attr('offset', function(d) {
return d + '%';
})
.attr('stop-color', function(d) {
return colorScale1(d);
});
// horizontal gradient and append multiple color stops by using D3's data/enter step
function getGradient2data() {
// Duplicates elements of domain2:
var duplicatedDomain = domain2.reduce(function (res, current, index, array) { return res.concat([current, current]); }, []).slice(1, -1);
// Duplicates elements of range2:
var duplicatedRange = range2.slice(1).reduce(function (res, current, index, array) { return res.concat([current, current]); }, []);
// Zips both domain and range:
return duplicatedDomain.map( function(e, i) { return { "offset": e + "%", "color": duplicatedRange[i] }; [e, duplicatedRange[i]]; });
}
var linearGradient2 = defs.append('linearGradient')
.attr('id', 'linear-gradient2')
.attr('x1', '0%').attr('y1', '0%')
.attr('x2', '100%').attr('y2', '0%')
.selectAll('stop')
.data(getGradient2data())
//.data([
// { offset: "0%", color: "lightgrey" },
// { offset: "95%", color: "lightgrey" },
// { offset: "95%", color: "grey" },
// { offset: "100%", color: "grey" }
//])
.enter().append('stop')
.attr('offset', function(d) {
return d.offset;
})
.attr('stop-color', function(d) {
return d.color;
});
// update the colors to a different color scale (colorScale1)
function updateColor1() {
// fill the legend rectangle
svgLegend.select('.legendRect')
.style('fill', 'url(#linear-gradient1)');
// transition the cell colors
svg.selectAll('.cell')
.transition().duration(1000)
.style('fill', function(d, i) {
var col;
if(d.valuePol == '') {
col = 'url(#pattern-stripes)';
}
else {
col = colorScale1(d.val);
}
return col;
});
axisLegend.tickValues(colorScale1.domain());
legend.call(axisLegend);
}
// update the colors to a different color scale (colorScale2)
function updateColor2() {
// fill the legend rectangle
svgLegend.select('.legendRect')
.style('fill', 'url(#linear-gradient2)');
// transition the cell colors
svg.selectAll('.cell')
.transition().duration(1000)
.style('fill', function(d, i) {
var col;
if(d.valuePol == '') {
col = 'url(#pattern-stripes)';
}
else {
col = colorScale2(d.val);
}
return col;
});
axisLegend.tickValues(colorScale2.domain());
legend.call(axisLegend);
}
// start set-up
updateColor1();
var currentFill = '1';
#heatmap {
float: left;
background-color: whitesmoke;
}
.cell {
stroke: #E6E6E6;
stroke-width: 1px;
}
/**
* Legend linear.
*/
.legendTitle {
font-size: 15px;
fill: black;
font-weight: 12;
font-family: Consolas, courier;
}
#legendLin {
background-color: yellow;
}
.legendLinAxis path, .legendLinAxis line {
fill: none;
stroke: none;
shape-rendering: crispEdges;
}
.legendLinAxis text {
font-family: Consolas, courier;
font-size: 8pt;
fill: black;
}
.legendLinG .tick line {
stroke: black;
stroke-width: 1px;
}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<script src="https://d3js.org/d3.v5.min.js" charset="utf-8"></script>
<link rel="stylesheet" type="text/css" href="./style.css" media="screen"/>
</head>
<body>
<div id='heatmap'></div>
<div id='legend'></div>
<script src="./script.js"></script>
</body>
</html>
Ticks update:
Within the update functions (updateColor1, updateColor2), in addition to the update of the color scale gradient, we can also include the update of legend ticks (similar to how it was first initialized):
axisLegend.tickValues(colorScale1.domain());
legend.call(axisLegend);
Gradient update:
The creation of "abrupt gradients" is slightly different from the one of "linear gradients". Here is a slightly modified version of your linear-gradient2 threshold gradient:
var linearGradient2 = defs.append('linearGradient')
.attr('id', 'linear-gradient2')
.attr('x1', '0%').attr('y1', '0%')
.attr('x2', '100%').attr('y2', '0%')
.selectAll('stop')
.data([
{ offset: "0%", color: "lightgrey" },
{ offset: "95%", color: "lightgrey" },
{ offset: "95%", color: "grey" },
{ offset: "100%", color: "grey" }
])
.enter().append('stop')
.attr('offset', function(d) { return d.offset; })
.attr('stop-color', function(d) { return d.color; });
Or if the threshold gradient is to change, instead of hardcoding it, we can also get it from the defined domain and range:
function getGradient2data() {
// Duplicates elements of domain2:
var duplicatedDomain = domain2.reduce(function (res, current, index, array) { return res.concat([current, current]); }, []).slice(1, -1);
// Duplicates elements of range2:
var duplicatedRange = range2.slice(1).reduce(function (res, current, index, array) { return res.concat([current, current]); }, []);
// Zips both domain and range:
return duplicatedDomain.map( function(e, i) { return { "offset": e + "%", "color": duplicatedRange[i] }; [e, duplicatedRange[i]]; });
}
which produces:
[
{ offset: "0%", color: "lightgrey" },
{ offset: "95%", color: "lightgrey" },
{ offset: "95%", color: "grey" },
{ offset: "100%", color: "grey" }
]

Unexpected d3 v4 tree behaviour

The following d3.js (v4) interactive tree layout I've put together as a proof of concept for a user interface project is not behaving as expected. This is my first d3.js visualisation and I'm still getting my head around all the concepts.
Essentially, clicking any yellow node should generate two yellow child nodes (& links). This works fine when following a left to right, top to bottom click sequence, otherwise it displays unexpected behaviour.
It's probably easiest to run you through an example, so here's a snippet:
var data = {
source: {
type: 'dataSource',
name: 'Data Source',
silos: [
{ name: 'Silo 1', selected: true },
{ name: 'Silo 2', selected: false },
{ name: 'Silo 3', selected: false }
],
union: {
type: 'union',
name: 'Union',
count: null,
cardinalities: [
{ type: 'cardinality', positive: false, name: 'Falsey', count: 40, cardinalities: [] },
{ type: 'cardinality', positive: true, name: 'Truthy', count: 60, cardinalities: [] }
]
}
}
}
// global variables
var containerPadding = 20;
var container = d3.select('#container').style('padding', containerPadding + 'px'); // contains the structured search svg
var svg = container.select('svg'); // the canvas that displays the structured search
var group = svg.append('g'); // contains the tree elements (nodes & links)
var nodeWidth = 40, nodeHeight = 30, nodeCornerRadius = 3, verticalNodeSeparation = 150, transitionDuration = 600;
var tree = d3.tree().nodeSize([nodeWidth, nodeHeight]);
var source;
function nodeClicked(d) {
source = d;
switch (d.data.type) {
case 'dataSource':
// todo: show the data source popup and update the selected values
d.data.silos[0].selected = !d.data.silos[0].selected;
break;
default:
// todo: show the operation popup and update the selected values
if (d.data.cardinalities && d.data.cardinalities.length) {
d.data.cardinalities.splice(-2, 2);
}
else {
d.data.cardinalities.push({ type: 'cardinality', positive: false, name: 'F ' + (new Date()).getSeconds(), count: 40, cardinalities: [] });
d.data.cardinalities.push({ type: 'cardinality', positive: true, name: 'T ' + (new Date()).getSeconds(), count: 60, cardinalities: [] });
}
break;
}
render();
}
function renderLink(source, destination) {
var x = destination.x + nodeWidth / 2;
var y = destination.y;
var px = source.x + nodeWidth / 2;
var py = source.y + nodeHeight;
return 'M' + x + ',' + y
+ 'C' + x + ',' + (y + py) / 2
+ ' ' + x + ',' + (y + py) / 2
+ ' ' + px + ',' + py;
}
function render() {
// map the data source to a heirarchy that d3.tree requires
// d3.tree instance needs the data structured in a specific way to generate the required layout of nodes & links (lines)
var hierarchy = d3.hierarchy(data.source, function (d) {
switch (d.type) {
case 'dataSource':
return d.silos.some(function (e) { return e.selected; }) ? [d.union] : undefined;
default:
return d.cardinalities;
}
});
// set the layout parameters (all required for resizing)
var containerBoundingRect = container.node().getBoundingClientRect();
var width = containerBoundingRect.width - containerPadding * 2;
var height = verticalNodeSeparation * hierarchy.height;
svg.transition().duration(transitionDuration).attr('width', width).attr('height', height + nodeHeight);
tree.size([width - nodeWidth, height]);
// tree() assigns the (x, y) coords, depth, etc, to the nodes in the hierarchy
tree(hierarchy);
// get the descendants
var descendants = hierarchy.descendants();
// store previous position for transitioning
descendants.forEach(function (d) {
d.x0 = d.x;
d.y0 = d.y;
});
// ensure source is set when rendering for the first time (hierarch is the root, same as descendants[0])
source = source || hierarchy;
// render nodes
var nodesUpdate = group.selectAll('.node').data(descendants);
var nodesEnter = nodesUpdate.enter()
.append('g')
.attr('class', 'node')
.attr('transform', 'translate(' + source.x0 + ',' + source.y0 + ')')
.style('opacity', 0)
.on('click', nodeClicked);
nodesEnter.append('rect')
.attr('rx', nodeCornerRadius)
.attr('width', nodeWidth)
.attr('height', nodeHeight)
.attr('class', function (d) { return 'box ' + d.data.type; });
nodesEnter.append('text')
.attr('dx', nodeWidth / 2 + 5)
.attr('dy', function (d) { return d.parent ? -5 : nodeHeight + 15; })
.text(function (d) { return d.data.name; });
nodesUpdate
.merge(nodesEnter)
.transition().duration(transitionDuration)
.attr('transform', function (d) { return 'translate(' + d.x + ',' + d.y + ')'; })
.style('opacity', 1);
nodesUpdate.exit().transition().duration(transitionDuration)
.attr('transform', function (d) { return 'translate(' + source.x + ',' + source.y + ')'; })
.style('opacity', 0)
.remove();
// render links
var linksUpdate = group.selectAll('.link').data(descendants.slice(1));
var linksEnter = linksUpdate.enter()
.append('path')
.attr('class', 'link')
.classed('falsey', function (d) { return d.data.positive === false })
.classed('truthy', function (d) { return d.data.positive === true })
.attr('d', function (d) { var o = { x: source.x0, y: source.y0 }; return renderLink(o, o); })
.style('opacity', 0);
linksUpdate
.merge(linksEnter)
.transition().duration(transitionDuration)
.attr('d', function (d) { return renderLink({ x: d.parent.x, y: d.parent.y }, d); })
.style('opacity', 1);
linksUpdate.exit()
.transition().duration(transitionDuration)
.attr('d', function (d) { var o = { x: source.x, y: source.y }; return renderLink(o, o); })
.style('opacity', 0)
.remove();
}
window.addEventListener('resize', render); // todo: use requestAnimationFrame (RAF) for this
render();
.link {
fill:none;
stroke:#555;
stroke-opacity:0.4;
stroke-width:1.5px
}
.truthy {
stroke:green
}
.falsey {
stroke:red
}
.box {
stroke:black;
stroke-width:1;
cursor:pointer
}
.dataSource {
fill:blue
}
.union {
fill:orange
}
.cardinality {
fill:yellow
}
<script src="https://d3js.org/d3.v4.min.js"></script>
<div id="container" style="background-color:gray">
<svg style="background-color:#fff" width="0" height="0"></svg>
</div>
If you click on the Falsey node then the Truthy node, you'll see two child nodes appear beneath each, as expected. However, if you click on the Truthy node first, when you then click the Falsey node, you'll see that the Truthy child nodes move under Falsey, and the Falsey child nodes move under Truthy. Plus, the child nodes beneath Falsey and Truthy are actually the same two nodes, even though the underlying data is different.
I've confirmed that the data object is correctly structured after creating the children. From what I can see, the d3.hierarchy() and d3.tree() methods are working correctly, so I'm assuming that there's an issue with the way I'm constructing the selections.
Hopefully someone can spot the problem.
A second issue that may be related to the first is: Clicking Falsey or Truthy a second time should cause the child nodes (& links) to transition back to the parent node, but it does not track the parent's position. Hopefully someone can spot the issue here too.
Thanks!
It seems to me that you need a key function when you join your data:
If a key function is not specified, then the first datum in data is assigned to the first selected element, the second datum to the second selected element, and so on. A key function may be specified to control which datum is assigned to which element, replacing the default join-by-index.
So, this should be your data binding selection:
var nodesUpdate = group.selectAll('.node')
.data(descendants, function(d){ return d.data.name});
Check the snippet:
var data = {
source: {
type: 'dataSource',
name: 'Data Source',
silos: [
{ name: 'Silo 1', selected: true },
{ name: 'Silo 2', selected: false },
{ name: 'Silo 3', selected: false }
],
union: {
type: 'union',
name: 'Union',
count: null,
cardinalities: [
{ type: 'cardinality', positive: false, name: 'Falsey', count: 40, cardinalities: [] },
{ type: 'cardinality', positive: true, name: 'Truthy', count: 60, cardinalities: [] }
]
}
}
}
// global variables
var containerPadding = 20;
var container = d3.select('#container').style('padding', containerPadding + 'px'); // contains the structured search svg
var svg = container.select('svg'); // the canvas that displays the structured search
var group = svg.append('g'); // contains the tree elements (nodes & links)
var nodeWidth = 40, nodeHeight = 30, nodeCornerRadius = 3, verticalNodeSeparation = 150, transitionDuration = 600;
var tree = d3.tree().nodeSize([nodeWidth, nodeHeight]);
var source;
function nodeClicked(d) {
source = d;
switch (d.data.type) {
case 'dataSource':
// todo: show the data source popup and update the selected values
d.data.silos[0].selected = !d.data.silos[0].selected;
break;
default:
// todo: show the operation popup and update the selected values
if (d.data.cardinalities && d.data.cardinalities.length) {
d.data.cardinalities.splice(-2, 2);
}
else {
d.data.cardinalities.push({ type: 'cardinality', positive: false, name: 'F ' + (new Date()).getSeconds(), count: 40, cardinalities: [] });
d.data.cardinalities.push({ type: 'cardinality', positive: true, name: 'T ' + (new Date()).getSeconds(), count: 60, cardinalities: [] });
}
break;
}
render();
}
function renderLink(source, destination) {
var x = destination.x + nodeWidth / 2;
var y = destination.y;
var px = source.x + nodeWidth / 2;
var py = source.y + nodeHeight;
return 'M' + x + ',' + y
+ 'C' + x + ',' + (y + py) / 2
+ ' ' + x + ',' + (y + py) / 2
+ ' ' + px + ',' + py;
}
function render() {
// map the data source to a heirarchy that d3.tree requires
// d3.tree instance needs the data structured in a specific way to generate the required layout of nodes & links (lines)
var hierarchy = d3.hierarchy(data.source, function (d) {
switch (d.type) {
case 'dataSource':
return d.silos.some(function (e) { return e.selected; }) ? [d.union] : undefined;
default:
return d.cardinalities;
}
});
// set the layout parameters (all required for resizing)
var containerBoundingRect = container.node().getBoundingClientRect();
var width = containerBoundingRect.width - containerPadding * 2;
var height = verticalNodeSeparation * hierarchy.height;
svg.transition().duration(transitionDuration).attr('width', width).attr('height', height + nodeHeight);
tree.size([width - nodeWidth, height]);
// tree() assigns the (x, y) coords, depth, etc, to the nodes in the hierarchy
tree(hierarchy);
// get the descendants
var descendants = hierarchy.descendants();
// store previous position for transitioning
descendants.forEach(function (d) {
d.x0 = d.x;
d.y0 = d.y;
});
// ensure source is set when rendering for the first time (hierarch is the root, same as descendants[0])
source = source || hierarchy;
// render nodes
var nodesUpdate = group.selectAll('.node').data(descendants, function(d){ return d.data.name});
var nodesEnter = nodesUpdate.enter()
.append('g')
.attr('class', 'node')
.attr('transform', 'translate(' + source.x0 + ',' + source.y0 + ')')
.style('opacity', 0)
.on('click', nodeClicked);
nodesEnter.append('rect')
.attr('rx', nodeCornerRadius)
.attr('width', nodeWidth)
.attr('height', nodeHeight)
.attr('class', function (d) { return 'box ' + d.data.type; });
nodesEnter.append('text')
.attr('dx', nodeWidth / 2 + 5)
.attr('dy', function (d) { return d.parent ? -5 : nodeHeight + 15; })
.text(function (d) { return d.data.name; });
nodesUpdate
.merge(nodesEnter)
.transition().duration(transitionDuration)
.attr('transform', function (d) { return 'translate(' + d.x + ',' + d.y + ')'; })
.style('opacity', 1);
nodesUpdate.exit().transition().duration(transitionDuration)
.attr('transform', function (d) { return 'translate(' + source.x + ',' + source.y + ')'; })
.style('opacity', 0)
.remove();
// render links
var linksUpdate = group.selectAll('.link').data(descendants.slice(1));
var linksEnter = linksUpdate.enter()
.append('path')
.attr('class', 'link')
.classed('falsey', function (d) { return d.data.positive === false })
.classed('truthy', function (d) { return d.data.positive === true })
.attr('d', function (d) { var o = { x: source.x0, y: source.y0 }; return renderLink(o, o); })
.style('opacity', 0);
linksUpdate
.merge(linksEnter)
.transition().duration(transitionDuration)
.attr('d', function (d) { return renderLink({ x: d.parent.x, y: d.parent.y }, d); })
.style('opacity', 1);
linksUpdate.exit()
.transition().duration(transitionDuration)
.attr('d', function (d) { var o = { x: source.x, y: source.y }; return renderLink(o, o); })
.style('opacity', 0)
.remove();
}
window.addEventListener('resize', render); // todo: use requestAnimationFrame (RAF) for this
render();
.link {
fill:none;
stroke:#555;
stroke-opacity:0.4;
stroke-width:1.5px
}
.truthy {
stroke:green
}
.falsey {
stroke:red
}
.box {
stroke:black;
stroke-width:1;
cursor:pointer
}
.dataSource {
fill:blue
}
.union {
fill:orange
}
.cardinality {
fill:yellow
}
<script src="https://d3js.org/d3.v4.min.js"></script>
<div id="container" style="background-color:gray">
<svg style="background-color:#fff" width="0" height="0"></svg>
</div>

d3 Uncaught TypeError: zoomed.x is not a function

I am trying to implement tooltips and a zoom on a graph, the tooltips work fine however I am having issues with the zoom function. I get an error on the debugger that says: Uncaught TypeError: zoomed.x is not a function. This occurs at the line 'return zoomed.x(x);' I am not sure how to change the code to get it to work.
//Builder for REal time flow chart
define((function () {
var categories = [
{
Name: "MediaServiceIndexes",
Title: "Media",
Style: "background-color:#ffffff;font-color:blue;",
Text: "Here are the top media companies with whom our customers shop.",
TitleStyle: "margin-left:0px"
},
];
var url = App.SiteUrl + "/Data/RequestDataFrom";
var svg, width, numberChecker;
var patterns = d3.scale.ordinal()
.range([
"url(#blue1)", "url(#yellow1)",
"url(#blue2)", "url(#yellow2)",
"url(#blue3)", "url(#yellow3)",
"url(#blue4)", "url(#yellow4)",
"url(#blue5)", "url(#yellow5)"
]);
var grayFill = "url(#gray)";
var colors = d3.scale.category10();
var getValues = function (values) {
var result = [];
try {
values.forEach(function (item) {
var o = {
Month: moment(item.month + "01", "YYYYMMDD"),
Value: parseFloat(item.change_in_market_share, 10)
};
var vendor = item.vendor.replace(/'/g, "");
//var e = result.find(function (it) { return it.Vendor == vendor; });
var e = false;
for (var x in result) {
if (result.hasOwnProperty(x) && typeof result[x] != "function") {
if (result[x].Vendor == vendor) {
e = (result[x]);
}
}
}
if (e) {
e.Values.push(o);
} else {
result.push({
Visible: true,
Vendor: vendor,
Values: [o]
});
}
});
result.forEach(function (item, i) {
item.Index = i;
item.Values.sort(function (a, b) {
return a.Month - b.Month;
});
});
} catch (e) {
result = null;
};
return result;
};
var createChart = function (chartElement, cd) {
var chartData = cd; //chart data is being passed in using cd via createChart function
//chart gives the location of the chart,
var chart = function (el, data) {
var margin = {
top: 20,
right: 180,
bottom: 50,
left: 110
};
var elem = el;
var chartEl = chartElement;
var fillData = [];
var defs,
gs,
height,
line,
maxDays,
minDays,
minValue,
maxValue,
x,
xAxis,
y,
yAxis;
var initialiseData = function (dataValues) {
minDays = d3.min(dataValues,
function (m) {
return d3.min(m.Values,
function (d) {
return d.Month;
});
});
maxDays = d3.max(dataValues,
function (m) {
return d3.max(m.Values,
function (d) {
return d.Month;
});
});
minValue = d3.min(chartData,
function (m) {
return d3.min(m.Values,
function (d) {
return d.Value;
});
});
maxValue = d3.max(chartData,
function (m) {
return d3.max(m.Values,
function (d) {
return d.Value;
});
});
console.log('min days: ' + minDays);
console.log('max days: ' + maxDays);
console.log('min value: ' + minValue);
console.log('max value: ' + maxValue);
dataValues.forEach(function (item) {
var nu = $.extend(true, {}, item);
nu.Values.push({ Month: maxDays, Value: minValue });
nu.Values.push({ Month: minDays, Value: minValue });
fillData.push(nu);
});
};
//initialise scales
var configSize = function () {
if (isNaN(numberChecker) === true) {
numberChecker = $(chartEl).width();
width = numberChecker;
}
width = 750;
height = 500 - margin.top - margin.bottom;
x = d3.scale.linear()
.range([0, width])
.domain([minDays, maxDays]);
y = d3.scale.linear()
.range([height, 0])
.domain([minValue, maxValue]);
$('.adjustmentZoom .tick text').attr('y', '25');
//initialise axis
xAxis = d3.svg.axis()
.scale(x)
.tickFormat(function (d, i) {
if (Math.floor(d) !== d) {
} else {
return moment(d).format("MMM YY");
}
})
.orient('bottom');
yAxis = d3.svg.axis()
.scale(y)
.tickFormat(function (d) {
return d3.round(d, 3) + "%"
})
.orient('left');
$('#clipper rect').attr('width', width);
$('.zoom-panel').attr('width', width);
};
var svgTransform = function(d) {
return "translate(" + x(d.Month) + "," + y(d.Value) + ")";
};
var zoomed = function () {
svg.select(".x.axis").call(xAxis);
svg.select(".y.axis").call(yAxis);
svg.selectAll(".datapoint").attr("transform", svgTransform);
};
//the path generator for the line chart
var initialise = function () {
line = d3.svg.line()
.interpolate(
'cardinal')
.x(function (d) {
return x(d.Month);
})
.y(function (d) {
return y(d.Value);
});
var zoomBeh = d3.behavior.zoom()
.x(x)
.y(y)
.scaleExtent([
1,
500
])
.on('zoom', zoomed);
//elem is window
svg = d3.select(elem).append('div').attr('id', 'scatter').append('svg')
.attr('width', '100%')
.attr('height', '100%')
.attr('viewBox', '0 0 1000 550')
.attr('preserveAspectRatio', 'xMinYMin meet')
.append('g')
.attr("class", "line-container")
.attr('transform',
function () {
if (window.innerWidth > 650) {
var marginWidth = margin.left;
}
else if (window.innerWidth <= 650 && window.innerWidth > 549) {
var marginWidth = margin.left + 30;
}
else if (window.innerWidth <= 549) {
var marginWidth = margin.left + 50;
}
return 'translate(' + marginWidth + ',' + margin.top + ')'
}).call(zoomBeh);
svg.append('rect')
.attr('class', 'zoom-panel')
.attr('width', width)
.attr('height', height)
.call(zoomBeh);
svg.append('g')
.attr('class', 'x axis')
.attr('transform', 'translate(0,' + height + ')')
.call(xAxis)
.selectAll('text');
svg.append('text')
.attr('x', 400)
.attr('y', (height + 70))
.style('text-anchor', 'middle')
.attr('class', 'xTitle')
.style('font-weight', '500')
.text('Date by month');
svg.append('g').attr('class', 'y axis').attr('transform', 'translate(0,0)').style('text-anchor', 'end').call(yAxis);
svg.append('text')
.attr('transform', 'rotate(-90)')
.attr('y', -100)
.attr('x', -200)
.attr('dy', '1em')
.attr('class', 'yTitle')
.style('text-anchor', 'middle')
.style('font-weight', '500')
.text('Share prices in percentage');
//zoom.scaleExtent([
// 1,
// moment(maxDays).diff(minDays, "months")
//]);
}
var tooltip = function () {
svg = d3.select(elem)
.append('div')
.attr('class', 'tooltip')
.style('opacity', 0);
};
//draw calls drawData which will draw the lines of the chart
var draw = function () {
var drawData = function (dat, className, baseFill, area) {
//var selection (supermarket)
var supermarket, supermarketEnter;
supermarket = svg.selectAll('.' + className)
.data(dat,
function (c) {
return c.Vendor;
});
//var new selection (supermarketEnter)
supermarketEnter = supermarket.enter()
.append('g')
.attr('class', className)
.attr('data-vendor', function(d) { return d.Vendor; })
.attr('width', width)
.attr('height', height);
supermarketEnter.append('path');
//update new selection
supermarketEnter.attr('clip-path', 'url(#clipper)').attr('class', 'line').attr('id', function (d) { return d.Vendor; });
supermarketEnter
.selectAll("circle")
.data(function (d) {
return d.Values;
})
.enter()
.append('circle')
.attr('class', 'datapoint')
.attr('r', 4)
.style('fill',
function (d, i, j) {
return dat[j].Visible ? baseFill(j) : grayFill;
})
.attr('transform',
function (d) {
return 'translate(' + x(d.Month) + ',' + y(d.Value) + ')';
})
.on('mouseover',
function (d, i, j) {
d3.select('.tooltip').style('opacity', '1');
d3.select('.tooltip')
.html(dat[j].Vendor +
'<br/> (' +
moment(d.Month).format("MMM YYYY") +
', ' +
d.Value.toPrecision(2) +
'% )')
.style('left',
function () {
if (window.innerWidth >= 1200) {
var newWidth = d3.event.pageX -
($(chartEl).width() / 2) +
'px'
} else if (window.innerWidth < 1200) {
var newWidth = d3.event.pageX - ($(chartEl).width() / 10) + 'px'
}
return newWidth;
})
.style('top', (d3.event.pageY) - 300 + 'px');
})
.on("mouseout",
function (d) {
d3.select('.tooltip')
.style('transition', '500')
.style('opacity', 0)
.style('color', d3.select(this).style('fill'));
});
supermarket.select('path')
.transition()
.duration(500)
.attr('d',
function (d) {
return line(d.Values);
});
var path = supermarket.select('path')
.style('stroke', function (d, i) {
return (d.Visible ? baseFill(d.Index) : grayFill);
});
if (area) {
path.style('fill', function (d, i) { return (d.Visible ? baseFill(d.Index) : grayFill); });
}
supermarket.exit().remove();
supermarket.order();
}
svg.selectAll('.x.axis').call(xAxis);
svg.selectAll('.y.axis').call(yAxis);
drawData(chartData, 'supermarket', patterns, false);
return zoomed.x(x);
};
var render = function () {
configSize();
draw();
};
//insertion of drawLegend into chart function so that this will draw as well as the lines of the chart.
var drawLegend = function (dat, className) {
//DATA JOIN
//Join new data with old elements, if any.
var supermarket, supermarketEnter;
supermarket = svg.selectAll('legend_' + className)
.data(dat, function (c) { return c.Vendor; });
//UPDATE
//Update old elements as needed.
supermarket.attr('class', 'update');
//ENTER + UPDATE
//After merging the entered elements with the update selection, apply operations to both.
supermarketEnter = supermarket.enter().append('g')
.attr('class', 'legend_' + className)
.attr('data-vendor', function (d) { return d.Vendor; });
supermarketEnter
.append('text')
.attr('class', 'supermarket-name')
.attr('data-vendor', function (d) { return d.Vendor; });//
supermarket.select('text.supermarket-name')
.attr('x', width + 25)
.attr('y', function (d, i) { return (i * 1.25) * 20; })
.attr('dy', '.35em')
.text(function (d) {
return d.Vendor;
})
.on('click', function (d, i, j) {
dat[i].Visible = !dat[i].Visible;
var newOpacity = dat[i].Visible ? 1 : 0;
d3.select("[data-vendor='" + dat[i].Vendor + "']").style('opacity', newOpacity);
});
supermarketEnter.append('rect')
.attr('class', 'supermarket-dot');
supermarket.select('rect.supermarket-dot')
.attr('x', width + 10).attr('y', function (d, i) {
return ((i * 1.25) * 20) - 5;
})
.attr('width', 12)
.attr('height', 12)
.style('fill', function (d, i) {
return patterns(d.Index);
});
//EXIT
//Remove old elements as needed.
supermarket.exit().remove();
};
//insertion of drawLegend into chart function originally placed after configSize(); draw(); };
var toggleLines = function () {
var dat = $(this.closest("g")).data();
var obj = chartData.find(function (item) { return item.Vendor === dat.vendor; });
if (obj.Visible) {
obj.Visible = false;
} else {
obj.Visible = true;
}
chartData.sort(function (a, b) {
if (a.Visible === b.Visible) return 0;
if (a.Visible && !b.Visible) return 1;
return -1;
});
draw();
};
initialiseData(data);
configSize();
initialise();
draw();
drawLegend(chartData, 'supermarket');
tooltip();
$('.adjustmentZoom .tick text').attr('y', '25');
return {
Draw: render
};
};
var c = chart(chartElement, chartData);
//draw draws the lines of the chart. chart function draws the space arou
$(window).on("resize", function () {
c.Draw();
$('.adjustmentZoom .tick text').attr('y', '25');
});
}
var initialiseElement = function () {
categories.forEach(function (item) {
$(".slidesIntroduction").append('<div class="">\
<div class="insight-chart chart tradingData ' + item.Name + '" style="' + item.Style + '">\
<h3 class="chartTitle" ><br />\All ' + item.Title + '</h3>\
<p style="' + item.TitleStyle + '">\
<br />\
' + item.Text + '\
</p>\
</div>\
<//div>');
});
};
var renderInsightData = function () {
initialiseElement();
categories.forEach(function (item) {
if (item.DataValues) {
createChart("." + item.Name, item.DataValues);
}
});
};
var startWork = function () {
var catCount = 0;
var startRequest = function () {
App.Modules.ServerComms.PollForData({
Url: url,
Data: { src: categories[catCount].Name },
Success: processResponse,
});
};
var processResponse = function (response) {
categories[catCount].DataValues = getValues(response);
catCount++;
if (catCount < categories.length) {
startRequest();
} else {
renderInsightData();
$(".spinner").removeClass("spinner");
}
};
startRequest();
};
return {
Name: "Introduction",
Init: function () { },
Start: startWork
}
})());
//Builder for REal time flow chart
define((function () {
var categories = [
{
Name: "MediaServiceIndexes",
Title: "Media",
Style: "background-color:#ffffff;font-color:blue;",
Text: "Here are the top media companies with whom our customers shop.",
TitleStyle: "margin-left:0px"
},
];
var url = App.SiteUrl + "/Data/RequestDataFrom";
var svg, width, numberChecker;
var patterns = d3.scale.ordinal()
.range([
"url(#blue1)", "url(#yellow1)",
"url(#blue2)", "url(#yellow2)",
"url(#blue3)", "url(#yellow3)",
"url(#blue4)", "url(#yellow4)",
"url(#blue5)", "url(#yellow5)"
]);
var grayFill = "url(#gray)";
var colors = d3.scale.category10();
var getValues = function (values) {
var result = [];
try {
values.forEach(function (item) {
var o = {
Month: moment(item.month + "01", "YYYYMMDD"),
Value: parseFloat(item.change_in_market_share, 10)
};
var vendor = item.vendor.replace(/'/g, "");
//var e = result.find(function (it) { return it.Vendor == vendor; });
var e = false;
for (var x in result) {
if (result.hasOwnProperty(x) && typeof result[x] != "function") {
if (result[x].Vendor == vendor) {
e = (result[x]);
}
}
}
if (e) {
e.Values.push(o);
} else {
result.push({
Visible: true,
Vendor: vendor,
Values: [o]
});
}
});
result.forEach(function (item, i) {
item.Index = i;
item.Values.sort(function (a, b) {
return a.Month - b.Month;
});
});
} catch (e) {
result = null;
};
return result;
};
var createChart = function (chartElement, cd) {
var chartData = cd; //chart data is being passed in using cd via createChart function
//chart gives the location of the chart,
var chart = function (el, data) {
var margin = {
top: 20,
right: 180,
bottom: 50,
left: 110
};
var elem = el;
var chartEl = chartElement;
var fillData = [];
var defs,
gs,
height,
line,
maxDays,
minDays,
minValue,
maxValue,
x,
xAxis,
zoomBeh,
y,
yAxis;
var initialiseData = function (dataValues) {
minDays = d3.min(dataValues,
function (m) {
return d3.min(m.Values,
function (d) {
return d.Month;
});
});
maxDays = d3.max(dataValues,
function (m) {
return d3.max(m.Values,
function (d) {
return d.Month;
});
});
minValue = d3.min(chartData,
function (m) {
return d3.min(m.Values,
function (d) {
return d.Value;
});
});
maxValue = d3.max(chartData,
function (m) {
return d3.max(m.Values,
function (d) {
return d.Value;
});
});
console.log('min days: ' + minDays);
console.log('max days: ' + maxDays);
console.log('min value: ' + minValue);
console.log('max value: ' + maxValue);
dataValues.forEach(function (item) {
var nu = $.extend(true, {}, item);
nu.Values.push({ Month: maxDays, Value: minValue });
nu.Values.push({ Month: minDays, Value: minValue });
fillData.push(nu);
});
};
//initialise scales
var configSize = function () {
if (isNaN(numberChecker) === true) {
numberChecker = $(chartEl).width();
width = numberChecker;
}
width = 750;
height = 500 - margin.top - margin.bottom;
x = d3.scale.linear()
.range([0, width])
.domain([minDays, maxDays]);
y = d3.scale.linear()
.range([height, 0])
.domain([minValue, maxValue]);
$('.adjustmentZoom .tick text').attr('y', '25');
//initialise axis
xAxis = d3.svg.axis()
.scale(x)
.tickFormat(function (d, i) {
if (Math.floor(d) !== d) {
} else {
return moment(d).format("MMM YY");
}
})
.orient('bottom');
yAxis = d3.svg.axis()
.scale(y)
.tickFormat(function (d) {
return d3.round(d, 3) + "%"
})
.orient('left');
$('#clipper rect').attr('width', width);
$('.zoom-panel').attr('width', width);
};
//the path generator for the line chart
var initialise = function () {
line = d3.svg.line()
.interpolate(
'cardinal')
.x(function (d) {
return x(d.Month);
})
.y(function (d) {
return y(d.Value);
});
zoomBeh = d3.behavior.zoom()
.x(x)
.y(y)
.scaleExtent([
1,
500
])
.on('zoom', zoomed);
//elem is window
svg = d3.select(elem).append('div').attr('id', 'scatter').append('svg')
.attr('width', '100%')
.attr('height', '100%')
.attr('viewBox', '0 0 1000 550')
.attr('preserveAspectRatio', 'xMinYMin meet')
.append('g')
.attr("class", "line-container")
.attr('transform',
function () {
if (window.innerWidth > 650) {
var marginWidth = margin.left;
}
else if (window.innerWidth <= 650 && window.innerWidth > 549) {
var marginWidth = margin.left + 30;
}
else if (window.innerWidth <= 549) {
var marginWidth = margin.left + 50;
}
return 'translate(' + marginWidth + ',' + margin.top + ')'
}).call(zoomBeh).on("dblclick.zoom", null);
svg.append('rect')
.attr('class', 'zoom-panel')
.attr('width', width)
.attr('height', height)
.call(zoomBeh);
svg.append('g')
.attr('class', 'x axis')
.attr('transform', 'translate(0,' + height + ')')
.call(xAxis)
.selectAll('text');
svg.append('text')
.attr('x', 400)
.attr('y', (height + 70))
.style('text-anchor', 'middle')
.attr('class', 'xTitle')
.style('font-weight', '500')
.text('Date by month');
svg.append('g').attr('class', 'y axis').attr('transform', 'translate(0,0)').style('text-anchor', 'end').call(yAxis);
svg.append('text')
.attr('transform', 'rotate(-90)')
.attr('y', -100)
.attr('x', -200)
.attr('dy', '1em')
.attr('class', 'yTitle')
.style('text-anchor', 'middle')
.style('font-weight', '500')
.text('Share prices in percentage');
//zoom.scaleExtent([
// 1,
// moment(maxDays).diff(minDays, "months")
//]);
}
//draw calls drawData which will draw the lines of the chart
var draw = function () {
var drawData = function (dat, className, baseFill, area) {
//var selection (supermarket)
var supermarket, supermarketEnter;
supermarket = svg.selectAll('.' + className)
.data(dat,
function (c) {
return c.Vendor;
});
//var new selection (supermarketEnter)
supermarketEnter = supermarket.enter()
.append('g')
.attr('class', className)
.attr('data-vendor', function(d) { return d.Vendor; })
.attr('width', width)
.attr('height', height);
supermarketEnter.append('path');
//update new selection
supermarketEnter.attr('clip-path', 'url(#clipper)').attr('class', 'line').attr('id', function (d) { return d.Vendor; });
supermarket.select('path')
.transition()
.attr('class', 'line-data')
.duration(500)
.attr('d',
function (d) {
return line(d.Values);
});
var path = supermarket.select('path')
.style('stroke', function (d, i) {
return (d.Visible ? baseFill(d.Index) : grayFill);
});
if (area) {
path.style('fill', function (d, i) { return (d.Visible ? baseFill(d.Index) : grayFill); });
}
supermarketEnter
.selectAll("circle")
.data(function (d) {
return d.Values;
})
.enter()
.append('circle')
.attr('class', 'datapoint')
.attr('r', 4)
.style('fill',
function (d, i, j) {
return dat[j].Visible ? baseFill(j) : grayFill;
})
.attr('transform',
function (d) {
return 'translate(' + x(d.Month) + ',' + y(d.Value) + ')';
})
.on('mouseover',
function (d, i, j) {
d3.select('.tooltip').style('opacity', '1');
d3.select('.tooltip')
.html(dat[j].Vendor +
'<br/> (' +
moment(d.Month).format("MMM YYYY") +
', ' +
d.Value.toPrecision(2) +
'% )')
.style('left',
function () {
if (window.innerWidth >= 1200) {
var newWidth = d3.event.pageX -
($(chartEl).width() / 2) +
'px'
} else if (window.innerWidth < 1200) {
var newWidth = d3.event.pageX - ($(chartEl).width() / 10) + 'px'
}
return newWidth;
})
.style('top', (d3.event.pageY) - 300 + 'px');
})
.on("mouseout",
function (d) {
d3.select('.tooltip')
.style('transition', '500')
.style('opacity', 0)
.style('color', d3.select(this).style('fill'));
});
supermarket.exit().remove();
supermarket.order();
}
svg.selectAll('.x.axis').call(xAxis);
svg.selectAll('.y.axis').call(yAxis);
drawData(chartData, 'supermarket', patterns, false);
};
var render = function () {
configSize();
draw();
};
//insertion of drawLegend into chart function so that this will draw as well as the lines of the chart.
var drawLegend = function (dat, className) {
//DATA JOIN
//Join new data with old elements, if any.
var supermarket, supermarketEnter;
supermarket = svg.selectAll('legend_' + className)
.data(dat, function (c) { return c.Vendor; });
//UPDATE
//Update old elements as needed.
supermarket.attr('class', 'update');
//ENTER + UPDATE
//After merging the entered elements with the update selection, apply operations to both.
supermarketEnter = supermarket.enter().append('g')
.attr('class', 'legend_' + className)
.attr('data-vendor', function (d) { return d.Vendor; });
supermarketEnter
.append('text')
.attr('class', 'supermarket-name')
.attr('data-vendor', function (d) { return d.Vendor + "-legend"; });//
supermarket.select('text.supermarket-name')
.attr('x', width + 25)
.attr('y', function (d, i) { return (i * 1.25) * 20; })
.attr('dy', '.35em')
.text(function (d) {
return d.Vendor;
})
.on('click', function (d, i, j) {
dat[i].Visible = !dat[i].Visible;
var newOpacity = dat[i].Visible ? 1 : 0;
d3.select("[data-vendor='" + dat[i].Vendor + "']").style('opacity', newOpacity);
});
supermarketEnter.append('rect')
.attr('class', 'supermarket-dot');
supermarket.select('rect.supermarket-dot')
.attr('x', width + 10).attr('y', function (d, i) {
return ((i * 1.25) * 20) - 5;
})
.attr('width', 12)
.attr('height', 12)
.style('fill', function (d, i) {
return patterns(d.Index);
});
//EXIT
//Remove old elements as needed.
supermarket.exit().remove();
d3.select(elem).append('div').attr('class', 'tooltip').style('opacity', 0);
return createChart().chart().initialise().zoomBeh.x(x);
};
var svgTransform = function (d) {
return "translate(" + x(d.Month) + "," + y(d.Value) + ")";
};
var lineTransform = function(d) {
return line(d.Values);
}
var zoomed = function () {
svg.select(".x.axis").call(xAxis);
svg.select(".y.axis").call(yAxis);
svg.selectAll(".datapoint").attr("transform", svgTransform);
svg.selectAll(".line-data").attr("d", lineTransform);
};
//insertion of drawLegend into chart function originally placed after configSize(); draw(); };
var toggleLines = function () {
var dat = $(this.closest("g")).data();
var obj = chartData.find(function (item) { return item.Vendor === dat.vendor; });
if (obj.Visible) {
obj.Visible = false;
} else {
obj.Visible = true;
}
chartData.sort(function (a, b) {
if (a.Visible === b.Visible) return 0;
if (a.Visible && !b.Visible) return 1;
return -1;
});
draw();
};
initialiseData(data);
configSize();
initialise();
draw();
drawLegend(chartData, 'supermarket');
tooltip();
$('.adjustmentZoom .tick text').attr('y', '25');
return {
Draw: render
};
};
var c = chart(chartElement, chartData);
//draw draws the lines of the chart. chart function draws the space arou
$(window).on("resize", function () {
c.Draw();
$('.adjustmentZoom .tick text').attr('y', '25');
});
}
var initialiseElement = function () {
categories.forEach(function (item) {
$(".slidesIntroduction").append('<div class="">\
<div class="insight-chart chart tradingData ' + item.Name + '" style="' + item.Style + '">\
<h3 class="chartTitle" ><br />\All ' + item.Title + '</h3>\
<p style="' + item.TitleStyle + '">\
<br />\
' + item.Text + '\
</p>\
</div>\
<//div>');
});
};
var renderInsightData = function () {
initialiseElement();
categories.forEach(function (item) {
if (item.DataValues) {
createChart("." + item.Name, item.DataValues);
}
});
};
var startWork = function () {
var catCount = 0;
var startRequest = function () {
App.Modules.ServerComms.PollForData({
Url: url,
Data: { src: categories[catCount].Name },
Success: processResponse,
});
};
var processResponse = function (response) {
categories[catCount].DataValues = getValues(response);
catCount++;
if (catCount < categories.length) {
startRequest();
} else {
renderInsightData();
$(".spinner").removeClass("spinner");
}
};
startRequest();
};
return {
Name: "Introduction",
Init: function () { },
Start: startWork
}
})());

d3.js - adding rectangles reading from json file in a svg

I am adding legends(through rectangles) on a svg. The data is read from different json files for each group of legend. I would like to draw the rectangles in the same svg. However only one set of legend is being added. Only the first json data is drawn as rectangles. The second is ignored.
My code is :
svgcheckins= d3.select("#legend").append("svg").attr("id","svgcheckins")
.attr("width", 250)
.attr("height", 200)
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
d3.json("checkins.json", function(data)
{
CreateLegend('#legend',svgcheckins,"checkins",data,'A - Checkins','');
});
d3.json("builds.json", function(data)
{
CreateLegend('#legend',svgcheckins,"Builds",data,'B - Builds','');
});
function CreateLegend(div,svg,svgid,data,header,trail)
{
var traillength=0;
var svgelem;
jsondata = data;
console.log(data);
rectangle= svg.selectAll("rect").data(data).enter().append("rect");
var RectangleAttrb = rectangle
.attr("id", function (d,i) { return svgid + "id" + i ; })
.attr("x", function (d) { return d.x_axis; })
.attr("y", function (d) { return d.y_axis; })
.attr("width",function(d) { return d.width; } )
.attr("height",function(d) { return d.height; })
.style("stroke", function (d) { return d.border;})
.style("fill", function(d) { return d.color; });
var textparam = svg.selectAll("text").data(data).enter().append("text");
var yearheader = d3.select("#header");
if(yearheader.empty())
{
var textheader = svg.append("text").attr("dx",20).attr("dy",5).text(header).attr("id",header).attr("style","margin-bottom:21px;border-bottom: solid 2px #ffd97f; font-size:12px;")
}
if (trail.length == 0)
{
d3.select(header).attr("style","font-size:15.1px;text-decoration:underline");
}
var text = textparam .attr("x", function (d) { traillength = d.x_axis + d.width +10; return d.x_axis + d.width +10; })
.attr("y", function (d) { return d.y_axis + d.height-5; })
.attr("width",30 )
.attr("height",20)
.attr("style", "text-decoration:none")
.text(function(d) { return d.text; });
var yearheader = d3.select("#trail");
if (trail.length > 0 && yearheader.empty() )
{
svg.append("text").attr("id","trail").attr("dx",traillength-10).attr("dy",5).text(trail).attr("style","margin-bottom:21px;border-bottom: solid 2px #ffd97f; font-size:12px;" )
}
}
The json data are :
checkins.json
[
{ "x_axis":10, "y_axis": 10,"width":20,"height":15,"color" : "#FE2E2E","border":"#000000"},
{ "x_axis":30, "y_axis": 10,"width":20,"height":15,"color" : "#FE9A2E","border":"#000000"},
{ "x_axis":50, "y_axis": 10,"width":20,"height":15,"color" : "#FFFF00","border":"#000000"},
{ "x_axis":70, "y_axis":10,"width":20,"height":15,"color" : "#D0FA58","border":"#000000"},
{ "x_axis":90, "y_axis":10,"width":20,"height":15,"color" : "#01DF01","border":"#000000"}
]
builds.json
[
{ "x_axis":10, "y_axis":60,"width":20,"height":15,"color" : "#424242","border":"#000000"},
{ "x_axis":30, "y_axis":60,"width":20,"height":15,"color" : "#D8D8D8","border":"#000000"},
{ "x_axis":50, "y_axis":60,"width":20,"height":15,"color" : "#FFFF00","border":"#000000"},
{ "x_axis":70, "y_axis":60,"width":20,"height":15,"color" : "#FAFAFA","border":"#000000"},
{ "x_axis":90, "y_axis":60,"width":20,"height":15,"color" : "#81DAF5","border":"#000000"}
]
This is because of how D3's data matching work. In particular, I'm referring to the following line:
rectangle= svg.selectAll("rect").data(data).enter().append("rect");
This selects all rect elements and matches data to them. For the first legend, no rects are there, so nothing is matched and it works as you expect. However, if there's a rect already, it is matched to data and your enter selection is empty. This is because D3 matches based on array index by default.
There are basically two ways around this. You could either use the optional second argument to .data() to tell D3 how to match, or assign a distinguishing class to the created legend to be used in the .selectAll(). Your data doesn't seem to have any distinguishing attributes in it, but you can use svgid for this purpose:
rectangle= svg.selectAll("rect." + svgid).data(data).enter().append("rect");
rectangle.attr("class", svgid);
// rest of code

Resources