Need help converting the d3 brush points in the xScale domain - javascript

I am trying to brush over a scatterplot in d3, and I'm getting the selection points as a 2X2 array matrix. Console output and the plot are shown in the image.
The problem arises when I try to convert these points to the xScale and yScale domains of the plot. The console output for example saysUncaught TypeError: Cannot read properties of undefined (reading '336.001953125') because xScale is undefined.
Below is the code snippet where I define the brush and try to convert the points in the xScale domain.
updateVis(xvar, yvar) {
let vis = this;
// Specificy accessor functions
vis.colorValue = d => d.cylinders;
vis.xValue = d => d[xvar];
vis.yValue = d => d[yvar];
// Set the scale input domains
vis.xScale.domain([d3.min(vis.data, vis.xValue), d3.max(vis.data, vis.xValue)]);
vis.yScale.domain([0, d3.max(vis.data, vis.yValue)]);
vis.brush = d3.brush()
.extent([[vis.config.margin.left-3, vis.config.margin.top-10], [vis.width+45, vis.height+30]])
.on("start", brushstart);
function brushstart() {
let vis = this;
console.log(1)
console.log(vis.xScale)
//vis.search();
if(d3.event.selection != null) {
// cell is the SplomCell object
var brushExtent = d3.event.selection;
// Check if this g element is different than the previous brush
console.log(brushExtent)
console.log(vis.xScale)
console.log(vis.xScale[brushExtent[0][0]])
}
}
vis.brushG = vis.svg.append('g')
.attr('class', 'brush x-brush')
.call(vis.brush);
vis.renderVis();
}
Below is the entire class file
class Scatterplot {
/**
* Class constructor with basic chart configuration
* #param {Object}
* #param {Array}
*/
constructor(_config, _data, _xvar, _yvar) {
this.config = {
parentElement: _config.parentElement,
colorScale: _config.colorScale,
containerWidth: _config.containerWidth || 600,
containerHeight: _config.containerHeight || 500,
margin: _config.margin || {top: 25, right: 20, bottom: 20, left: 35},
tooltipPadding: _config.tooltipPadding || 15
}
this.data = _data;
this.xScale;
this.initVis();
}
/**
* We initialize scales/axes and append static elements, such as axis titles.
*/
initVis() {
let vis = this;
// Calculate inner chart size. Margin specifies the space around the actual chart.
vis.width = vis.config.containerWidth - vis.config.margin.left - vis.config.margin.right;
vis.height = vis.config.containerHeight - vis.config.margin.top - vis.config.margin.bottom;
// vis.xScale = d3.scaleLinear()
// .range([0, vis.width]);
vis.xScale = d3.scaleLinear()
.range([0, vis.width]);
vis.xScale2 = d3.scaleLinear()
.range([0, vis.width]);
vis.yScale = d3.scaleLinear()
.range([vis.height, 0]);
// Initialize axes
vis.xAxis = d3.axisBottom(vis.xScale)
.ticks(6)
.tickSize(-vis.height - 10)
.tickPadding(10)
.tickFormat(d => d);
//.tickFormat(d => d.cylinders);
vis.yAxis = d3.axisLeft(vis.yScale)
.ticks(6)
.tickSize(-vis.width - 10)
.tickPadding(10);
// Define size of SVG drawing area
vis.svg = d3.select(vis.config.parentElement)
.attr('width', vis.config.containerWidth)
.attr('height', vis.config.containerHeight);
// Append group element that will contain our actual chart
// and position it according to the given margin config
vis.chart = vis.svg.append('g')
.attr('transform', `translate(${vis.config.margin.left},${vis.config.margin.top})`);
// Append empty x-axis group and move it to the bottom of the chart
vis.xAxisG = vis.chart.append('g')
.attr('class', 'axis x-axis')
.attr('transform', `translate(0,${vis.height})`);
// Append y-axis group
vis.yAxisG = vis.chart.append('g')
.attr('class', 'axis y-axis');
}
/**
* Prepare the data and scales before we render it.
*/
updateVis(xvar, yvar) {
let vis = this;
// Specificy accessor functions
vis.colorValue = d => d.cylinders;
vis.xValue = d => d[xvar];
vis.yValue = d => d[yvar];
// Set the scale input domains
vis.xScale.domain([d3.min(vis.data, vis.xValue), d3.max(vis.data, vis.xValue)]);
vis.xScale2.domain([d3.min(vis.data, vis.xValue), d3.max(vis.data, vis.xValue)]);
//console.log(vis.xScale2)
vis.yScale.domain([0, d3.max(vis.data, vis.yValue)]);
vis.brush = d3.brush()
.extent([[vis.config.margin.left-3, vis.config.margin.top-10], [vis.width+45, vis.height+30]])
.on("start", brushstart);
//.on("brush", brushmove)
//.on("end", brushend);
function brushstart() {
let vis = this;
console.log(1)
console.log(vis.xScale2)
//vis.search();
if(d3.event.selection != null) {
// cell is the SplomCell object
var brushExtent = d3.event.selection;
// Check if this g element is different than the previous brush
console.log(brushExtent)
console.log(vis.xScale)
console.log(vis.xScale[brushExtent[0][0]])
}
}
vis.brushG = vis.svg.append('g')
.attr('class', 'brush x-brush')
.call(vis.brush);
vis.renderVis();
}
/**
* Bind data to visual elements.
*/
renderVis() {
let vis = this;
// Add circles
const circles = vis.chart.selectAll('.point')
.data(vis.data, d => d.name)
.join('circle')
.attr('class', 'point')
.attr('r', 4)
.attr('cy', d => vis.yScale(vis.yValue(d)))
.attr('cx', d => vis.xScale(vis.xValue(d)))
.attr('fill', d => vis.config.colorScale(vis.colorValue(d)));
// Tooltip event listeners
// Update the axes/gridlines
// We use the second .call() to remove the axis and just show gridlines
vis.xAxisG
.call(vis.xAxis)
.call(g => g.select('.domain').remove());
vis.yAxisG
.call(vis.yAxis)
.call(g => g.select('.domain').remove())
//vis.search();
}
}

Related

Updating D3 interactive barchart by removing bars that are already there

Issue: I want to update the bars in my graph so that when the "Dreamworks" button is clicked, it appends new bars and gets rid of the old ones. I know it is an enter(), exit() issue, but I do not know exactly how to implement it.
Context: When my button is clicked, it activates a function that extracts the inner HTML of my button and uses it to filter my data so only observations from a company remain. The code works, but instead of getting rid of old bars, it appends the new bars on top of the old ones. When you look in the console, a new "g" element appears (which contains the new "rects") every time the button is clicked. I lowered the opacity of the "rects" to show what is going on. I removed all exit() and remove() attempts from my code because nothing was working.
HTML Code:
<div class= "button-holder">
<button class= "button button-dreamworks">DreamWorks</button>
<button class= "button button-disney">Disney</button>
<button class= "button button-pixar">Pixar</button>
</div>
<div class = "chart chart1"></div>
JS Code:
async function drawBar() {
// 2. Create Chart Dimensions
const width = 600
let dimensions = {
width,
height: width*0.6,
margin: {
top: 30,
right: 10,
bottom: 50,
left: 50
}
}
dimensions.boundedWidth = dimensions.width
-dimensions.margin.right -dimensions.margin.left
dimensions.boundedHeight = dimensions.height
-dimensions.margin.top -dimensions.margin.left
// 3. Draw Canvas
const wrapper = d3.select(".chart1")
.append("svg")
.attr("width", dimensions.width)
.attr("height", dimensions.height)
// 4. Load Data
const raw_data = await d3.csv("./data/all_movie_data.csv")
const drawBarChart = function(company_name) {
const dataset = raw_data.filter(function(d){ return d["company"] == company_name })
const xAccessor = d => d["name"]
const yAccessor = d => parseFloat(d.budget)
let bounds = wrapper
.append("g")
.attr("class", "bounds")
.style(
"transform",
`translate(${dimensions.margin.left}px,${dimensions.margin.top}px)`
);
// 5. Create scales
const xScale = d3.scaleBand()
.domain(dataset.map(xAccessor))
.range([0,dimensions.boundedWidth])
.padding(0.4);
const yScale = d3.scaleLinear()
.domain(d3.extent(dataset,yAccessor))
.range([dimensions.boundedHeight, 0])
// 6. Draw Data
bounds.selectAll("rect")
.data(dataset)
.join("rect")
.attr("x", (d) => xScale(xAccessor(d)))
.attr("y", (d) => yScale(yAccessor(d)))
.attr("width", xScale.bandwidth())
.attr("height", (d) => dimensions.boundedHeight - yScale(yAccessor(d)))
.attr("fill", "blue");
}
//6. Interactions
drawBarChart("Pixar");
const button1 = d3.select(".button-dreamworks")
.node()
.addEventListener("click", onClick1)
function onClick1() {
const company = document.querySelector(".button-dreamworks").innerHTML;
drawBarChart(company);
}
}
drawBar();
You can find a version of my code in this code pen: https://codepen.io/larylc/pen/XWzbQGy
Everything is the same except for the data, which I just made up to show the issue.
Answer: I understand the enter(), exit() structure now.
By setting the "rects" as a variable (which I called bars) I can now manipulate them ( this gave me a selection object that was different from before).
I can now add exit() and remove() to the bars variable. This was not possible before.
I moved the bounds variable to the section where my canvas was, which completed the exit() and enter() pattern. Every time a button was clicked, the SVG elements in the DOM (the bars) would match the number of data elements that were supposed to be added. So if I had 22 bars in the DOM, it would update to 34(or whatever the new dataset was). I tried this before and it worked but my bars were not updating correctly, which brings me to my last point.
The last problem was the bars would be added or removed to match the number of new data points without changing existing ones. This meant that the DOM would not update the bars to match the actual data. So if I had 13 existing bars and the new data was 22, it would just keep the 13 and add the last 9. This did not necessarily match the data. So I needed to add the merge(bars) statement to ensure that all my bars (including the existing ones) would update.
My New JS Code (with all of the buttons working)
async function drawBar() {
// Create Chart Dimensions
const width = 600
let dimensions = {
width,
height: width*0.6,
margin: {
top: 30,
right: 10,
bottom: 50,
left: 50
}
}
dimensions.boundedWidth = dimensions.width
-dimensions.margin.right -dimensions.margin.left
dimensions.boundedHeight = dimensions.height
-dimensions.margin.top -dimensions.margin.left
// Draw Canvas
const wrapper = d3.select(".chart1")
.append("svg")
.attr("width", dimensions.width)
.attr("height", dimensions.height)
let bounds = wrapper
.append("g")
.attr("class", "bounds")
.style(
"transform",
`translate(${dimensions.margin.left}px,${dimensions.margin.top}px)`
);
// Load Data
const raw_data = await d3.csv("./data/all_movie_data.csv")
// Function that draws data
const drawBarChart = function(company_name) {
const dataset = raw_data.filter(function(d){ return d["company"] == company_name })
const xAccessor = d => d["name"]
const yAccessor = d => parseFloat(d.budget)
// Create scales
const xScale = d3.scaleBand()
.domain(dataset.map(xAccessor))
.range([0,dimensions.boundedWidth])
.padding(0.4);
const yScale = d3.scaleLinear()
.domain(d3.extent(dataset,yAccessor))
.range([dimensions.boundedHeight, 0])
// Draw Data
const bars = bounds.selectAll("rect")
.data(dataset)
bars.join("rect").merge(bars)
.attr("x", (d) => xScale(xAccessor(d)))
.attr("y", (d) => yScale(yAccessor(d)))
.attr("width", xScale.bandwidth())
.attr("height", (d) => dimensions.boundedHeight - yScale(yAccessor(d)))
.attr("fill", "blue")
.attr("opacity", 0.4)
bars.exit().remove();
}
// Interactions
drawBarChart("Pixar");
// All Buttons and functions that triggers data change
const button1 = d3.select(".button-dreamworks")
.node()
.addEventListener("click", onClick1)
function onClick1() {
const company = document.querySelector(".button-dreamworks").innerHTML;
drawBarChart(company);
}
const button2 = d3.select(".button-disney")
.node()
.addEventListener("click", onClick2)
function onClick2() {
const company2 = document.querySelector(".button-disney").innerHTML;
drawBarChart(company2);
}
const button3 = d3.select(".button-pixar")
.node()
.addEventListener("click", onClick3)
function onClick3() {
const company3 = document.querySelector(".button-pixar").innerHTML;
drawBarChart(company3);
}
}
drawBar();

dc.js - Rendering two objects (one chart - renders, one shape - doesn't) together in one group?

I have two elements I need to render and a context of the big picture I am trying to achieve (a complete dashboard).
One is a chart that renders fine.
$scope.riskChart = new dc.pieChart('#risk-chart');
$scope.riskChart
.width(width)
.height(height)
.radius(Math.round(height/2.0))
.innerRadius(Math.round(height/4.0))
.dimension($scope.quarter)
.group($scope.quarterGroup)
.transitionDuration(250);
The other is a triangle, to be used for a more complex shape
$scope.openChart = d3.select("#risk-chart svg g")
.enter()
.attr("width", 55)
.attr("height", 55)
.append('path')
.attr("d", d3.symbol('triangle-up'))
.attr("transform", function(d) { return "translate(" + 100 + "," + 100 + ")"; })
.style("fill", fill);
On invocation of render functions, the dc.js render function is recognized and the chart is seen, but the d3.js render() function is not recognized.
How do I add this shape to my dc.js canvas (an svg element).
$scope.riskChart.render(); <--------------Works!
$scope.openChart.render(); <--------------Doesn't work (d3.js)!
How do I make this work?
EDIT:
I modified dc.js to include my custom chart, it is a work in progress.
dc.starChart = function(parent, fill) {
var _chart = {};
var _count = null, _category = null;
var _width, _height;
var _root = null, _svg = null, _g = null;
var _region;
var _minHeight = 20;
var _dispatch = d3.dispatch('jump');
_chart.count = function(count) {
if(!arguments.length)
return _count;
_count = count;
return _chart;
};
_chart.category = function(category) {
if(!arguments.length)
return _category
_category = category;
return _chart;
};
function count() {
return _count;
}
function category() {
return _category;
}
function y(height) {
return isNaN(height) ? 3 : _y(0) - _y(height);
}
_chart.redraw = function(fill) {
var color = fill;
var triangle = d3.symbol('triangle-up');
this._g.attr("width", 55)
.attr("height", 55)
.append('path')
.attr("d", triangle)
.attr("transform", function(d) { return "translate(" + 25 + "," + 25 + ")"; })
.style("fill", fill);
return _chart;
};
_chart.render = function() {
_g = _svg
.append('g');
_svg.on('click', function() {
if(_x)
_dispatch.jump(_x.invert(d3.mouse(this)[0]));
});
if (_root.select('svg'))
_chart.redraw();
else{
resetSvg();
generateSvg();
}
return _chart;
};
_chart.on = function(event, callback) {
_dispatch.on(event, callback);
return _chart;
};
_chart.width = function(w) {
if(!arguments.length)
return this._width;
this._width = w;
return _chart;
};
_chart.height = function(h) {
if(!arguments.length)
return this._height;
this._height = h;
return _chart;
};
_chart.select = function(s) {
return this._root.select(s);
};
_chart.selectAll = function(s) {
return this._root.selectAll(s);
};
function resetSvg() {
if (_root.select('svg'))
_chart.select('svg').remove();
generateSvg();
}
function generateSvg() {
this._svg = _root.append('svg')
.attr({width: _chart.width(),
height: _chart.height()});
}
_root = d3.select(parent);
return _chart;
}
I think I confused matters by talking about how to create a new chart, when really you just want to add a symbol to an existing chart.
In order to add things to an existing chart, the easiest thing to do is put an event handler on its pretransition or renderlet event. The pretransition event fires immediately once a chart is rendered or redrawn; the renderlet event fires after its animated transitions are complete.
Adapting your code to D3v4/5 and sticking it in a pretransition handler might look like this:
yearRingChart.on('pretransition', chart => {
let tri = chart.select('svg g') // 1
.selectAll('path.triangle') // 2
.data([0]); // 1
tri = tri.enter()
.append('path')
.attr('class', 'triangle')
.merge(tri);
tri
.attr("d", d3.symbol().type(d3.symbolTriangle).size(200))
.style("fill", 'darkgreen'); // 5
})
Some notes:
Use chart.select to select items within the chart. It's no different from using D3 directly, but it's a little safer. We select the containing <g> here, which is where we want to add the triangle.
Whether or not the triangle is already there, select it.
.data([0]) is a trick to add an element once, only if it doesn't exist - any array of size 1 will do
If there is no triangle, append one and merge it into the selection. Now tri will contain exactly one old or new triangle.
Define any attributes on the triangle, here using d3.symbol to define a triangle of area 200.
Example fiddle.
Because the triangle is not bound to any data array, .enter() should not be called.
Try this way:
$scope.openChart = d3.select("#risk-chart svg g")
.attr("width", 55)
.attr("height", 55)
.append('path')
.attr("d", d3.symbol('triangle-up'))
.attr("transform", function(d) { return "translate(" + 100 + "," + 100 + ")"; })
.style("fill", fill);

Adding nodes dynamically to D3 Force Layout in version 4

I am trying to implement a simple force layout in which nodes (without links) can be dynamically added and removed. I was successful in implementing the concept in D3 version 3, but I am unable to translate it to version 4. After adding and updating nodes the simulation freezes and incoming circles are drawn in the upper left corner of the svg. Does someone knows why this is the case? Thanks for any help :)
My concept is based on this solution:
Adding new nodes to Force-directed layout
JSFiddle: working code in d3 v3
/* Define class */
class Planet {
constructor(selector) {
this.w = $(selector).innerWidth();
this.h = $(selector).innerHeight();
this.svg = d3.select(selector)
.append('svg')
.attr('width', this.w)
.attr('height', this.h);
this.force = d3.layout.force()
.gravity(0.05)
.charge(-100)
.size([this.w, this.h]);
this.nodes = this.force.nodes();
}
/* Methods (are called on object) */
update() {
/* Join selection to data array -> results in three new selections enter, update and exit */
const circles = this.svg.selectAll('circle')
.data(this.nodes, d => d.id); // arrow function, function(d) { return d.y;}
/* Add missing elements by calling append on enter selection */
circles.enter()
.append('circle')
.attr('r', 10)
.style('fill', 'steelblue')
.call(this.force.drag);
/* Remove surplus elements from exit selection */
circles.exit()
.remove();
this.force.on('tick', () => {
circles.attr('cx', d => d.x)
.attr('cy', d => d.y);
});
/* Restart the force layout */
this.force.start();
}
addThought(content) {
this.nodes.push({ id: content });
this.update();
}
findThoughtIndex(content) {
return this.nodes.findIndex(node => node.id === content);
}
removeThought(content) {
const index = this.findThoughtIndex(content);
if (index !== -1) {
this.nodes.splice(index, 1);
this.update();
}
}
}
/* Instantiate class planet with selector and initial data*/
const planet = new Planet('.planet');
planet.addThought('Hallo');
planet.addThought('Ballo');
planet.addThought('Yallo');
This is my intent of translating the code into v4:
/* Define class */
class Planet {
constructor(selector) {
this.w = $(selector).innerWidth();
this.h = $(selector).innerHeight();
this.svg = d3.select(selector)
.append('svg')
.attr('width', this.w)
.attr('height', this.h);
this.simulation = d3.forceSimulation()
.force('charge', d3.forceManyBody())
.force('center', d3.forceCenter(this.w / 2, this.h / 2));
this.nodes = this.simulation.nodes();
}
/* Methods (are called on object) */
update() {
/* Join selection to data array -> results in three new selections enter, update and exit */
let circles = this.svg.selectAll('circle')
.data(this.nodes, d => d.id); // arrow function, function(d) { return d.y;}
/* Add missing elements by calling append on enter selection */
const circlesEnter = circles.enter()
.append('circle')
.attr('r', 10)
.style('fill', 'steelblue');
circles = circlesEnter.merge(circles);
/* Remove surplus elements from exit selection */
circles.exit()
.remove();
this.simulation.on('tick', () => {
circles.attr('cx', d => d.x)
.attr('cy', d => d.y);
});
/* Assign nodes to simulation */
this.simulation.nodes(this.nodes);
/* Restart the force layout */
this.simulation.restart();
}
addThought(content) {
this.nodes.push({ id: content });
this.update();
}
findThoughtIndex(content) {
return this.nodes.findIndex(node => node.id === content);
}
removeThought(content) {
const index = this.findThoughtIndex(content);
if (index !== -1) {
this.nodes.splice(index, 1);
this.update();
}
}
}
Please see plunkr example
I'm using canvas, but the theory is the same:
You have to give your new array of nodes and links to D3 core functions first, before adding them to the original array.
drawData: function(graph){
var countExtent = d3.extent(graph.nodes,function(d){return d.connections}),
radiusScale = d3.scalePow().exponent(2).domain(countExtent).range(this.nodes.sizeRange);
// Let D3 figure out the forces
for(var i=0,ii=graph.nodes.length;i<ii;i++) {
var node = graph.nodes[i];
node.r = radiusScale(node.connections);
node.force = this.forceScale(node);
};
// Concat new and old data
this.graph.nodes = this.graph.nodes.concat(graph.nodes);
this.graph.links = this.graph.links.concat(graph.links);
// Feed to simulation
this.simulation
.nodes(this.graph.nodes);
this.simulation.force("link")
.links(this.graph.links);
this.simulation.alpha(0.3).restart();
}
Afterwards, tell D3 to restart with the new data.
When D3 calls your tick() function, it already knows what coordinates you need to apply to your SVG elements.
ticked: function(){
if(!this.graph) {
return false;
}
this.context.clearRect(0,0,this.width,this.height);
this.context.save();
this.context.translate(this.width / 2, this.height / 2);
this.context.beginPath();
this.graph.links.forEach((d)=>{
this.context.moveTo(d.source.x, d.source.y);
this.context.lineTo(d.target.x, d.target.y);
});
this.context.strokeStyle = this.lines.stroke.color;
this.context.lineWidth = this.lines.stroke.thickness;
this.context.stroke();
this.graph.nodes.forEach((d)=>{
this.context.beginPath();
this.context.moveTo(d.x + d.r, d.y);
this.context.arc(d.x, d.y, d.r, 0, 2 * Math.PI);
this.context.fillStyle = d.colour;
this.context.strokeStyle =this.nodes.stroke.color;
this.context.lineWidth = this.nodes.stroke.thickness;
this.context.fill();
this.context.stroke();
});
this.context.restore();
}
Plunkr example

Converting static code into reusable D3.js pie animation

I'm trying to rework a pen (http://codepen.io/anon/pen/JgyCz) by Travis Palmer so that I can use it on multiple elements. We are trying to place several <div class="donut" data-donut="x">'s on a page.
So it would look similar to the html below:
////// HTML
<div class="donut" data-donut="22"></div>
<div class="donut" data-donut="48"></div>
<div class="donut" data-donut="75></div>
The D3.js / jQuery example I'm trying to convert to a reusable compunent is below. (To see full working example go to this link - http://codepen.io/anon/pen/JgyCz)
////// D3.js
var duration = 500,
transition = 200;
drawDonutChart(
'.donut',
$('.donut').data('donut'),
290,
290,
".35em"
);
function drawDonutChart(element, percent, width, height, text_y) {
width = typeof width !== 'undefined' ? width : 290;
height = typeof height !== 'undefined' ? height : 290;
text_y = typeof text_y !== 'undefined' ? text_y : "-.10em";
var dataset = {
lower: calcPercent(0),
upper: calcPercent(percent)
},
radius = Math.min(width, height) / 2,
pie = d3.layout.pie().sort(null),
format = d3.format(".0%");
var arc = d3.svg.arc()
.innerRadius(radius - 20)
.outerRadius(radius);
var svg = d3.select(element).append("svg")
.attr("width", width)
.attr("height", height)
.append("g")
.attr("transform", "translate(" + width / 2 + "," + height / 2 + ")");
var path = svg.selectAll("path")
.data(pie(dataset.lower))
.enter().append("path")
.attr("class", function(d, i) { return "color" + i })
.attr("d", arc)
.each(function(d) { this._current = d; }); // store the initial values
var text = svg.append("text")
.attr("text-anchor", "middle")
.attr("dy", text_y);
if (typeof(percent) === "string") {
text.text(percent);
}
else {
var progress = 0;
var timeout = setTimeout(function () {
clearTimeout(timeout);
path = path.data(pie(dataset.upper)); // update the data
path.transition().duration(duration).attrTween("d", function (a) {
// Store the displayed angles in _current.
// Then, interpolate from _current to the new angles.
// During the transition, _current is updated in-place by d3.interpolate.
var i = d3.interpolate(this._current, a);
var i2 = d3.interpolate(progress, percent)
this._current = i(0);
return function(t) {
text.text( format(i2(t) / 100) );
return arc(i(t));
};
}); // redraw the arcs
}, 200);
}
};
function calcPercent(percent) {
return [percent, 100-percent];
};
The best way to do this is to use angular directives. An angular directive basically wraps html inside a custom tag and let's you stamp the directive over and over across multiple pages or multiple times a page. See this video: http://www.youtube.com/watch?v=aqHBLS_6gF8
There is also a library that is out called nvd3.js that contains prebuilt angular directives that can be re-used: http://nvd3.org/
Hope this helps.
ok, I figured it out. I feel a bit dumb in hindsight, but what can I say, I'm a js n00b. All you have to do is make a few more call to the drawDonutChart() method. In short:
drawDonutChart(
'#donut1',
$('#donut1').data('donut'),
220,
220,
".35em"
);
drawDonutChart(
'#donut2',
$('#donut2').data('donut'),
120,
120,
".35em"
);
drawDonutChart(
'#donut3',
$('#donut3').data('donut'),
150,
150,
".2em"
);

d3.js time series infinite scroll

I am working on a time series line chart that lets the user scroll back from the present. I can find tutorials on real-time d3.js charts, I can find tutorials on zooming and panning, and I can find tutorials on using external data sources. I'm having trouble putting all this knowledge together.
Here is the behavior that I am looking for:
The chart can pan backward in time (meaning that the lines, data points, and axes move with dragging of the mouse or finger)
Panning should only effect the x-axis, and no zooming should occur.
As the user pans the chart, more data loads in, giving an experience of infinite scrolling
I plan on buffering in at least one extra "page" worth of data for the user to scroll into (already got this part figured out)
I don't think I need transitions, because the panning of the chart will already smoothly translate it
This is what I have working so far:
// set up a zoom handler only for panning
// by limiting the scaleExtent
var zoom = d3.behavior.zoom()
.x(x)
.y(y)
.scaleExtent([1, 1])
.on("zoom", pan);
var loadedPage = 1; // begin with one page of data loaded
var nextPage = 2; // next page will be page 2
var panX = 0;
function pan()
{
if (d3.event)
{
panX = d3.event ? d3.event.translate[0] : 0;
// is there a better way to determine when
// to load the next page?
nextPage = panX / (width + margin.left + margin.right) + 2;
nextPage = Math.floor(nextPage);
// if we haven't loaded in the next page's data
// load it in so that the user can scroll into it
if (nextPage > loadedPage) {
console.log("Load a new page");
loadedPage += 1;
// load more data
Chart.query( /*params will be here*/ ).then(
function(response) {
// append the new data onto the front of the array
data = data.concat(response);
console.log(data.length);
// I need to add the new data into the line chart
// but how do I make that work with the pan
// logic from zoom?
}
);
}
// is this where I update the axes and scroll the chart?
// What's the best way to do that?
}
}
In this code, I can know when to pull more data from the server, but I'm not sure how to insert the data into the chart in a way that works with the pan offset. Do I use transform translate, or can I update the d value of the path of my line?
Any suggestions would be welcome... also, if anyone knows of any demos which already show panning infinitely through time series data, that would be much appreciated.
As mentioned in the other answer, I know this is a very old post but hopefully the following will help someone...
I made a pen that I think hits all the requirements mentioned. As I didn't have a real API to use, I created some data using a json-generator (great tool), included it, and sorted it in descending order. Then I use the built in slice and concat methods to take bits of the array, data, and add to the chart_data variable (similarly to how one might use an api).
Important Sections:
Once you've created your scales, axes, and points (lines, bars, etc.), you need to create the zoom behavior. As mentioned in the question, keeping the scaleExtent limited to the same number on both sides prevents zooming:
var pan = d3.behavior.zoom()
.x(x_scale)
.scale(scale)
.size([width, height])
.scaleExtent([scale, scale])
.on('zoom', function(e) { ... });
Now that we've created the behavior, we need to call it. I'm also calculating what the x translation will be for this moment in time, now, and programmatically panning there:
// Apply the behavior
viz.call(pan);
// Now that we've scaled in, find the farthest point that
// we'll allow users to pan forward in time (to the right)
max_translate_x = width - x_scale(new Date(now));
viz.call(pan.translate([max_translate_x, 0]).event);
Both preventing the user from scrolling past now and loading more data is all done in the zoom event handler:
...
.scaleExtent([scale, scale])
.on('zoom', function(e) {
var current_domain = x_scale.domain(),
current_max = current_domain[1].getTime();
// If we go past the max (i.e. now), reset translate to the max
if (current_max > now)
pan.translate([max_translate_x, 0]);
// Update the data & points once user hits the point where current data ends
if (pan.translate()[0] > min_translate_x) {
updateData();
addNewPoints();
}
// Redraw any components defined by the x axis
x_axis.call(x_axis_generator);
circles.attr('cx', function(d) {
return x_scale(new Date(d.registered));
});
});
The other functions are pretty straightforward and can be found at the bottom of the pen. I'm not aware of any built in D3 function to prevent panning past the present but I'm definitely open to feedback if I've missed an easier way to do some of this.
Let me know if you have trouble viewing the pen or need clarification on something. If I have time I'll update this with another version demoing an infinite scrolling line chart.
P.S. In the pen, I'm consoling out the selection and data as they update. I suggest opening the console to see exactly what's happening.
This is too late, but answering just in case somebody needs again. I was having most of the code ready for my scatterplot so uploading that. Hope it helps you. The code is created as a trial when I was learning this features. So please check before you use.
Note:
D3js panning implemented with zoom behavior,
zooming disabled with scaleExtent,
Y panning restricted.
Data loaded when x extremes are reached.
Please check the Plunkr link
// Code goes here
window.chartBuilder = {};
(function(ns) {
function getMargin() {
var margin = {
top: 20,
right: 15,
bottom: 60,
left: 60
};
var width = 960 - margin.left - margin.right;
var height = 500 - margin.top - margin.bottom;
return {
margin: margin,
width: width,
height: height
};
}
function getData() {
var data = [
[5, 3],
[10, 17],
[15, 4],
[2, 8]
];
return data;
}
//function defineScales(data, width, height) {
// var x = d3.scale.linear()
// .domain([0, d3.max(data, function (d) {
// return d[0];
// })])
// .range([0, width]);
//
// var y = d3.scale.linear()
// .domain([0, d3.max(data, function (d) {
// return d[1];
// })])
// .range([height, 0]);
// return {x: x, y: y};
//}
function defineYScale(data, domain, range) {
var domainArr = domain;
if (!domain || domain.length == 0) {
domainArr = [0, d3.max(data, function(d) {
return d[1];
})];
}
var y = d3.scale.linear()
.domain(domainArr)
.range(range);
return y;
}
function defineXScale(data, domain, range) {
var domainArr = domain;
if (!domain || domain.length == 0) {
domainArr = [d3.min(data, function(d) {
return d[0];
}), d3.max(data, function(d) {
return d[0];
})];
}
var x = d3.scale.linear()
.domain(domainArr)
.range(range);
return x;
}
function getSvg(width, margin, height) {
var chart = d3.select('body')
.append('svg:svg')
.attr('width', width + margin.right + margin.left)
.attr('height', height + margin.top + margin.bottom)
.attr('class', 'chart');
return chart;
}
function getContainerGroup(chart, margin, width, height) {
var main = chart.append('g')
.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')')
.attr('width', width)
.attr('height', height)
.attr('class', 'main');
return main;
}
function renderXAxis(x, main, height) {
var xAxis = d3.svg.axis()
.scale(x)
.orient('bottom');
var xAxisElement = main.select('.x.axis');
if (xAxisElement.empty()) {
xAxisElement = main.append('g')
.attr('transform', 'translate(0,' + height + ')')
.attr('class', 'x axis')
}
xAxisElement.call(xAxis);
return xAxis;
}
function renderYAxis(y, main) {
var yAxis = d3.svg.axis()
.scale(y)
.orient('left');
var yAxisElement = main.select('.y.axis');
if (yAxisElement.empty()) {
yAxisElement = main.append('g')
.attr('transform', 'translate(0,0)')
.attr('class', 'y axis');
}
yAxisElement.call(yAxis);
return yAxis;
}
function renderScatterplot(main, data, scales) {
var g = main.append("svg:g");
var divTooltip = d3.select('.tooltip1');
if (divTooltip.empty()) {
divTooltip = d3.select('body').append('div')
.attr('class', 'tooltip1')
.style('opacity', 0);
}
g.selectAll("scatter-dots")
.data(data, function(d, i) {
return i;
})
.enter().append("svg:circle")
.attr("cx", function(d, i) {
return scales.x(d[0]);
})
.attr("cy", function(d) {
return scales.y(d[1]);
})
.on('click', function(d) {
// log(d.toString());
})
.attr("r", 8);
}
function addZoomRect(main, scales, zoom) {
var zoomRect = main.append('rect')
.attr('width', function() {
return scales.x(d3.max(scales.x.domain()));
})
.attr('height', function() {
return scales.y(d3.min(scales.y.domain()));
})
.attr('x', 0)
.attr('y', 0)
.attr('fill', 'transparent')
.attr('stroke', 'red');
if (zoom) {
zoomRect.call(zoom);
}
return zoomRect;
}
function restrictYPanning(zoom) {
var zoomTranslate = this.translate();
this.translate([zoomTranslate[0], 0]);
}
function addXScrollEndEvent(scales, direction, data) {
var zoomTranslate = this.translate();
var condition;
var currentDomainMax = d3.max(scales.x.domain());
var dataMax = d3.max(data, function(d) {
return d[0];
});
var currentDomainMin = d3.min(scales.x.domain());
var dataMin =
d3.min(data, function(d) {
return d[0];
});
if (currentDomainMax > dataMax && direction === 'right') {
//log('currentDomainMax ', currentDomainMax);
//log('dataMax ', dataMax);
//log('----------------');
condition = true;
}
if (dataMin > currentDomainMin && direction === 'left') {
//log('currentDomainMin ', currentDomainMin);
//log('dataMin ', dataMin);
//log('----------------');
condition = true;
}
//var xRightLimit, xTranslate;
//if (direction === 'right') {
// xRightLimit = scales.x(d3.max(scales.x.domain())) - (getMargin().width + 60);
//
// xTranslate = 0 - zoomTranslate[0];// + scales.x(d3.min(scales.x.domain()));
//
// condition = xTranslate > xRightLimit;
//} else {
// xRightLimit = scales.x(d3.min(scales.x.domain()));
//
// xTranslate = zoomTranslate[0];// + scales.x(d3.min(scales.x.domain()));
//
// condition = xTranslate > xRightLimit;
//}
return condition;
}
function onZoom(zoom, main, xAxis, yAxis, scales, data) {
//var xAxis = d3.svg.axis()
// .scale(scales.x)
// .orient('bottom');
//var yAxis = d3.svg.axis()
// .scale(scales.y)
// .orient('left');
//alert(data);
var translate = zoom.translate();
var direction = '';
if (translate[0] < ns.lastTranslate[0]) {
direction = 'right';
} else {
direction = 'left';
}
ns.lastTranslate = translate; //d3.transform(main.attr('transform')).translate ;
// log('zoom translate', ns.lastTranslate);
// log('d3 Event translate', d3.event.translate);
window.scales = scales;
window.data = data;
// ns.lastTranslate = translate;
var divTooltip = d3.select('.tooltip1');
if (divTooltip.empty()) {
divTooltip = d3.select('body').append('div')
.attr('class', 'tooltip1')
.style('opacity', 0);
}
restrictYPanning.call(zoom);
var xScrollEndCondition = addXScrollEndEvent.call(zoom, scales, direction, data);
if (xScrollEndCondition) {
if (zoom.onXScrollEnd) {
zoom.onXScrollEnd.call(this, {
'translate': translate,
'direction': direction
});
}
}
main.select(".x.axis").call(xAxis);
main.select(".y.axis").call(yAxis);
var dataElements = main.selectAll("circle")
.data(data, function(d, i) {
return i;
});
dataElements.attr("cx", function(d, i) {
return scales.x(d[0]);
})
.attr("cy", function(d) {
return scales.y(d[1]);
}).attr("r", 8);
dataElements.enter().append("svg:circle")
.attr("cx", function(d, i) {
return scales.x(d[0]);
})
.attr("cy", function(d) {
return scales.y(d[1]);
}).on('click', function(d) {
// log(d.toString());
})
.attr("r", 8);
// log(direction);
}
//var xRangeMax;
//var xRangeMin;
ns.lastTranslate = [0, 0];
/**
* Created by Lenovo on 7/4/2015.
*/
function log(titlee, msgg) {
var msg = msgg;
var title;
if (titlee) {
title = titlee + ':-->';
}
if (!msgg) {
msg = titlee;
title = '';
} else {
if (Array.isArray(msgg)) {
msg = msgg.toString();
}
if ((typeof msg === "object") && (msg !== null)) {
msg = JSON.stringify(msg);
}
}
var tooltip = d3.select('.tooltip1');
var earlierMsg = tooltip.html();
var num = tooltip.attr('data-serial') || 0;
num = parseInt(num) + 1;
msg = '<div style="border-bottom:solid 1px green"><span style="color:white">' + num + ')</span><strong>' + title + '</strong> ' + decodeURIComponent(msg) + ' </div>';
tooltip.html('<br>' + msg + '<br>' + earlierMsg).style({
'color': 'lightGray',
'background': 'darkGray',
'font-family': 'courier',
'opacity': 1,
'max-height': '200px',
'overflow': 'auto'
})
.attr('data-serial', num);
}
function addLoggerDiv() {
var divTooltip = d3.select('.tooltip1');
if (divTooltip.empty()) {
divTooltip = d3.select('body').append('div')
.attr('class', 'tooltip1')
.style({
'opacity': 0,
'position': 'relative'
});
d3.select('body').append('div')
.text('close')
.style({
'top': 0,
'right': 0,
'position': 'absolute',
'background': 'red',
'color': 'white',
'cursor': 'pointer'
})
.on('click', function() {
var thisItem = divTooltip;
var txt = thisItem.text();
var display = 'none';
if (txt === 'close') {
thisItem.text('open');
display = 'none';
} else {
thisItem.text('close');
display = 'block';
}
devTooltip.style('display', display);
});
d3.select('body').append('div')
.text('clear')
.style({
'top': 0,
'right': 20,
'position': 'absolute',
'background': 'red',
'color': 'white',
'cursor': 'pointer'
})
.on('click', function() {
divTooltip.html('');
divTooltip.attr('data-serial', '0');
});
}
}
$(document).ready(function() {
var data = getData();
var __ret = getMargin();
var margin = __ret.margin;
var width = __ret.width;
var height = __ret.height;
var scales = {};
var xRangeMax = width;
scales.x = defineXScale(data, [], [0, xRangeMax]);
scales.y = defineYScale(data, [], [height, 0]);
addLoggerDiv();
var svg = getSvg(width, margin, height);
var main = getContainerGroup(svg, margin, width, height);
// draw the x axis
var xAxis = renderXAxis(scales.x, main, height);
// draw the y axis
var yAxis = renderYAxis(scales.y, main);
var thisobj = this;
var zoom = d3.behavior.zoom().x(scales.x).y(scales.y).scaleExtent([1, 1]).on('zoom', function() {
onZoom.call(null, zoom, main, xAxis, yAxis, scales, data);
});
zoom.onXScrollEnd = function(e) {
var maxX = d3.max(data, function(d) {
return d[0];
});
var minX = d3.min(data, function(d) {
return d[0];
});
var incrementX = Math.floor((Math.random() * 3) + 1);
var maxY = d3.max(data, function(d) {
return d[1];
})
var minY = d3.min(data, function(d) {
return d[1];
})
var incrementY = Math.floor((Math.random() * 1) + 16);
var xRangeMin1, xRangeMax1, dataPoint;
if (e.direction === 'left') {
incrementX = incrementX * -1;
dataPoint = minX + incrementX;
// log('dataPoint ', dataPoint);
//xRangeMin1 = d3.min(scales.x.range()) - Math.abs(scales.x(minX) - scales.x(dataPoint));
xRangeMin1 = scales.x(dataPoint);
xRangeMax1 = d3.max(scales.x.range());
} else {
dataPoint = maxX + incrementX;
// log('dataPoint ', dataPoint);
//xRangeMax1 = d3.max(scales.x.range()) + (scales.x(dataPoint) - scales.x(maxX));
xRangeMax1 = d3.max(scales.x.range()) + 20; //scales.x(dataPoint);
xRangeMin1 = d3.min(scales.x.range()) //e.translate[0];
}
data.push([dataPoint, incrementY]);
//scales = defineScales(data, width + incrementX, height );
// scales.x = defineXScale(data, [], [xRangeMin1, xRangeMax1]);
// scales.y = defineYScale(data, [], [height, 0]);
scales.x.domain(d3.extent(data, function(d) {
return d[0];
}));
x = scales.x;
y = scales.y;
xAxis = renderXAxis(scales.x, main, height);
// draw the y axis
yAxis = renderYAxis(scales.y, main);
zoom.x(scales.x).y(scales.y);
}
var zoomRect = addZoomRect(main, scales, zoom);
renderScatterplot(main, data, scales);
});
})(window.chartBuilder);
/* Styles go here */
.chart {
font-family: Arial, sans-serif;
font-size: 10px;
}
.axis path, .axis line {
fill: none;
stroke: #000;
shape-rendering: crispEdges;
}
.bar {
fill: steelblue;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
I have created zoom.onXScrollEnd function to add new points to data.
Hope it helps.

Categories

Resources