I have created a stacked bar graph using a tutorial. The stack is created with the following:
var layers = d3.layout.stack()(["passed", "failed", "skipped"].map(function(cause){
return data.map(function(d){
return {x: d.UP, y: +d[cause]};
});
}));
When I hover my mouse over a stack, I want the whole stack to be marked (e.g. stronger border line).
I am currently using:
var barMouseOverFunction = function(d, i) {
var bar = d3.select(this);
bar.attr("stroke", "black" );
However this only changes for the current stack. Any ideas of how to get all the bars in the stack?
EDIT FULLCODE:
d3.json('db/data.php', function(data) {
var layers = d3.layout.stack()(["passed", "failed", "skipped"].map(function(cause){
return data.map(function(d){
return {x: d.UP, y: +d[cause]};
});
}));
var n = 3, // number of layers
stack = d3.layout.stack(),
yGroupMax = d3.max(layers, function(layer) { return d3.max(layer, function(d) { return d.y; }); }),
yStackMax = d3.max(layers, function(layer) { return d3.max(layer, function(d) { return d.y0 + d.y; }); });
var x = d3.scale.ordinal()
.domain(data.map(function(d){return d.UP;}))
.rangeRoundBands([0, width], .4);
var y = d3.scale.linear()
.domain([0, yStackMax])
.range([height, 0]);
var color = d3.scale.ordinal()
.range(["#6ad46a", "#ed2828", "#fae650"]);
var xAxis = d3.svg.axis()
.scale(x)
.tickSize(0)
.tickPadding(6)
.orient("bottom");
// Grid lines
var gridcontainer = svg.append('g');
gridcontainer.selectAll("line").data(y.ticks(10)).enter().append("line")
.attr("x1", 0)
.attr("x2", width)
.attr("y1", y)
.attr("y2", y)
.style("stroke", "#87cbff")
.style("stroke-width", .3);
var layer = svg.selectAll(".layer")
.data(layers)
.enter().append("g")
.attr("class", "layer")
.style("fill", function(d, i) { return color(i); })
.style("stroke", function(d, i){return d3.rgb(color(i)).darker();});
var timeformatter = d3.time.format("%b %e %Y %x%p");
// On mouseover display test case information
var barMouseOverFunction = function(d, i) {
var bar = d3.select(this);
bar.attr("stroke", "black" );
var dataItem = data[i];
}
var barMouseOutFunction = function() {
var bar = d3.select(this);
bar.attr("stroke", function(d){
return StatusColor(d.result);
});
d3.select("p").text("");
}
var rect = layer.selectAll("rect")
.data(function(d) { return d; })
.enter().append("rect")
.attr("x", function(d) { return x(d.x); })
.attr("y", height)
.attr("width", x.rangeBand())
.attr("height", 0)
.attr("rx", 0.5)
.attr("ry", 0.5)
.on("mouseover", barMouseOverFunction)
.on("mouseout", barMouseOutFunction);
The easiest way to do this is probably to assign a separate class to the bars for a particular stack and then select based on that, i.e. something like
var rect = layer.selectAll("rect")
.data(function(d) { return d; })
.enter().append("rect")
.attr("class", function(d, i, j) { return "stack" + j; })
...
var barMouseOverFunction = function(d, i, j) {
d3.selectAll("rect.stack" + j).attr("stroke", "black");
var dataItem = data[i];
}
Related
I have a D3 visualization with a map and a bar chart. I am trying to get the bar chart to change depending on which circle on the map is clicked. Not sure how to do this. I have a function in my bar_chart.js file named update(newData) and a few extra arrays for the different circles on the map. Here is the link to the bl.ocks for the map and bar char.
js code for map
var myData = [21, 3, 5, 21, 15];
//Width and height
var w = 200;
var h = 125;
var yScale = null;
function draw(initialData) {
var xScale = d3.scale.ordinal()
.domain(d3.range(initialData.length))
.rangeRoundBands([0, w], 0.05);
yScale = d3.scale.linear()
.domain([0, d3.max(initialData)])
.range([0, h]);
//Create SVG element
var svg = d3.select("#chart")
.append("svg")
.attr("width", w)
.attr("height", h);
svg.selectAll("rect")
.data(initialData)
.enter()
.append("rect")
.attr("x", function(d, i) {
return xScale(i);
})
.attr("y", function(d) {
return h - yScale(d);
})
.attr("width", xScale.rangeBand())
.attr("height", function(d) {
return yScale(d);
})
.attr("fill", "steelblue");
svg.selectAll("text")
.data(initialData)
.enter()
.append("text")
.text(function(d) {
return d;
})
.attr("text-anchor", "middle")
.attr("x", function(d, i) {
return xScale(i) + xScale.rangeBand() / 2;
})
.attr("y", function(d) {
return h - yScale(d) + 14;
})
.attr("font-family", "sans-serif")
.attr("font-size", "11px")
.attr("fill", "white");
}
draw(myData);
//update function
function update(newData) {
yScale.domain([0, d3.max(newData)]);
var rects = d3.select("#chart svg")
.selectAll("rect")
.data(newData);
// enter selection
rects
.enter().append("rect");
// update selection
rects
.transition()
.duration(300)
.attr("y", function(d) {
return h - yScale(d);
})
.attr("height", function(d) {
return yScale(d);
})
// exit selection
rects
.exit().remove();
var texts = d3.select("#chart svg")
.selectAll("text")
.data(newData);
// enter selection
texts
.enter().append("rect");
// update selection
texts
.transition()
.duration(300)
.attr("y", function(d) {
return h - yScale(d) + 14;
})
.text(function(d) {
return d;
})
// exit selection
texts
.exit().remove();
}
var mk = [10,17,20,14,8];
var cn = [18,4,9,20,15];
var nd = [5,12,7,15,21];
d3.select("#update").on("click", function() { update(newData); });
You have to incorporate the barchart data in your cities.csv file.
In the on-click handler of cities.csv where you show the tooltip you have to transform the data from the CSV into an array and call the bar chart update() method with this array.
One way of doing is to replace the , from the bar chart data with another char and split the string and convert the parts to numbers.
var cityData = d.barchart.split('#').map(Number);
update(cityData);
You also have to set the attributes of the new rects and texts of the bar chart. And the x-position will change if the number of bars change.
I want to add labels that show the values on a line chart and can't figure out how to do this.
var svg = d3.select('svg')
.attr("width", width)
.attr("height", height);
var g = svg.append("g")
.attr("transform", "translate(" + 0 + "," + 0 + ")");
var x = d3.scaleTime()
.rangeRound([0, width]);
var y = d3.scaleLinear()
.rangeRound([height, 0]);
var line = d3.line()
.x(function(d) { return x(d.date)})
.y(function(d) { return y(d.value)})
x.domain(d3.extent(data, function(d) { return d.date }));
y.domain(d3.extent(data, function(d) { return d.value }));
I don't know how to add labels, like here for example (though my case is simpler, because I have only one chart)
I've generated some random data here. Relevant code to add the text labels:
g.selectAll('.textLabels')
.data(data).enter()
.append('text')
.classed('textLabels', true)
.attr('x', function(d) { return x(d.date); })
.attr('y', function(d) { return y(d.value); })
.text(function(d) { return d.value; });
which are positioned based on x(date) and y(value) values in the data array. You can adjust the offsets using dx, dy attributes and use some styles to fill/stroke the text.
var data = [];
function randomDate(start, end) {
return new Date(start.getTime() + Math.random() * (end.getTime() - start.getTime()));
}
for(var i=0; i<20; i++) {
var obj = {};
obj.date = randomDate(new Date(2018, 01,01), new Date());
obj.value = Math.floor(Math.random() * 50);
data.push(obj);
}
data.sort(function(a, b) {
return a.date > b.date ? -1 : 1;
})
var width = 800, height = 400;
var svg = d3.select('svg')
.attr("width", width)
.attr("height", height);
var g = svg.append("g")
.attr("transform", "translate(" + 0 + "," + 0 + ")");
var x = d3.scaleTime()
.rangeRound([0, width]);
var y = d3.scaleLinear()
.rangeRound([height, 0]);
var line = d3.line()
.x(function(d) {
return x(d.date)
})
.y(function(d) {
return y(d.value)
})
x.domain(d3.extent(data, function(d) {
return d.date
}));
y.domain(d3.extent(data, function(d) {
return d.value
}));
g.append("path")
.datum(data)
.attr("fill", "none")
.attr("stroke", "steelblue")
.attr("stroke-linejoin", "round")
.attr("stroke-linecap", "round")
.attr("stroke-width", 1.5)
.attr("d", line);
g.selectAll('.textLabels')
.data(data).enter()
.append('text')
.classed('textLabels', true)
.attr('x', function(d) { return x(d.date); })
.attr('y', function(d) { return y(d.value); })
.text(function(d) { return d.value; });
<script src="https://d3js.org/d3.v4.min.js"></script>
<svg></svg>
Let me know if you have any questions. Hope this helps.:)
I'm trying to build simple line chart using d3.js V4.
I've got basic concept from multiple bl.ocks samples.
My idea is to create char and then add data to it, with max 9 data points.
Here is view of what I've build so far:
I'm able to update line using this:
addValue: function(val) {
chartData.push(val);
if (chartData.length > 9) {
chartData.shift();
}
y.domain([
-2,
d3.max(chartData, function(d) {
return d + 2;
})
]);
var svg = element.transition();
svg
.select(".d3-line")
.duration(750)
.attr("d", valueline(chartData));
}
but I also want to add/move point and lines when I add new data, without this my buggy chart looks like this:
I'm adding initial points and lines using this code:
var lineGuides = svg
.append("g")
.selectAll(".d3-line-guides-group")
.data(chartData);
lineGuides
.enter()
.append("line")
.attr("class", "d3-line-guides")
.attr("x1", function(t, e) {
return x(e);
})
.attr("y1", function(t, a) {
return height;
})
.attr("x2", function(t, e) {
return x(e);
})
.attr("y2", function(t, a) {
return height;
})
.style("stroke", "rgba(255,255,255,0.3)")
.style("stroke-dasharray", "4,2")
.style("shape-rendering", "crispEdges")
.transition()
.duration(1000)
.delay(function(t, x) {
return 150 * x;
})
.attr("y2", function(t) {
return y(t);
})
.transition();
var points = svg
.insert("g")
.selectAll(".d3-line-circle")
.data(chartData)
.enter()
.append("circle")
.attr("class", "d3-line-circle d3-line-circle-medium")
.attr("cx", function(t, e) {
return x(e);
})
.attr("cy", function(t) {
return y(t);
})
.attr("r", 3)
.style("stroke", "#fff")
.style("fill", "#29B6F6")
.on("mouseover", function(t) {
d3
.select(this)
.transition()
.duration(250)
.attr("r", 5);
})
.on("mouseout", function(t) {
d3
.select(this)
.transition()
.duration(250)
.attr("r", 3);
});
points
.style("opacity", 0)
.transition()
.duration(250)
.ease(d3.easeLinear, 2)
.delay(1000)
.style("opacity", 1);
How can I add new points and update old when data changes?
Here is code that I have so far:
/* global window, define, module */
(function(global, factory) {
var MicroChart = factory(global);
if (typeof define === "function" && define.amd) {
// AMD support
define(function() {
return MicroChart;
});
} else if (typeof module === "object" && module.exports) {
// CommonJS support
module.exports = MicroChart;
} else {
// We are probably running in the browser
global.MicroChart = MicroChart;
}
})(typeof window === "undefined" ? this : window, function(global, undefined) {
var document = global.document;
var slice = Array.prototype.slice;
var MicroChart = (function() {
var defaultOptions = {
height: 50
};
function shallowCopy(/* source, ...targets*/) {
var target = arguments[0],
sources = slice.call(arguments, 1);
sources.forEach(function(s) {
for (k in s) {
if (s.hasOwnProperty(k)) {
target[k] = s[k];
}
}
});
return target;
}
return function MicroChart(elem, opts) {
opts = shallowCopy({}, defaultOptions, opts);
var gaugeContainer = elem,
chartHeigh = opts.height,
instance;
var xScale, yScale, valueline, x, y;
var chartData = [5, 8, 2];
var element;
function initializeMicroChart(elem, height) {
element = d3.select(elem);
var margins = { top: 0, right: 0, bottom: 0, left: 0 };
var width =
element.node().getBoundingClientRect().width -
margins.left -
margins.right;
var height = chartHeigh - margins.top - margins.bottom;
var l = 10;
x = d3
.scaleLinear()
.domain([0, 8])
.range([l, width - l]);
y = d3.scaleLinear().range([height, 0]);
valueline = d3
.line()
.x(function(d, i) {
console.log(i);
return x(i);
})
.y(function(d) {
return y(d);
});
var svg = element
.append("svg")
.attr("width", width + margins.left + margins.right)
.attr("height", height + margins.top + margins.bottom);
y.domain([
-2,
d3.max(chartData, function(d) {
return d + 2;
})
]);
var s4 =function() {
return Math.floor((1 + Math.random()) * 0x10000)
.toString(16)
.substring(1);
}
var guid = s4()+s4();
console.log(guid);
var path = svg
.append("path")
.data([chartData])
.attr("class", "d3-line d3-line-medium")
.attr("clip-path", "url(#"+guid+")")
.attr("d", valueline)
.style("stroke", "#fff");
var clipPath = svg
.append("defs")
.append("clipPath")
.attr("id", guid);
var rect = clipPath
.append("rect")
.attr("class", "clip")
.attr("width", 0)
.attr("height", height)
.attr("transform", null)
.transition()
.duration(1000)
.ease(d3.easeLinear, 2)
.attr("width", width);
var lineGuides = svg
.append("g")
.selectAll(".d3-line-guides-group")
.data(chartData);
lineGuides
.enter()
.append("line")
.attr("class", "d3-line-guides")
.attr("x1", function(t, e) {
return x(e);
})
.attr("y1", function(t, a) {
return height;
})
.attr("x2", function(t, e) {
return x(e);
})
.attr("y2", function(t, a) {
return height;
})
.style("stroke", "rgba(255,255,255,0.3)")
.style("stroke-dasharray", "4,2")
.style("shape-rendering", "crispEdges")
.transition()
.duration(1000)
.delay(function(t, x) {
return 150 * x;
})
.attr("y2", function(t) {
return y(t);
})
.transition();
var points = svg
.insert("g")
.selectAll(".d3-line-circle")
.data(chartData)
.enter()
.append("circle")
.attr("class", "d3-line-circle d3-line-circle-medium")
.attr("cx", function(t, e) {
return x(e);
})
.attr("cy", function(t) {
return y(t);
})
.attr("r", 3)
.style("stroke", "#fff")
.style("fill", "#29B6F6")
.on("mouseover", function(t) {
d3
.select(this)
.transition()
.duration(250)
.attr("r", 5);
})
.on("mouseout", function(t) {
d3
.select(this)
.transition()
.duration(250)
.attr("r", 3);
});
points
.style("opacity", 0)
.transition()
.duration(250)
.ease(d3.easeLinear, 2)
.delay(1000)
.style("opacity", 1);
}
instance = {
addValue: function(val) {
chartData.push(val);
if (chartData.length > 9) {
chartData.shift();
}
y.domain([
-2,
d3.max(chartData, function(d) {
return d + 2;
})
]);
var svg = element.transition();
svg
.select(".d3-line")
.duration(750)
.attr("d", valueline(chartData));
}
};
initializeMicroChart(gaugeContainer, chartHeigh);
return instance;
};
})();
return MicroChart;
});
var gauge1 = MicroChart(document.getElementById("chart1"));
var gauge2 = MicroChart(document.getElementById("chart2"), {
height: 70
});
var randomInt = function(min, max) {
return Math.floor(Math.random() * (max - min + 1)) + min;
};
$("#update").on("click", function() {
gauge1.addValue(randomInt(5, 15));
gauge2.addValue(randomInt(5, 15));
});
And here is codepen to see my code in action: https://codepen.io/Misiu/pen/dmGyZW?editors=0010
Use .enter().append() to add new nodes, .merge() to merge existing and appended nodes, then update all nodes and call .exit().remove() to remove unnecessary nodes. So, you could use following update pattern:
d3.select(window).on('load', function() {
// Join data
var joined = d3.select('div').selectAll('p').data([1, 2, 3, 4, 5]);
joined
// Add new elements
.enter().append('p')
// Merge both new and existing elements
.merge(joined)
// Update new and existing elements
.text(d => d);
// Remove excess elements
joined.exit().remove();
});
<script src="https://d3js.org/d3.v4.min.js"></script>
<div>
<p>a</p>
<p>b</p>
<p>c</p>
</div>
As result, the two <p> elements will be created and all <p> elements will be updated.
See also General Update Pattern, I.
I'm trying to add a drop down menu that selects which measure to graph. I have 8 graphs, all graphing the same measure but by different ethnicities. Below is the code, any thoughts on what I'm doing wrong? Right now i get the error exit() is not a function.
Ok i've made some progress with the following, however it's still a little wonky. The graphs are changing but are going off the charts - the yAxis is rescaling to the max of all of the graphs, not the local one.:
function updateGraphs(newData) {
d3.selectAll("svg").each(function(d, i){
eachRace = d.values;
svg = d3.select(this);
yMax = d3.max(eachRace, function(d) { return d[newData]; });
yScale = d3.scale.linear().domain([0, yMax*1.25]).range([height/8, 0]).nice();
yAxis = d3.svg.axis().scale(yScale).orient("left").ticks(5);
line = d3.svg.line()
.x(function (d) { return x(d.date); })
.y(function (d) { return yScale(d[newData]); })
d3.transition().duration(1000).selectAll(".line")
.attr("id", function(d) { return d.key ;})
.attr('class', 'line')
.attr('opacity', .8)
.attr('d', function(d) { return line(d.values); })
d3.selectAll(".y.axis")
.call(yAxis);
});
}
var svgContainer = d3.select("body").selectAll("svg")
.data(data2)
svgContainer.enter()
.append("svg")
.attr("width", 150)
.attr("height", 400)
.attr("transform", "translate(" + margin.left + "," + margin.top+ ")");
d3.selectAll("svg").each(function(d, i){
var eachRace = d.values;
var svg = d3.select(this);
var yMax = d3.max(eachRace, function(d) { return d.app; });
var yScale = d3.scale.linear().domain([0, yMax*1.25]).range([height/8, 0]).nice();
var yAxis = d3.svg.axis().scale(yScale).orient("left").ticks(5);
var line = d3.svg.line()
.x(function (d) { return x(d.date); })
.y(function (d) { return yScale(d.app); })
svg.append("path")
.attr("id", function(d) { return d.key ;})
.attr('class', 'line')
.attr('opacity', .8)
.attr('d', function(d) { return line(d.values); })
svg.append("g")
.attr("class", "y axis")
.call(yAxis);
});
Thanks JSBob for working with me. I've got it working with:
function updateGraphs(newData) {
d3.selectAll("svg").each(function(d, i){
eachRace = d.values;
svg = d3.select(this);
yMax = d3.max(eachRace, function(d) { return d[newData]; });
yScale = d3.scale.linear().domain([0, yMax*1.25]).range([height/8, 0]).nice();
yAxis = d3.svg.axis().scale(yScale).orient("left").ticks(5);
console.log(yMax);
line = d3.svg.line()
.x(function (d) { return x(d.date); })
.y(function (d) { return yScale(d[newData]); })
svg.transition().duration(1000).select(".line")
.attr("id", function(d) { return d.key ;})
.attr('class', 'line')
.attr('opacity', .8)
.attr('d', function(d) { return line(d.values); });
svg.transition().duration(1000).select(".y.axis")
.call(yAxis);
});
}
jsfiddle example can be seen here
I'm expecting the chart to update correctly with the new dataset contained in the function "redraw", however this isn't happening.
Although the current rects are updated with the new dataset, the issue seems to be that the enter().append() part of the code isn't working:
var groups = svg.selectAll('g')
.data(dataset, retd);
groups.style('fill', function (d, i) {
return colours(i);
});
groups.enter()
.append('g')
.style('fill', function (d, i) {
return colours(i);
});
groups.exit().remove();
//update the Rects
var rects = groups.selectAll('rect')
.data(function (d) {
return d;
}, thisy);
rects.transition()
.attr('x', function (d) {
return xScale(d.x0);
})
.attr('y', function (d, i) {
return yScale(d.y);
})
.attr('height', function (d) {
return yScale.rangeBand();
})
.attr('width', function (d) {
return xScale(d.x);
});
rects.enter()
.append('rect')
.attr('x', function (d) {
return xScale(d.x0);
})
.attr('y', function (d, i) {
return yScale(d.y);
})
.attr('height', function (d) {
return yScale.rangeBand();
})
.attr('width', function (d) {
return xScale(d.x);
});
Any help/insight would be appreciated.
Since the first draw worked fine, I turned it into a function. Only had to remove the already written SVG to make it reusable:
JSFiddle
<button id="reset">Redraw</button>
<script>
document.getElementById('reset').onclick = function() {
var dataset = [
{"data":[
{"IssueMonth":"Apr","IS":"350","month":"Apr"},
{"IssueMonth":null,"IS":"100","month":"Aug"},
{"IssueMonth":null,"IS":"0","month":"Dec"}],
"name":"CHF"},
{"data":[
{"IssueMonth":null,"IS":"100","month":"Apr"},
{"IssueMonth":null,"IS":"200","month":"Aug"},
{"IssueMonth":null,"IS":"40","month":"Dec"}],
"name":"GBP"}
];
drawit(dataset);
}
var dataset = [
{"data":[
{"IssueMonth":"Apr","IS":"350","month":"Apr"},
{"IssueMonth":null,"IS":"0","month":"Aug"},
{"IssueMonth":null,"IS":"0","month":"Dec"}],
"name":"CHF"},
{"data":[
{"IssueMonth":null,"IS":"100","month":"Apr"},
{"IssueMonth":null,"IS":"0","month":"Aug"},
{"IssueMonth":null,"IS":"50","month":"Dec"}],
"name":"GBP"}
];
drawit(dataset);
function drawit(dataset) {
var margins = {
top: 40,
left: 40,
right: 40,
bottom: 40
};
var width = 400;
var height = 500 - margins.top - margins.bottom;
var series = dataset.map(function (d) {
return d.name;
});
var dataset = dataset.map(function (d) {
return d.data.map(function (o, i) {
// Structure it so that your numeric
// axis (the stacked amount) is y
return {
y: +o.IS,
x: o.month
};
});
});
stack = d3.layout.stack();
stack(dataset);
var dataset = dataset.map(function (group) {
return group.map(function (d) {
// Invert the x and y values, and y0 becomes x0
return {
x: d.y,
y: d.x,
x0: d.y0
};
});
});
d3.select("svg").remove();
var svg = d3.select('body')
.append('svg')
.attr('width', width + margins.left + margins.right)
.attr('height', height + margins.top + margins.bottom)
.attr('transform', 'translate(' + margins.left + ',' + margins.top + ')')
var xMax = d3.max(dataset, function (group) {
return d3.max(group, function (d) {
return d.x + d.x0;
});
})
var xScale = d3.scale.linear()
.domain([0, xMax])
.range([0, width]);
var months = dataset[0].map(function (d) {
return d.y;
});
var yScale = d3.scale.ordinal()
.domain(months)
.rangeRoundBands([0, height], .1);
var xAxis = d3.svg.axis()
.scale(xScale)
.orient('bottom');
var yAxis = d3.svg.axis()
.scale(yScale)
.orient('left');
var colours = d3.scale.category20();
var groups = svg.selectAll('g')
.data(dataset)
.enter()
.append('g')
.style('fill', function (d, i) {
return colours(i);
});
var rects = groups.selectAll('rect')
.data(function (d) {
return d;
})
.enter()
.append('rect')
.attr('x', function (d) {
return xScale(d.x0);
})
.attr('y', function (d, i) {
return yScale(d.y);
})
.attr('height', function (d) {
return yScale.rangeBand();
})
.attr('width', function (d) {
return xScale(d.x);
});
svg.append('g')
.attr('class', 'x axis')
.attr('transform', 'translate(0,' + height + ')')
.call(xAxis);
svg.append('g')
.attr('class', 'y axis')
.call(yAxis);
}
</script>