D3 v4 Realtime Chart with Brushing - javascript

I'm trying to get this block working with d3 v4 and can't figure out what I'm doing wrong. I've placed my code in a fiddle, there is an issue with the setInterval and brushing - the extent is expecting a date object but I'm not sure how to get it.
function realTimeChart() {
var version = "0.1.0",
datum, initialData, data,
maxSeconds = 300, pixelsPerSecond = 10,
svgWidth = 700, svgHeight = 300,
margin = { top: 20, bottom: 20, left: 50, right: 30, topNav: 10, bottomNav: 20 },
dimension = { chartTitle: 20, xAxis: 20, yAxis: 20, xTitle: 20, yTitle: 20, navChart: 70 },
barWidth = 3,
maxY = 100, minY = 0,
chartTitle, yTitle, xTitle,
drawXAxis = true, drawYAxis = true, drawNavChart = true,
border,
selection,
barId = 0;
// create the chart
var chart = function(s) {
selection = s;
if (selection == undefined) {
console.error("selection is undefined");
return;
};
// process titles
chartTitle = chartTitle || "";
xTitle = xTitle || "";
yTitle = yTitle || "";
// compute component dimensions
var chartTitleDim = chartTitle == "" ? 0 : dimension.chartTitle;
var xTitleDim = xTitle == "" ? 0 : dimension.xTitle;
var yTitleDim = yTitle == "" ? 0 : dimension.yTitle;
var xAxisDim = !drawXAxis ? 0 : dimension.xAxis;
var yAxisDim = !drawYAxis ? 0 : dimension.yAxis;
var navChartDim = !drawNavChart ? 0 : dimension.navChart;
// compute chart dimension and offset
var marginTop = margin.top + chartTitleDim;
var height = svgHeight - marginTop - margin.bottom - chartTitleDim - xTitleDim - xAxisDim - navChartDim + 30;
var heightNav = navChartDim - margin.topNav - margin.bottomNav;
var marginTopNav = svgHeight - margin.bottom - heightNav - margin.topNav;
var width = svgWidth - margin.left - margin.right;
var widthNav = width;
// append the svg
var svg = selection.append("svg")
.attr("width", svgWidth)
.attr("height", svgHeight)
.style("border", function(d) {
if (border) return "1px solid lightgray";
else return null;
});
// create main group and translate
var main = svg.append("g")
.attr("transform", "translate (" + margin.left + "," + marginTop + ")");
// define clip-path
main.append("defs").append("clipPath")
.attr("id", "myClip")
.append("rect")
.attr("x", 0)
.attr("y", 0)
.attr("width", width)
.attr("height", height);
// create chart background
main.append("rect")
.attr("x", 0)
.attr("y", 0)
.attr("width", width)
.attr("height", height)
.style("fill", "#f5f5f5");
// note that two groups are created here, the latter assigned to barG;
// the former will contain a clip path to constrain objects to the chart area;
// no equivalent clip path is created for the nav chart as the data itself
// is clipped to the full time domain
var barG = main.append("g")
.attr("class", "barGroup")
.attr("transform", "translate(0, 0)")
.attr("clip-path", "url(#myClip")
.append("g");
// add group for x axis
var xAxisG = main.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + height + ")");
// add group for y axis
var yAxisG = main.append("g")
.attr("class", "y axis");
// in x axis group, add x axis title
xAxisG.append("text")
.attr("class", "title")
.attr("x", width / 2)
.attr("y", 25)
.attr("dy", ".71em")
.text(function(d) {
var text = xTitle == undefined ? "" : xTitle;
return text;
});
// in y axis group, add y axis title
yAxisG.append("text")
.attr("class", "title")
.attr("transform", "rotate(-90)")
.attr("x", - height / 2)
.attr("y", -35)
.attr("dy", ".71em")
.text(function(d) {
var text = yTitle == undefined ? "" : yTitle;
return text;
});
// in main group, add chart title
main.append("text")
.attr("class", "chartTitle")
.attr("x", width / 2)
.attr("y", -20)
.attr("dy", ".71em")
.text(function(d) {
var text = chartTitle == undefined ? "" : chartTitle;
return text;
});
// define main chart scales
var x = d3.scaleTime().range([0, width]);
var y = d3.scaleLinear().domain([minY, maxY]).range([height, 0]);
// define main chart axis
var xAxis = d3.axisBottom(x);
var yAxis = d3.axisLeft(y);
// add nav chart
var nav = svg.append("g")
.attr("transform", "translate (" + margin.left + "," + marginTopNav + ")");
// add nav background
nav.append("rect")
.attr("x", 0)
.attr("y", 0)
.attr("width", width)
.attr("height", heightNav)
.style("fill", "#F5F5F5")
.style("shape-rendering", "crispEdges")
.attr("transform", "translate(0, 0)");
// add group to hold line and area paths
var navG = nav.append("g")
.attr("class", "nav");
// add group to hold nav x axis
var xAxisGNav = nav.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + heightNav + ")");
// define nav scales
var xNav = d3.scaleTime().range([0, widthNav]);
var yNav = d3.scaleLinear().domain([minY, maxY]).range([heightNav, 0]);
// define nav axis
var xAxisNav = d3.axisBottom(xNav);
// define function that will draw the nav area chart
var navArea = d3.area()
.x(function (d) { return xNav(d.time); })
.y1(function (d) { return yNav(d.value); })
.y0(heightNav);
// define function that will draw the nav line chart
var navLine = d3.line()
.x(function (d) { return xNav(d.time); })
.y(function (d) { return yNav(d.value); });
// compute initial time domains...
var ts = new Date().getTime();
// first, the full time domain
var endTime = new Date(ts);
var startTime = new Date(endTime.getTime() - maxSeconds * 1000);
var interval = endTime.getTime() - startTime.getTime();
// then the viewport time domain (what's visible in the main chart
// and the viewport in the nav chart)
var endTimeViewport = new Date(ts);
var startTimeViewport = new Date(endTime.getTime() - width / pixelsPerSecond * 1000);
var intervalViewport = endTimeViewport.getTime() - startTimeViewport.getTime();
var offsetViewport = startTimeViewport.getTime() - startTime.getTime();
// set the scale domains for main and nav charts
x.domain([startTimeViewport, endTimeViewport]);
xNav.domain([startTime, endTime]);
// update axis with modified scale
xAxis.scale(x)(xAxisG);
yAxis.scale(y)(yAxisG);
xAxisNav.scale(xNav)(xAxisGNav);
// create brush (moveable, changable rectangle that determines
// the time domain of main chart)
var sel;
var viewport = d3.brush()
// .x(xNav)
.extent([startTimeViewport, endTimeViewport])
.on("brush", function () {
// get the current time extent of viewport
// var extent = viewport.extent();
var extent = d3.event.selection || x.range();
console.log(extent);
startTimeViewport = extent[0];
endTimeViewport = extent[1];
intervalViewport = endTimeViewport.getTime() - startTimeViewport.getTime();
offsetViewport = startTimeViewport.getTime() - startTime.getTime();
// handle invisible viewport
if (intervalViewport == 0) {
intervalViewport = maxSeconds * 1000;
offsetViewport = 0;
}
// update the x domain of the main chart
x.domain((extent === null) ? xNav.domain() : extent);
// update the x axis of the main chart
xAxis.scale(x)(xAxisG);
// update display
refresh();
});
window.viewport = viewport;
// create group and assign to brush
var viewportG = nav.append("g")
.attr("class", "viewport")
.call(viewport)
.selectAll("rect")
.attr("height", heightNav);
// initial invocation
data = initialData || [];
// update display
refresh();
// function to refresh the viz upon changes of the time domain
// (which happens constantly), or after arrival of new data,
// or at init
function refresh() {
// process data to remove too late or too early data items
// (the latter could occur if the chart is stopped, while data
// is being pumped in)
data = data.filter(function(d) {
if (d.time.getTime() > startTime.getTime() &&
d.time.getTime() < endTime.getTime())
return true;
})
// here we bind the new data to the main chart
// note: no key function is used here; therefore the data binding is
// by index, which effectivly means that available DOM elements
// are associated with each item in the available data array, from
// first to last index; if the new data array contains fewer elements
// than the existing DOM elements, the LAST DOM elements are removed;
// basically, for each step, the data items "walks" leftward (each data
// item occupying the next DOM element to the left);
// This data binding is very different from one that is done with a key
// function; in such a case, a data item stays "resident" in the DOM
// element, and such DOM element (with data) would be moved left, until
// the x position is to the left of the chart, where the item would be
// exited
var updateSel = barG.selectAll(".bar")
.data(data);
// remove items
updateSel.exit().remove();
// append items
updateSel.enter().append("rect")
.attr("class", "bar")
.attr("id", function() {
return "bar-" + barId++;
})
.attr("shape-rendering", "crispEdges");
// update items
updateSel
.attr("x", function(d) { return Math.round(x(d.time) - barWidth); })
.attr("y", function(d) { return y(d.value); })
.attr("width", barWidth)
.attr("height", function(d) { return height - y(d.value); })
.style("fill", function(d) { return d.color == undefined ? "black" : d.color; })
//.style("stroke", "none")
//.style("stroke-width", "1px")
//.style("stroke-opacity", 0.5)
.style("fill-opacity", 1);
// also, bind data to nav chart
// first remove current paths
navG.selectAll("path").remove();
// then append area path...
navG.append('path')
.attr('class', 'area')
.attr('d', navArea(data));
// ...and line path
navG.append('path')
.attr('class', 'line')
.attr('d', navLine(data));
} // end refreshChart function
// function to keep the chart "moving" through time (right to left)
window.intvl = setInterval(function() {
// get current viewport extent
// var extent = viewport.empty() ? xNav.domain() : viewport.extent();
var extent = (sel === null) ? xNav.domain() : viewport.extent();
window.extent = extent;
var interval = extent[1].getTime() - extent[0].getTime();
var offset = extent[0].getTime() - xNav.domain()[0].getTime();
// compute new nav extents
endTime = new Date();
startTime = new Date(endTime.getTime() - maxSeconds * 1000);
// compute new viewport extents
startTimeViewport = new Date(startTime.getTime() + offset);
endTimeViewport = new Date(startTimeViewport.getTime() + interval);
viewport.extent([startTimeViewport, endTimeViewport])
// update scales
x.domain([startTimeViewport, endTimeViewport]);
xNav.domain([startTime, endTime]);
// update axis
xAxis.scale(x)(xAxisG);
xAxisNav.scale(xNav)(xAxisGNav);
// refresh svg
refresh();
}, 200)
// end setInterval function
return chart;
} // end chart function
// chart getter/setters
// array of inital data
chart.initialData = function(_) {
if (arguments.length == 0) return initialData;
initialData = _;
return chart;
}
// new data item (this most recent item will appear
// on the right side of the chart, and begin moving left)
chart.datum = function(_) {
if (arguments.length == 0) return datum;
datum = _;
data.push(datum);
return chart;
}
// svg width
chart.width = function(_) {
if (arguments.length == 0) return svgWidth;
svgWidth = _;
return chart;
}
// svg height
chart.height = function(_) {
if (arguments.length == 0) return svgHeight;
svgHeight = _;
return chart;
}
// svg border
chart.border = function(_) {
if (arguments.length == 0) return border;
border = _;
return chart;
}
// chart title
chart.title = function(_) {
if (arguments.length == 0) return chartTitle;
chartTitle = _;
return chart;
}
// x axis title
chart.xTitle = function(_) {
if (arguments.length == 0) return xTitle;
xTitle = _;
return chart;
}
// y axis title
chart.yTitle = function(_) {
if (arguments.length == 0) return yTitle;
yTitle = _;
return chart;
}
// bar width
chart.barWidth = function(_) {
if (arguments.length == 0) return barWidth;
barWidth = _;
return chart;
}
// version
chart.version = version;
return chart;
} // end realTimeChart function
https://jsfiddle.net/drhhw8c2/
Thanks in advance for any help!

Finally got a working solution based on Mark's advice: https://jsfiddle.net/drhhw8c2/1/
function realTimeChart() {
var version = "0.1.0",
datum, initialData, data,
maxSeconds = 300, pixelsPerSecond = 10,
svgWidth = 700, svgHeight = 300,
margin = { top: 20, bottom: 20, left: 50, right: 30, topNav: 10, bottomNav: 20 },
dimension = { chartTitle: 20, xAxis: 20, yAxis: 20, xTitle: 20, yTitle: 20, navChart: 70 },
barWidth = 3,
maxY = 100, minY = 0,
chartTitle, yTitle, xTitle,
drawXAxis = true, drawYAxis = true, drawNavChart = true,
border,
selection,
barId = 0;
// create the chart
var chart = function(s) {
selection = s;
if (selection == undefined) {
console.error("selection is undefined");
return;
};
// process titles
chartTitle = chartTitle || "";
xTitle = xTitle || "";
yTitle = yTitle || "";
// compute component dimensions
var chartTitleDim = chartTitle == "" ? 0 : dimension.chartTitle;
var xTitleDim = xTitle == "" ? 0 : dimension.xTitle;
var yTitleDim = yTitle == "" ? 0 : dimension.yTitle;
var xAxisDim = !drawXAxis ? 0 : dimension.xAxis;
var yAxisDim = !drawYAxis ? 0 : dimension.yAxis;
var navChartDim = !drawNavChart ? 0 : dimension.navChart;
// compute chart dimension and offset
var marginTop = margin.top + chartTitleDim;
var height = svgHeight - marginTop - margin.bottom - chartTitleDim - xTitleDim - xAxisDim - navChartDim + 30;
var heightNav = navChartDim - margin.topNav - margin.bottomNav;
var marginTopNav = svgHeight - margin.bottom - heightNav - margin.topNav;
var width = svgWidth - margin.left - margin.right;
var widthNav = width;
// append the svg
var svg = selection.append("svg")
.attr("width", svgWidth)
.attr("height", svgHeight)
.style("border", function(d) {
if (border) return "1px solid lightgray";
else return null;
});
// create main group and translate
var main = svg.append("g")
.attr("transform", "translate (" + margin.left + "," + marginTop + ")");
// define clip-path
main.append("defs").append("clipPath")
.attr("id", "myClip")
.append("rect")
.attr("x", 0)
.attr("y", 0)
.attr("width", width)
.attr("height", height);
// create chart background
main.append("rect")
.attr("x", 0)
.attr("y", 0)
.attr("width", width)
.attr("height", height)
.style("fill", "#f5f5f5");
// note that two groups are created here, the latter assigned to barG;
// the former will contain a clip path to constrain objects to the chart area;
// no equivalent clip path is created for the nav chart as the data itself
// is clipped to the full time domain
var barG = main.append("g")
.attr("class", "barGroup")
.attr("transform", "translate(0, 0)")
.attr("clip-path", "url(#myClip")
.append("g");
// add group for x axis
var xAxisG = main.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + height + ")");
// add group for y axis
var yAxisG = main.append("g")
.attr("class", "y axis");
// in x axis group, add x axis title
xAxisG.append("text")
.attr("class", "title")
.attr("x", width / 2)
.attr("y", 25)
.attr("dy", ".71em")
.text(function(d) {
var text = xTitle == undefined ? "" : xTitle;
return text;
});
// in y axis group, add y axis title
yAxisG.append("text")
.attr("class", "title")
.attr("transform", "rotate(-90)")
.attr("x", - height / 2)
.attr("y", -35)
.attr("dy", ".71em")
.text(function(d) {
var text = yTitle == undefined ? "" : yTitle;
return text;
});
// in main group, add chart title
main.append("text")
.attr("class", "chartTitle")
.attr("x", width / 2)
.attr("y", -20)
.attr("dy", ".71em")
.text(function(d) {
var text = chartTitle == undefined ? "" : chartTitle;
return text;
});
// define main chart scales
var x = d3.scaleTime().range([0, width]);
var y = d3.scaleLinear().domain([minY, maxY]).range([height, 0]);
// define main chart axis
var xAxis = d3.axisBottom(x);
var yAxis = d3.axisLeft(y);
// add nav chart
var nav = svg.append("g")
.attr("transform", "translate (" + margin.left + "," + marginTopNav + ")");
// add nav background
nav.append("rect")
.attr("x", 0)
.attr("y", 0)
.attr("width", width)
.attr("height", heightNav)
.style("fill", "#F5F5F5")
.style("shape-rendering", "crispEdges")
.attr("transform", "translate(0, 0)");
// add group to hold line and area paths
var navG = nav.append("g")
.attr("class", "nav");
// add group to hold nav x axis
var xAxisGNav = nav.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + heightNav + ")");
// define nav scales
var xNav = d3.scaleTime().range([0, widthNav]);
var yNav = d3.scaleLinear().domain([minY, maxY]).range([heightNav, 0]);
// define nav axis
var xAxisNav = d3.axisBottom(xNav);
// define function that will draw the nav area chart
var navArea = d3.area()
.x(function (d) { return xNav(d.time); })
.y1(function (d) { return yNav(d.value); })
.y0(heightNav);
// define function that will draw the nav line chart
var navLine = d3.line()
.x(function (d) { return xNav(d.time); })
.y(function (d) { return yNav(d.value); });
// compute initial time domains...
var ts = new Date().getTime();
// first, the full time domain
var endTime = new Date(ts);
var startTime = new Date(endTime.getTime() - maxSeconds * 1000);
var interval = endTime.getTime() - startTime.getTime();
// then the viewport time domain (what's visible in the main chart
// and the viewport in the nav chart)
var endTimeViewport = new Date(ts);
var startTimeViewport = new Date(endTime.getTime() - width / pixelsPerSecond * 1000);
var intervalViewport = endTimeViewport.getTime() - startTimeViewport.getTime();
var offsetViewport = startTimeViewport.getTime() - startTime.getTime();
// set the scale domains for main and nav charts
x.domain([startTimeViewport, endTimeViewport]);
xNav.domain([startTime, endTime]);
// update axis with modified scale
xAxis.scale(x)(xAxisG);
yAxis.scale(y)(yAxisG);
xAxisNav.scale(xNav)(xAxisGNav);
// create brush (moveable, changable rectangle that determines
// the time domain of main chart)
var sel;
var viewport = d3.brushX()
// .x(xNav)
// .extent([startTimeViewport, endTimeViewport])
.extent([ [0,0], [widthNav,heightNav] ])
.on("brush end", function () {
// get the current time extent of viewport
var extent = d3.event.selection || xNav.range();
// var extent = d3.event.selection || x.range();
// console.log(extent);
startTimeViewport = xNav.invert(extent[0]);
endTimeViewport = xNav.invert(extent[1]);
console.log(startTimeViewport.getMinutes() + ':' + startTimeViewport.getSeconds() + ' - ' + endTimeViewport.getMinutes() + ':' + endTimeViewport.getSeconds());
intervalViewport = endTimeViewport.getTime() - startTimeViewport.getTime();
offsetViewport = startTimeViewport.getTime() - startTime.getTime();
// handle invisible viewport
if (intervalViewport == 0) {
intervalViewport = maxSeconds * 1000;
offsetViewport = 0;
}
// update the x domain of the main chart
x.domain((extent === null) ? xNav.domain() : extent);
// update the x axis of the main chart
xAxis.scale(x)(xAxisG);/**/
sel = d3.event.selection || xNav.range();
x.domain(sel.map(xNav.invert, xNav));
// update display
refresh();
});
window.viewport = viewport;
// create group and assign to brush
var viewportG = nav.append("g")
.attr("class", "viewport")
.call(viewport)
.selectAll("rect")
.attr("height", heightNav);
// initial invocation
data = initialData || [];
// update display
refresh();
// function to refresh the viz upon changes of the time domain
// (which happens constantly), or after arrival of new data,
// or at init
function refresh() {
// process data to remove too late or too early data items
// (the latter could occur if the chart is stopped, while data
// is being pumped in)
data = data.filter(function(d) {
if (d.time.getTime() > startTime.getTime() &&
d.time.getTime() < endTime.getTime())
return true;
})
// here we bind the new data to the main chart
// note: no key function is used here; therefore the data binding is
// by index, which effectivly means that available DOM elements
// are associated with each item in the available data array, from
// first to last index; if the new data array contains fewer elements
// than the existing DOM elements, the LAST DOM elements are removed;
// basically, for each step, the data items "walks" leftward (each data
// item occupying the next DOM element to the left);
// This data binding is very different from one that is done with a key
// function; in such a case, a data item stays "resident" in the DOM
// element, and such DOM element (with data) would be moved left, until
// the x position is to the left of the chart, where the item would be
// exited
var updateSel = barG.selectAll(".bar")
.data(data);
// remove items
updateSel.exit().remove();
// append items
updateSel.enter().append("rect")
.attr("class", "bar")
.attr("id", function() {
return "bar-" + barId++;
})
.attr("shape-rendering", "crispEdges");
// update items
updateSel
.attr("x", function(d) {
// console.log(x(d.time));
var val = Math.round(x(d.time) - barWidth);
if(val !== val) {
val = 0;
}
return val; })
.attr("y", function(d) { return y(d.value); })
.attr("width", barWidth)
.attr("height", function(d) { return height - y(d.value); })
.style("fill", function(d) { return d.color == undefined ? "black" : d.color; })
//.style("stroke", "none")
//.style("stroke-width", "1px")
//.style("stroke-opacity", 0.5)
.style("fill-opacity", 1);
// also, bind data to nav chart
// first remove current paths
navG.selectAll("path").remove();
// then append area path...
navG.append('path')
.attr('class', 'area')
.attr('d', navArea(data));
// ...and line path
navG.append('path')
.attr('class', 'line')
.attr('d', navLine(data));
} // end refreshChart function
// function to keep the chart "moving" through time (right to left)
window.intvl = setInterval(function() {
// get current viewport extent
// var extent = viewport.empty() ? xNav.domain() : viewport.extent();
// var extent = (sel === null) ? xNav.domain() : viewport.extent()();
var extent = xNav.domain();
window.extent = extent;
var interval = extent[1] - extent[0];
var offset = extent[0] - xNav.domain()[0];
// var interval = extent[1].getTime() - extent[0].getTime();
// var offset = extent[0].getTime() - xNav.domain()[0].getTime();
// compute new nav extents
endTime = new Date();
startTime = new Date(endTime.getTime() - maxSeconds * 1000);
// compute new viewport extents
startTimeViewport = new Date(startTime.getTime() + offset);
endTimeViewport = new Date(startTimeViewport.getTime() + interval);
viewport.extent([startTimeViewport, endTimeViewport])
// update scales
x.domain([startTimeViewport, endTimeViewport]);
if(sel) {
x.domain(sel.map(xNav.invert, xNav));
}
xNav.domain([startTime, endTime]);
// update axis
xAxis.scale(x)(xAxisG);
xAxisNav.scale(xNav)(xAxisGNav);
// refresh svg
refresh();
}, 200)
// end setInterval function
return chart;
} // end chart function
// chart getter/setters
// array of inital data
chart.initialData = function(_) {
if (arguments.length == 0) return initialData;
initialData = _;
return chart;
}
// new data item (this most recent item will appear
// on the right side of the chart, and begin moving left)
chart.datum = function(_) {
if (arguments.length == 0) return datum;
datum = _;
data.push(datum);
return chart;
}
// svg width
chart.width = function(_) {
if (arguments.length == 0) return svgWidth;
svgWidth = _;
return chart;
}
// svg height
chart.height = function(_) {
if (arguments.length == 0) return svgHeight;
svgHeight = _;
return chart;
}
// svg border
chart.border = function(_) {
if (arguments.length == 0) return border;
border = _;
return chart;
}
// chart title
chart.title = function(_) {
if (arguments.length == 0) return chartTitle;
chartTitle = _;
return chart;
}
// x axis title
chart.xTitle = function(_) {
if (arguments.length == 0) return xTitle;
xTitle = _;
return chart;
}
// y axis title
chart.yTitle = function(_) {
if (arguments.length == 0) return yTitle;
yTitle = _;
return chart;
}
// bar width
chart.barWidth = function(_) {
if (arguments.length == 0) return barWidth;
barWidth = _;
return chart;
}
// version
chart.version = version;
return chart;
} // end realTimeChart function
'use strict';
// mean and deviation for time interval
var meanMs = 1000, // milliseconds
dev = 150;
// define time scale
var timeScale = d3.scaleLinear()
.domain([300, 1700])
.range([300, 1700])
.clamp(true);
// define value scale
var valueScale = d3.scaleLinear()
.domain([0, 1])
.range([30, 95]);
// generate initial data
var normal = d3.randomNormal(1000, 150);
var currMs = new Date().getTime() - 300000 - 4000;
var data = d3.range(300).map(function(d, i, arr) {
var value = valueScale(Math.random()); // random data
//var value = Math.round((d % 60) / 60 * 95); // ramp data
var interval = Math.round(timeScale(normal()));
currMs += interval;
var time = new Date(currMs);
var obj = { interval: interval, value: value, time: time, ts: currMs }
return obj;
})
// create the real time chart
var chart = realTimeChart()
.title("Chart Title")
.yTitle("Y Scale")
.xTitle("X Scale")
.border(true)
.width(600)
.height(290)
.barWidth(1)
.initialData(data);
console.log("Version: ", chart.version);
console.dir("Dir++");
console.trace();
console.warn("warn")
// invoke the chart
var chartDiv = d3.select("#viewDiv").append("div")
.attr("id", "chartDiv")
.call(chart);
// alternative invocation
//chart(chartDiv);
// drive data into the chart roughly every second
// in a normal use case, real time data would arrive through the network or some other mechanism
var d = 0;
function dataGenerator() {
var timeout = Math.round(timeScale(normal()));
setTimeout(function() {
// create new data item
var now = new Date();
var obj = {
value: valueScale(Math.random()), // random data
//value: Math.round((d++ % 60) / 60 * 95), // ramp data
time: now,
color: "red",
ts: now.getTime(),
interval: timeout
};
// send the datum to the chart
chart.datum(obj);
// do forever
dataGenerator();
}, timeout);
}
// start the data generator
dataGenerator();

Related

How to zoom my stream-graph with using D3V6?

Update:
The zooming stream graph question is resolved. Thank you, Andrew! But the zooming of the streamgraph doesn't align with the zooming of the X and Y axis.
Thisis the picture showing how it looks like now2
the original post is here:
I am new to StackOverflow and the javascript community. I am trying to zoom a streamgraph I did use javascript and D3 and I followed this tutorial: https://www.d3-graph-gallery.com/graph/interactivity_zoom.html#axisZoom.
My code can be viewed here: https://github.com/Feisnowflakes/zoomtest222/tree/main/streamgraph-test
However, currently, I can zoom X aXis and Y aXis, but not my stream graph is zoomable. I didn't see any errors in my console, so I am stuck now. Can anyone like to look at my codes and help me figure out why my streamgraph cannot be zoomed?
Thank you so much!
(function () {
// first, load the dataset from a CSV file
d3.csv("https://raw.githubusercontent.com/Feisnowflakes/zoomtest222/main/streamgraph-test/Los_Angeles_International_Airport_-_Passenger_Traffic_By_Terminal.csv")
.then(data => {
// log csv in browser console
console.log(data);
var advanceVisData = {};
var airport = new Set();
data.forEach(d => {
airport.add(d['Terminal']);
var period = new Date(d['ReportPeriod']);
if (period in advanceVisData) {
if (d['Terminal'] in advanceVisData[period]) {
advanceVisData[period][d['Terminal']] += Number(d['Passenger_Count']);
}
else {
advanceVisData[period][d['Terminal']] = Number(d['Passenger_Count']);
}
}
else {
advanceVisData[period] = {};
advanceVisData[period][d['Terminal']] = Number(d['Passenger_Count']);
}
});
console.log(airport);
console.log(advanceVisData);
// reformat the advanceVisData for d3.stack()
var formattedData = [];
Object.keys(advanceVisData).forEach(d => {
var item = {};
item['year'] = d;
airport.forEach(terminal => {
if (terminal in advanceVisData[d]) {
item[terminal] = advanceVisData[d][terminal];
} else {
item[terminal] = 0;
}
});
formattedData.push(item);
});
console.log(formattedData);
/*********************************
* Visualization codes start here
* ********************************/
var width = 1200;
var height = 400;
var margin = { left: 60, right: 20, top: 20, bottom: 60 };
//set the dimensions and margins of the graph
// append the svg object to the body of the page
var svg = d3.select('#container')
.append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
// List of groups = header of the csv files
var keys = Array.from(airport);
//stack the data?
var stackedData = d3.stack()
//.offset(d3.stackOffsetSilhouette)
.keys(keys)
(formattedData);
console.log(stackedData);
var max_val = 0;
var min_val = 0;
stackedData.forEach(terminal => {
terminal.forEach(year => {
if (year[0] < min_val) min_val = year[0];
if (year[1] < min_val) min_val = year[1];
if (year[0] > max_val) max_val = year[0];
if (year[1] > max_val) max_val = year[1];
})
});
//console.log(max_val, min_val);
// Add X axis
var x = d3.scaleTime()
.domain(d3.extent(formattedData, function (d) {
return new Date(d.year);
}))
.range([0, width]);
var xAxis = svg.append("g")
.attr("transform", "translate(0," + height + ")")
.call(d3.axisBottom(x).ticks(20));
// Add Y axis
var y = d3.scaleLinear()
.domain([min_val, max_val])
.range([height, 0]);
var yAxis = svg.append("g")
.call(d3.axisLeft(y));
// color palette
var color = d3.scaleOrdinal()
.domain(keys)
.range(['#e41a1c', '#377eb8', '#4daf4a', '#984ea3', '#ff7f00', '#f781bf', "#87sbf", "#ff981bf","#d6a3b6", '#b3afb0', '#ddd8c2']);
// create a tooltip
var Tooltip = svg
.append("text")
.attr("x", 0)
.attr("y", 0)
.style("opacity", 0)
.style("font-size", 17)
// Show the areas
var stream = svg.append("g")
stream
.selectAll(".myStreamArea")
.data(stackedData)
.enter()
.append("path")
.attr("class", "myStreamArea")
.style("fill", function (d) {
return color(d.key);
})
.style("opacity", 1)
.attr("d", d3.area()
.x(function (d) {
return x(new Date(d.data.year));
})
.y0(function (d) {
return y(d[0]);
})
.y1(function (d) {
return y(d[1]);
})
);
// Set the zoom and Pan features: how much you can zoom, on which part, and what to do when there is a zoom
var zoom = d3.zoom()
.scaleExtent([.5, 20]) // This control how much you can unzoom (x0.5) and zoom (x20)
.extent([[0, 0], [width, height]])
.on("zoom", updateChart);
// This add an invisible rect on top of the chart area. This rect can recover pointer events: necessary to understand when the user zoom
svg.append("rect")
.attr("width", width)
.attr("height", height)
.style("fill", "none")
.style("pointer-events", "all")
.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')')
.call(zoom);
// now the user can zoom and it will trigger the function called updateChart
// A function that updates the chart when the user zoom and thus new boundaries are available
function updateChart() {
// recover the new scale
var transform = d3.zoomTransform(this);
var newX = transform.rescaleX(x);
var newY = transform.rescaleY(y);
// var newX = d3.event.transform.rescaleX(x);
// var newY = d3.event.transform.rescaleY(y);
// update axes with these new boundaries
xAxis.call(d3.axisBottom(newX))
yAxis.call(d3.axisLeft(newY))
stream
.selectAll(".myStreamArea")
.attr("d", d3.area()
.x(function (d) {
return newX(new Date(d.data.year));
})
.y0(function (d) {
return newY(d[0]);
})
.y1(function (d) {
return newY(d[1]);
}));
}
})
})();
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Zoomable streamgraph</title>
<script src="https://d3js.org/d3.v6.min.js"></script>
<style>
#tooltip {
min-width: 100px;
min-height: 50px;
background-color: white;
}
</style>
</head>
<body>
<div id="container"> <div id="tooltip"></div></div>
<script src="main.js"></script>
</body>
</html>

exit().remove() doesn't remove node when it goes out of view

I'm using d3js to move circular dots from right to left with respect to current time. I have a couple of issues:
1. .exit().remove() don't work. Node doesn't get removed once it goes out of the view.
2. Transition of circles are a bit jumpy
var circles = g.selectAll('path')
circles.data(data)
.enter()
.append('path')
.attr("d", symbol.type(d3.symbolCircle))
.merge(circles)
.attr("transform", (d) => "translate(" + x(d.x) + "," + y(d.y) + ")");
circles.exit().remove();
You can see my full code here: http://jsfiddle.net/hsdhott/3tdhuLgm/
Besides your enter-exit-update pattern not being correct (please check the snippet bellow), the big problem here is the data:
selection.exit() method won't magically select — normally for using remove() later — an element based on any arbitrary criterion, such as "getting out of the view". It's based on the data only. And the problem is that your data never stops increasing:
if (count % 10 === 0) {
var point = {
x: globalX,
y: ((Math.random() * 200 + 50) >> 0)
};
data.push(point);
}
So, a very quick solution in this case is removing the data points based on your x domain:
data = data.filter(function(d) {
return d.x > globalX - 10000;
});
This is just a suggestion, use the logic you want for removing the objects from the data array. However, regardless the logic you use, you have to remove them.
Regarding the jumpy transition, the problem is that you're using selection.transition and setInterval. That won't work, chose one of them.
Here is your updated code:
var data = [];
var width = 500;
var height = 350;
var globalX = new Date().getTime();
/* var globalX = 0; */
var duration = 250;
var step = 10;
var count = 0;
var chart = d3.select('#chart')
.attr('width', width + 50)
.attr('height', height + 50);
var x = d3.scaleTime()
.domain([globalX, (globalX - 10000)])
.range([0, width]);
var y = d3.scaleLinear()
.domain([0, 300])
.range([300, 0]);
// -----------------------------------
// Draw the axis
var xAxis = d3.axisBottom()
.scale(x)
.ticks(10)
.tickFormat(formatter);
var axisX = chart.append('g')
.attr('class', 'x axis')
.attr('transform', 'translate(0, 300)')
.call(xAxis);
// Append the holder for line chart and circles
var g = chart.append('g');
function formatter(time) {
if ((time.getSeconds() % 5) != 0) {
return "";
}
return d3.timeFormat('%H:%M:%S')(time);
}
function createData() {
// Generate new data
var point = {
x: globalX,
y: ((Math.random() * 200 + 50) >> 0)
};
data.push(point);
}
function callInterval() {
count++;
if (count % 3 === 0) createData();
}
// Main loop
function tick() {
// Generate new data
if (count % 10 === 0) {
var point = {
x: globalX,
y: ((Math.random() * 200 + 50) >> 0)
};
data.push(point);
}
data = data.filter(function(d) {
return d.x > globalX - 10000;
});
count++;
globalX = new Date().getTime();
var timer = new Date().getTime();
var symbol = d3.symbol().size([100]),
color = d3.schemeCategory10;
var circles = g.selectAll('path')
.data(data);
circles = circles.enter()
.append('path')
.attr("d", symbol.type(d3.symbolCircle))
.merge(circles)
.attr("transform", (d) => "translate(" + x(d.x) + "," + y(d.y) + ")");
circles.exit().remove();
// Shift the chart left
x.domain([timer - 10000, timer]);
axisX.call(xAxis);
g.attr('transform', null)
.attr('transform', 'translate(' + x(globalX - 10000) + ')');
// Remote old data (max 50 points)
if (data.length && (data[data.length - 1].x < (globalX - 10000))) data.shift();
}
tick();
setInterval(tick, 10);
.axis {
font-family: sans-serif;
font-size: 12px;
}
.line {
fill: none;
stroke: #f1c40f;
stroke-width: 3px;
}
.circle {
stroke: #e74c3c;
stroke-width: 3px;
fill: #FFF;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.13.0/d3.min.js"></script>
<svg id="chart"></svg>

How to add legend to sequence sunburst chart of d3js for my own data?

Legend Color not matching Screenshot I am Working on the d3js sequence sunburst chart.
My Sample Data is:
Shift-Mode-Machineid,MachineDuration
DayShift-auto-1091,1
DayShift-auto-1100,5
DayShift-auto-1100,117
DayShift-manual-1111,4
DayShift-manual-1112,6
DayShift-manual-1120,43
NightShift-auto-1150,9
NightShift-auto-1150,85
NightShift-auto-1150,6
NightShift-manual-1120,16
NightShift-manual-1120,1
NightShift-manual-1120,4
Please suggest how to match the legend color and the chart color. The text inside the legend is perfect but the color is a mismatch. I have to put correct colors. Please help.
// Dimensions of sunburst.[Screenshot of wrong color in legend][1]
var width = 750;
var height = 600;
var radius = Math.min(width, height) / 2;
// Breadcrumb dimensions: width, height, spacing, width of tip/tail.
var b = {
w: 75, h: 30, s: 3, t: 10
};
// Mapping of step names to colors.
var colors = {};
var color = d3.scale.category20b();
/*{
"home": "#5687d1",
"product": "#7b615c",
"search": "#de783b",
"account": "#6ab975",
"other": "#a173d1",
"end": "#bbbbbb"
}*/;
// Total size of all segments; we set this later, after loading the data.
var totalSize = 0;
var vis = d3.select("#chart").append("svg:svg")
.attr("width", width)
.attr("height", height)
.append("svg:g")
.attr("id", "container")
.attr("transform", "translate(" + width / 2 + "," + height / 2 + ")");
var partition = d3.layout.partition()
.size([2 * Math.PI, radius * radius])
.value(function(d) { return d.size; });
var arc = d3.svg.arc()
.startAngle(function(d) { return d.x; })
.endAngle(function(d) { return d.x + d.dx; })
.innerRadius(function(d) { return Math.sqrt(d.y); })
.outerRadius(function(d) { return Math.sqrt(d.y + d.dy); });
// Use d3.text and d3.csv.parseRows so that we do not need to have a header
// row, and can receive the csv as an array of arrays.
d3.text("Data.csv", function(text) {
var csv = d3.csv.parseRows(text);
var json = buildHierarchy(csv);
/*After getting proper json, we are making colors variable to hold our data keys
*/
json.children.forEach(function(root, ind){
colors[root.name]=color(ind);
});
createVisualization(json);
});
// Main function to draw and set up the visualization, once we have the data.
function createVisualization(json) {
// Basic setup of page elements.
initializeBreadcrumbTrail();
drawLegend();
d3.select("#togglelegend").on("click", toggleLegend);
// Bounding circle underneath the sunburst, to make it easier to detect
// when the mouse leaves the parent g.
vis.append("svg:circle")
.attr("r", radius)
.style("opacity", 0);
// For efficiency, filter nodes to keep only those large enough to see.
var nodes = partition.nodes(json)
.filter(function(d) {
return (d.dx > 0.005); // 0.005 radians = 0.29 degrees
});
var path = vis.data([json]).selectAll("path")
.data(nodes)
.enter().append("svg:path")
.attr("display", function(d) { return d.depth ? null : "none"; })
.attr("d", arc)
.attr("fill-rule", "evenodd")
//.style("fill", function(d) { return colors[d.name]; })
.style("fill", function(d) { return color((d.parent ? d : d.children).name); })
.style("opacity", 1)
.on("mouseover", mouseover);
// Add the mouseleave handler to the bounding circle.
d3.select("#container").on("mouseleave", mouseleave);
// Get total size of the tree = value of root node from partition.
totalSize = path.node().__data__.value;
};
// Fade all but the current sequence, and show it in the breadcrumb trail.
function mouseover(d) {
var percentage = (100 * d.value / totalSize).toPrecision(3);
var percentageString = percentage + "%";
if (percentage < 0.1) {
percentageString = "< 0.1%";
}
d3.select("#percentage")
.text(percentageString);
d3.select("#explanation")
.style("visibility", "");
var sequenceArray = getAncestors(d);
updateBreadcrumbs(sequenceArray, percentageString);
// Fade all the segments.
d3.selectAll("path")
.style("opacity", 0.3);
// Then highlight only those that are an ancestor of the current segment.
vis.selectAll("path")
.filter(function(node) {
return (sequenceArray.indexOf(node) >= 0);
})
.style("opacity", 1);
}
// Restore everything to full opacity when moving off the visualization.
function mouseleave(d) {
// Hide the breadcrumb trail
d3.select("#trail")
.style("visibility", "hidden");
// Deactivate all segments during transition.
d3.selectAll("path").on("mouseover", null);
// Transition each segment to full opacity and then reactivate it.
d3.selectAll("path")
.transition()
.duration(1000)
.style("opacity", 1)
.each("end", function() {
d3.select(this).on("mouseover", mouseover);
});
d3.select("#explanation")
.style("visibility", "hidden");
}
// Given a node in a partition layout, return an array of all of its ancestor
// nodes, highest first, but excluding the root.
function getAncestors(node) {
var path = [];
var current = node;
while (current.parent) {
path.unshift(current);
current = current.parent;
}
return path;
}
function initializeBreadcrumbTrail() {
// Add the svg area.
var trail = d3.select("#sequence").append("svg:svg")
.attr("width", width)
.attr("height", 50)
.attr("id", "trail");
// Add the label at the end, for the percentage.
trail.append("svg:text")
.attr("id", "endlabel")
.style("fill", "#000");
}
// Generate a string that describes the points of a breadcrumb polygon.
function breadcrumbPoints(d, i) {
var points = [];
points.push("0,0");
points.push(b.w + ",0");
points.push(b.w + b.t + "," + (b.h / 2));
points.push(b.w + "," + b.h);
points.push("0," + b.h);
if (i > 0) { // Leftmost breadcrumb; don't include 6th vertex.
points.push(b.t + "," + (b.h / 2));
}
return points.join(" ");
}
// Update the breadcrumb trail to show the current sequence and percentage.
function updateBreadcrumbs(nodeArray, percentageString) {
// Data join; key function combines name and depth (= position in sequence).
var g = d3.select("#trail")
.selectAll("g")
.data(nodeArray, function(d) { return d.name + d.depth; });
// Add breadcrumb and label for entering nodes.
var entering = g.enter().append("svg:g");
entering.append("svg:polygon")
.attr("points", breadcrumbPoints)
// .style("fill", function(d) { return colors[d.name]; });
.style("fill", function(d) { return color((d.parent ? d : d.children).name); });
entering.append("svg:text")
.attr("x", (b.w + b.t) / 2)
.attr("y", b.h / 2)
.attr("dy", "0.35em")
.attr("text-anchor", "middle")
.text(function(d) { return d.name; });
// Set position for entering and updating nodes.
g.attr("transform", function(d, i) {
return "translate(" + i * (b.w + b.s) + ", 0)";
});
// Remove exiting nodes.
g.exit().remove();
// Now move and update the percentage at the end.
d3.select("#trail").select("#endlabel")
.attr("x", (nodeArray.length + 0.5) * (b.w + b.s))
.attr("y", b.h / 2)
.attr("dy", "0.35em")
.attr("text-anchor", "middle")
.text(percentageString);
// Make the breadcrumb trail visible, if it's hidden.
d3.select("#trail")
.style("visibility", "");
}
function drawLegend() {
// Dimensions of legend item: width, height, spacing, radius of rounded rect.
var li = {
w: 75, h: 30, s: 3, r: 3
};
var legend = d3.select("#legend").append("svg:svg")
.attr("width", li.w)
.attr("height", d3.keys(colors).length * (li.h + li.s));
var g = legend.selectAll("g")
.data(d3.entries(colors))
.enter().append("svg:g")
.attr("transform", function(d, i) {
return "translate(0," + i * (li.h + li.s) + ")";
});
g.append("svg:rect")
.attr("rx", li.r)
.attr("ry", li.r)
.attr("width", li.w)
.attr("height", li.h)
.style("fill", function(d) { return d.value; });
g.append("svg:text")
.attr("x", li.w / 2)
.attr("y", li.h / 2)
.attr("dy", "0.35em")
.attr("text-anchor", "middle")
.text(function(d) { return d.key; });
}
function toggleLegend() {
var legend = d3.select("#legend");
if (legend.style("visibility") == "hidden") {
legend.style("visibility", "");
} else {
legend.style("visibility", "hidden");
}
}
// Take a 2-column CSV and transform it into a hierarchical structure suitable
// for a partition layout. The first column is a sequence of step names, from
// root to leaf, separated by hyphens. The second column is a count of how
// often that sequence occurred.
function buildHierarchy(csv) {
var root = {"name": "root", "children": []};
for (var i = 0; i < csv.length; i++) {
var sequence = csv[i][0];
var size = +csv[i][1];
if (isNaN(size)) { // e.g. if this is a header row
continue;
}
var parts = sequence.split("-");
var currentNode = root;
for (var j = 0; j < parts.length; j++) {
var children = currentNode["children"];
var nodeName = parts[j];
var childNode;
if (j + 1 < parts.length) {
// Not yet at the end of the sequence; move down the tree.
var foundChild = false;
for (var k = 0; k < children.length; k++) {
if (children[k]["name"] == nodeName) {
childNode = children[k];
foundChild = true;
break;
}
}
// If we don't already have a child node for this branch, create it.
if (!foundChild) {
childNode = {"name": nodeName, "children": []};
children.push(childNode);
}
currentNode = childNode;
} else {
// Reached the end of the sequence; create a leaf node.
childNode = {"name": nodeName, "size": size};
children.push(childNode);
}
}
}
return root;
};
After doing some analysis, I could suggest you below code, try it once.
// Dimensions of sunburst.
var width = 750;
var height = 600;
var radius = Math.min(width, height) / 2;
// Breadcrumb dimensions: width, height, spacing, width of tip/tail.
var b = {
w: 75, h: 30, s: 3, t: 10
};
// Mapping of step names to colors.
var colors = {};/*{
"home": "#5687d1",
"product": "#7b615c",
"search": "#de783b",
"account": "#6ab975",
"other": "#a173d1",
"end": "#bbbbbb"
}*/;
// Total size of all segments; we set this later, after loading the data.
var totalSize = 0;
var vis = d3.select("#chart").append("svg:svg")
.attr("width", width)
.attr("height", height)
.append("svg:g")
.attr("id", "container")
.attr("transform", "translate(" + width / 2 + "," + height / 2 + ")");
var partition = d3.layout.partition()
.size([2 * Math.PI, radius * radius])
.value(function(d) { return d.size; });
var arc = d3.svg.arc()
.startAngle(function(d) { return d.x; })
.endAngle(function(d) { return d.x + d.dx; })
.innerRadius(function(d) { return Math.sqrt(d.y); })
.outerRadius(function(d) { return Math.sqrt(d.y + d.dy); });
// Use d3.text and d3.csv.parseRows so that we do not need to have a header
// row, and can receive the csv as an array of arrays.
d3.text("visit-sequences.csv", function(text) {
var csv = d3.csv.parseRows(text);
var json = buildHierarchy(csv);
/*After getting proper json, we are making colors variable to hold our data keys
*/
json.children.forEach(function(root, ind){
colors[root.name]=color(root.name); //Changed ind to root.name
});
createVisualization(json);
});
// Main function to draw and set up the visualization, once we have the data.
function createVisualization(json) {
// Basic setup of page elements.
initializeBreadcrumbTrail();
drawLegend();
d3.select("#togglelegend").on("click", toggleLegend);
// Bounding circle underneath the sunburst, to make it easier to detect
// when the mouse leaves the parent g.
vis.append("svg:circle")
.attr("r", radius)
.style("opacity", 0);
// For efficiency, filter nodes to keep only those large enough to see.
var nodes = partition.nodes(json)
.filter(function(d) {
return (d.dx > 0.005); // 0.005 radians = 0.29 degrees
});
var path = vis.data([json]).selectAll("path")
.data(nodes)
.enter().append("svg:path")
.attr("display", function(d) { return d.depth ? null : "none"; })
.attr("d", arc)
.attr("fill-rule", "evenodd")
.style("fill", function(d) { return colors[d.name]; })
.style("opacity", 1)
.on("mouseover", mouseover);
// Add the mouseleave handler to the bounding circle.
d3.select("#container").on("mouseleave", mouseleave);
// Get total size of the tree = value of root node from partition.
totalSize = path.node().__data__.value;
};
// Fade all but the current sequence, and show it in the breadcrumb trail.
function mouseover(d) {
var percentage = (100 * d.value / totalSize).toPrecision(3);
var percentageString = percentage + "%";
if (percentage < 0.1) {
percentageString = "< 0.1%";
}
d3.select("#percentage")
.text(percentageString);
d3.select("#explanation")
.style("visibility", "");
var sequenceArray = getAncestors(d);
updateBreadcrumbs(sequenceArray, percentageString);
// Fade all the segments.
d3.selectAll("path")
.style("opacity", 0.3);
// Then highlight only those that are an ancestor of the current segment.
vis.selectAll("path")
.filter(function(node) {
return (sequenceArray.indexOf(node) >= 0);
})
.style("opacity", 1);
}
// Restore everything to full opacity when moving off the visualization.
function mouseleave(d) {
// Hide the breadcrumb trail
d3.select("#trail")
.style("visibility", "hidden");
// Deactivate all segments during transition.
d3.selectAll("path").on("mouseover", null);
// Transition each segment to full opacity and then reactivate it.
d3.selectAll("path")
.transition()
.duration(1000)
.style("opacity", 1)
.each("end", function() {
d3.select(this).on("mouseover", mouseover);
});
d3.select("#explanation")
.style("visibility", "hidden");
}
// Given a node in a partition layout, return an array of all of its ancestor
// nodes, highest first, but excluding the root.
function getAncestors(node) {
var path = [];
var current = node;
while (current.parent) {
path.unshift(current);
current = current.parent;
}
return path;
}
function initializeBreadcrumbTrail() {
// Add the svg area.
var trail = d3.select("#sequence").append("svg:svg")
.attr("width", width)
.attr("height", 50)
.attr("id", "trail");
// Add the label at the end, for the percentage.
trail.append("svg:text")
.attr("id", "endlabel")
.style("fill", "#000");
}
// Generate a string that describes the points of a breadcrumb polygon.
function breadcrumbPoints(d, i) {
var points = [];
points.push("0,0");
points.push(b.w + ",0");
points.push(b.w + b.t + "," + (b.h / 2));
points.push(b.w + "," + b.h);
points.push("0," + b.h);
if (i > 0) { // Leftmost breadcrumb; don't include 6th vertex.
points.push(b.t + "," + (b.h / 2));
}
return points.join(" ");
}
// Update the breadcrumb trail to show the current sequence and percentage.
function updateBreadcrumbs(nodeArray, percentageString) {
// Data join; key function combines name and depth (= position in sequence).
var g = d3.select("#trail")
.selectAll("g")
.data(nodeArray, function(d) { return d.name + d.depth; });
// Add breadcrumb and label for entering nodes.
var entering = g.enter().append("svg:g");
entering.append("svg:polygon")
.attr("points", breadcrumbPoints)
.style("fill", function(d) { return colors[d.name]; });
entering.append("svg:text")
.attr("x", (b.w + b.t) / 2)
.attr("y", b.h / 2)
.attr("dy", "0.35em")
.attr("text-anchor", "middle")
.text(function(d) { return d.name; });
// Set position for entering and updating nodes.
g.attr("transform", function(d, i) {
return "translate(" + i * (b.w + b.s) + ", 0)";
});
// Remove exiting nodes.
g.exit().remove();
// Now move and update the percentage at the end.
d3.select("#trail").select("#endlabel")
.attr("x", (nodeArray.length + 0.5) * (b.w + b.s))
.attr("y", b.h / 2)
.attr("dy", "0.35em")
.attr("text-anchor", "middle")
.text(percentageString);
// Make the breadcrumb trail visible, if it's hidden.
d3.select("#trail")
.style("visibility", "");
}
function drawLegend() {
// Dimensions of legend item: width, height, spacing, radius of rounded rect.
var li = {
w: 75, h: 30, s: 3, r: 3
};
var legend = d3.select("#legend").append("svg:svg")
.attr("width", li.w)
.attr("height", d3.keys(colors).length * (li.h + li.s));
var g = legend.selectAll("g")
.data(d3.entries(colors))
.enter().append("svg:g")
.attr("transform", function(d, i) {
return "translate(0," + i * (li.h + li.s) + ")";
});
g.append("svg:rect")
.attr("rx", li.r)
.attr("ry", li.r)
.attr("width", li.w)
.attr("height", li.h)
.style("fill", function(d) { return d.value; });
g.append("svg:text")
.attr("x", li.w / 2)
.attr("y", li.h / 2)
.attr("dy", "0.35em")
.attr("text-anchor", "middle")
.text(function(d) { return d.key; });
}
function toggleLegend() {
var legend = d3.select("#legend");
if (legend.style("visibility") == "hidden") {
legend.style("visibility", "");
} else {
legend.style("visibility", "hidden");
}
}
// Take a 2-column CSV and transform it into a hierarchical structure suitable
// for a partition layout. The first column is a sequence of step names, from
// root to leaf, separated by hyphens. The second column is a count of how
// often that sequence occurred.
function buildHierarchy(csv) {
var root = {"name": "root", "children": []};
for (var i = 0; i < csv.length; i++) {
var sequence = csv[i][0];
var size = +csv[i][1];
if (isNaN(size)) { // e.g. if this is a header row
continue;
}
var parts = sequence.split("-");
var currentNode = root;
for (var j = 0; j < parts.length; j++) {
var children = currentNode["children"];
var nodeName = parts[j];
var childNode;
if (j + 1 < parts.length) {
// Not yet at the end of the sequence; move down the tree.
var foundChild = false;
for (var k = 0; k < children.length; k++) {
if (children[k]["name"] == nodeName) {
childNode = children[k];
foundChild = true;
break;
}
}
// If we don't already have a child node for this branch, create it.
if (!foundChild) {
childNode = {"name": nodeName, "children": []};
children.push(childNode);
}
currentNode = childNode;
} else {
// Reached the end of the sequence; create a leaf node.
childNode = {"name": nodeName, "size": size};
children.push(childNode);
}
}
}
return root;
};
Try this code n tell me.
We need to change
json.children.forEach(function(root, ind){
colors[root.name]=color(ind); //Changed ind to root.name
});
so that
json.children.forEach(function(root, ind){
colors[root.name]=color(root.name);
});
Hope this is causing colors not to match.
Try n tell me now.

D3 seemingly updating wrong SVG element

I have a d3 layout that is intended to produce 3 charts:
Custom item layout w/ transitions
Pie chart for sales by category (with transitions at some point)
Bar chart for top 5 performing items w/ transitions.
1 & 2 work OK but when I add the third chart, I see some strange behavior. The intention is to create a bar chart where the width of each bar is tied to the sales metric for the top N items determined like so:
data = data.filter(function(d){ return d.date === someDate});
var cf = crossfilter(data);
var salesDimension = cf.dimension(function(d){ return d.sales; });
topData = salesDimension.top(5);
The problem is that instead of drawing the bar chart, my code somehow overwrites the position of 5 items in chart 1 and moves them back to the origin. If I change 5 to 10 above then 10 items are overwritten and so on.
I double checked the scope of my variables and even tried changing the names of everything in my drawTopItems() which made no difference. I suspect that I am doing something incorrectly when it comes to selecting the svg element or applying classes to the svg group elements that I want to modify but I can't for the life of me see what. Can anyone tell me what I might be doing wrong?
Here is my issue in a fiddle: https://jsfiddle.net/Sledge/4eggpd5e/12/.
Here is my javascript code:
var item_width = 40, item_height = 60;
var margin = {top: 50, right: 50, bottom: 75, left: 40},
width = 700 - margin.left - margin.right,
height = 500 - margin.top - margin.bottom;
var x = d3.scaleLinear().range([0, width]);
var y = d3.scaleLinear().range([0, height]);
var colorScale = d3.scaleLinear().domain([500,3000]).range(["white","#4169e1"]);
// Pie Chart parameters
var pieWidth = 300, pieHeight = 300;
var outerRadius = Math.min(pieWidth, pieHeight) / 2,
innerRadius = outerRadius * .50;
var pieColor = d3.scaleOrdinal(['#42b9f4','#3791f2','#374ff1','#25b22e','#107222']); // custom color scale
var legendRectSize = 18; // NEW
var legendSpacing = 4; // NEW
// Top Item Parameters
var topItemMargin = {top:25, right:25, bottom: 25, left: 25};
var topItemWidth = 300 - topItemMargin.left - topItemMargin.right,
topItemHeight = 300 - topItemMargin.top - topItemMargin.bottom;
var topItemXScale = d3.scaleLinear().range([0, topItemWidth]);
var barHeight = 20, barSpacing = 5;
/* SVG */
var svgItemLayout = d3.select("#item_layout")
.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 svgPieChart = d3.select("#pie_chart")
.append("svg")
.attr("width", pieWidth)
.attr("height", pieHeight)
.append("g")
.attr("transform", "translate(" + outerRadius + "," + outerRadius + ")") ;
var svgTopItems = d3.select("#top_items")
.append("svg")
.attr("width", topItemWidth)
.attr("height", topItemHeight)
.append("g")
.attr("transform", "translate(" + topItemMargin.left + "," + topItemMargin.top + ")");
/* DRAW FUNCTIONS */
// a single function to draw
function drawItemLayout(data, someDate){
data.forEach(function(d) {
d.x_pos = +d.x_pos;
d.y_pos = +d.y_pos;
d.sales = +d.sales;
});
// pre-filter data
data = data.filter(function(d){ return d.date === someDate});
var x_offset = 5, y_offset = 5;
x.domain(d3.extent(data, function(d) { return d.x_pos; })); // set the x domain
y.domain(d3.extent(data, function(d) { return d.y_pos; })); // set the y domain
// create an update selection with a key function
var g_sel = svgItemLayout.selectAll("g")
.data(data, function(d){
return d.item_name;
});
// get rid of those leaving the update
g_sel.exit().remove();
// our entering g
var g_ent = g_sel.enter()
.append("g");
// add our rects to our g
g_ent.append("rect")
.attr("class", "dot") // wonder if I really need this class?
.attr("width", item_width)
.attr("height", item_height)
.attr("rx", 3)
.attr("ry", 3)
.style("fill", function(d){ return colorScale(d.sales); }) // color factor variable
.style("fill-opacity", 0.5);
// add our text to our g
g_ent.append("text")
.attr("font-size", 10)
.attr("text-anchor", "middle")
.attr("fill", "black")
.attr("dx", item_width/2)
.attr("dy", item_height/2)
.text(function(d){ return d.item_name; });
// UPDATE + ENTER selection
g_sel = g_ent.merge(g_sel);
// move them into position with transition
g_sel
.transition()
.duration(1200)
.delay(function(d, i) { return i *40; })
.attr("transform", function(d){
return "translate(" + (x(d.x_pos) + x_offset) + "," + (y(d.y_pos) + y_offset) + ")";
});
}
function drawPieChart(data, someDate) {
data.forEach(function(d) {
d.x_pos = +d.x_pos;
d.y_pos = +d.y_pos;
d.sales = +d.sales;
});
// pre-filter data
data = data.filter(function(d){ return d.date === someDate});
var cf = crossfilter(data);
var salesDimension = cf.dimension(function(d){ return d.sales; });
var categoryDimension = cf.dimension(function(d){ return d.category; });
var categoryGroup = categoryDimension.group();
function reduceInitial(p, v) {
return {
sales : 0,
count : 0
};
}
function reduceAdd(p, v) {
p.sales = p.sales + v.sales;
p.count = p.count + 1;
return p;
}
function reduceRemove(p, v) {
p.sales = p.sales - v.sales;
p.count = p.count - 1;
return p;
}
categoryAggregated = categoryGroup.reduce(reduceAdd, reduceRemove, reduceInitial).all();
var arc = d3.arc()
.innerRadius(innerRadius)
.outerRadius(outerRadius);
var pie = d3.pie()
.value(function(d) { return d.value.sales; })
.sort(null);
var path = svgPieChart.selectAll('path')
.data(pie(categoryAggregated))
.enter()
.append('path')
.attr('d', arc)
.attr('fill', function(d, i) { return pieColor(i);});
// Add a legend:
var legend = svgPieChart.selectAll('.legend')
.data(pieColor.domain())
.enter()
.append('g')
.attr('class', 'legend')
.attr('transform', function(d, i) {
var height = legendRectSize + legendSpacing;
var offset = height * pieColor.domain().length / 2;
var horz = -3 * legendRectSize;
var vert = i * height - offset;
return 'translate(' + horz + ',' + vert + ')';
});
legend.append('rect')
.attr('width', legendRectSize)
.attr('height', legendRectSize)
.style('fill', pieColor)
.style('stroke', pieColor);
legend.append('text')
.attr('x', legendRectSize + legendSpacing)
.attr('y', legendRectSize - legendSpacing)
.text(function(d) { return categoryAggregated[d].key; }); // returns text based on data index
}
function drawTopItems (data, someDate) {
data.forEach(function(d) {
d.x_pos = +d.x_pos;
d.y_pos = +d.y_pos;
d.sales = +d.sales;
});
// pre-filter data
data = data.filter(function(d){ return d.date === someDate});
var cf = crossfilter(data);
var salesDimension = cf.dimension(function(d){ return d.sales; });
topData = salesDimension.top(5);
topItemXScale.domain(d3.extent(topData, function(d) { return d.sales; })); // set the x domain
var f_sel = svgTopItems.selectAll("g")
.data(topData,function(d){ return d.item_name; }).enter();
f_sel.exit().remove();
var f_ent = f_sel.enter().append("g");
f_ent.append("rect")
.attr("class", "dot") // wonder if I really need this class?
.attr("width", function(d){ return d.sales })
.attr("height", barHeight)
.style("fill","#351eff") // color factor variable
.style("fill-opacity", 0.75);
// add our text to our g
f_ent.append("text")
.attr("font-size", 10)
.attr("text-anchor", "left")
.attr("fill", "black")
.attr("dx", item_width/2)
.attr("dy", item_height/2)
.text(function(d){ return d.item_name});
// UPDATE + ENTER selection
f_sel = f_ent.merge(f_sel);
f_sel.transition()
.duration(1200)
.delay(function(d, i) { return i *40; })
.attr("transform", function(d, i){
return "translate( 0, "+ i*25 +")" + ")";
});
}
/* MAIN */
var data = d3.csvParse( d3.select("pre#data").text());
drawItemLayout(data, '1-20-2017');
drawPieChart(data, '1-20-2017');
drawTopItems(data, '1-20-2017');
/* UPDATE DATA */
function updateData(date) {
//d3.csv("http://localhost:8080/udacity_test_vis_1/output_fixture_data.csv", function(data) {
var data = d3.csvParse( d3.select("pre#data").text());
drawItemLayout (data, date);
drawPieChart(data, date);
drawTopItems(data, date);
}
/* GET SELECTION */
$("#select_params").change(function(){
var date = $("#select_params :selected").val();
console.log(date);
updateData(date);
})
Just three problems:
You are repeating the enter() function:
var f_sel = svgTopItems.selectAll("g")
.data(topData,function(d){ return d.item_name; }).enter();
//this is an enter selection...
var f_ent = f_sel.enter().append("g");
//and you have enter() again here
So, remove the first enter: f_sel should be just the data-binding selection.
Move your merged selection to before appending the rectangles
Your translate has an extra parenthesis:
return "translate( 0, "+ i*25 +")" + ")";
With that problems corrected, this is your updated fiddle: https://jsfiddle.net/utf5hva2/

Updating D3 V3 Zoomable Sunburst data breaks chart

I'm new to programming D3, we are having a problem updating the sunburst chart without causing it to break its transitions. When the data is changed, parts of the chart stop resizing for the zooming feature and appear to be over each other. Additionally, I get errors resulting, apparently, from the interpolation of the flags each "arc" in the graph (i.e. Error: attribute d: Expected arc flag ('0' or '1'), "…125862517296 0 0.987032831999999…".). We are using a tween that is specific for this problem and thus can't understand what might be causing it.
Since I'm inexperienced in this language I've spent several days on this problem only to see it come back. If any of you could help I would appreciate it immensely.
Here's a working fiddle that I created to illustrate our problem:
- https://jsfiddle.net/v9ab0vms/1/
The problem can be seen by waiting 5 seconds and clicking the "Commercial" and then "Communications" arcs. Causing the transition errors and breaking some of the arcs. Zooming the graph while the data gets changed causes it to break in other ways, any help on that would also be greatly appreciated. I was just trying to fix it step by step.
Picture of broken chart arcs
The code has to be posted as well so I'm just inserting the two main functions for the sunburst:
function genSunburst2() {
var width = d3.select("#container_sunburst").style("width").split("px")[0],
height = d3.select("#container_sunburst").style("height").split("px")[0],
radius = Math.min(width, height) / 2;
var x = d3.scale.linear()
.range([0, 2 * Math.PI]);
var y = d3.scale.sqrt()
.range([0, radius]);
var color = d3.scale.category20c();
var svg = d3.select("#sunburst")
.attr("id", "sunburst")
.attr("width", width)
.attr("height", height)
.select("g")
.attr("transform", "translate(" + width / 2 + "," + height / 2 + ")");
var partition = d3.layout.partition()
.value(function(d) {
return d.size;
});
var arc = d3.svg.arc()
.startAngle(function(d) {
return Math.max(0, Math.min(2 * Math.PI, x(d.x)));
})
.endAngle(function(d) {
return Math.max(0, Math.min(2 * Math.PI, x(d.x + d.dx)));
})
.innerRadius(function(d) {
return Math.max(0, y(d.y));
})
.outerRadius(function(d) {
return Math.max(0, y(d.y + d.dy));
});
function computeTextRotation(d) {
var angle = x(d.x + d.dx / 2) - Math.PI / 2;
return angle / Math.PI * 180;
}
function arcTween(d) {
var xd = d3.interpolate(x.domain(), [d.x, d.x + d.dx]),
yd = d3.interpolate(y.domain(), [d.y, 1]),
yr = d3.interpolate(y.range(), [d.y ? 20 : 0, radius]);
return function(d, i) {
return i ? function(t) {
return arc(d);
} : function(t) {
x.domain(xd(t));
y.domain(yd(t)).range(yr(t));
return arc(d);
};
};
}
function arcTweenUpdate(a) {
console.log(path);
var _self = this;
var i = d3.interpolate({
x: this.x0,
dx: this.dx0
}, a);
return function(t) {
var b = i(t);
console.log(window);
_self.x0 = b.x;
_self.dx0 = b.dx;
return arc(b);
};
}
updateSunburst3 = function() {
if (sunburstClick) {
sunburstClick = false;
return;
}
var root = createJsonDataset();
// DATA JOIN - Join new data with old elements, if any.
var gs = svg.selectAll("g").data(partition.nodes(root));
// ENTER
var g = gs.enter().append("g").on("click", click);
// UPDATE
var path = g.append("path");
gs.select('path')
.style("fill", function(d) {
return color(d.name);
})
.on("click", click)
.each(function(d) {
this.x0 = d.x;
this.dx0 = d.dx;
})
.transition().duration(500)
.attr("d", arc);
var text = g.append("text");
gs.select('text')
.attr("x", function(d) {
return y(d.y);
})
.attr("dx", "6") // margin
.attr("dy", ".35em") // vertical-align
.attr("transform", function(d) {
return "rotate(" + computeTextRotation(d) + ")";
})
.text(function(d) {
return d.name;
})
.style("fill", "white");
function click(d) {
sunburstClick = true;
console.log(d);
// fade out all text elements
/*if (d.size !== undefined) {
d.size += 100;
};*/
text.transition().attr("opacity", 0);
console.log(path);
for (var i = 0; i < path[0].length; ++i) {
if (path[0][i] === undefined || path[0][i] === null) {
path[0].splice(i, 1);
--i;
}
}
path.transition()
.duration(750)
.attrTween("d", arcTween(d))
.each("end", function(e, i) {
// check if the animated element's data e lies within the visible angle span given in d
if (e.x >= d.x && e.x < (d.x + d.dx)) {
// get a selection of the associated text element
var arcText = d3.select(this.parentNode).select("text");
// fade in the text element and recalculate positions
arcText.transition().duration(750)
.attr("opacity", 1)
.attr("transform", function() {
return "rotate(" + computeTextRotation(e) + ")"
})
.attr("x", function(d) {
return y(d.y);
});
}
});
userSelection = undefined;
purposeSelection = undefined;
// TODO: alterar para ele ter em conta a hierarquia da selecao corrente
// para so sair de uma hierarquia
if (typeof d.name != "undefined" && d.name != "") {
if (typeof d.size === "undefined") {
userSelection = d.name;
purposeSelection = undefined;
} else {
userSelection = d.parent.name;
purposeSelection = d.name;
}
} else {
// if only the user was selected, back out to no selection
if (purposeSelection === undefined && userSelection != undefined) {
userSelection = undefined;
// if both the user and the purpose were selected, back out to just user selection
} else if (purposeSelection != undefined && userSelection != undefined) {
purposeSelection = undefined;
}
}
applySelection();
}
// EXIT - Remove old elements as needed.
gs.exit().transition().duration(500).style("fill-opacity", 1e-6).remove();
}
}
Thank you in advance
Edit: To clarify:
This is in D3 V3.
We have two variables one with the full dataset
and another with the "working" dataset. We apply filters to the full
dataset and store the result in the working dataset.

Categories

Resources