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"
);
Related
I followed this Observable post to easily create a legend.
Since the line
DOM.canvas(1, n)
in the ramp works only on Observable, I replaced it with
document.createElement("canvas")
and also modified the SVG so that it's appended to the main div tag. These changed do not cause any errors however the problem is that the legend is not displayed even though the legend SVG is present in the raw HTML.
Here's the link to a JSFiddle. Any guidance would be greatly appreciated.
The canvas is being created, that's not the problem. The problem is that, since you are now missing the width and height in...
const canvas = DOM.canvas(n, 1);
//these are w & h --------^--^
... you now need to set those yourself. For instance:
d3.select(canvas).attr("width", n)
.attr("height", 1);
Here is a simplified version of that JSFiddle, showing that the canvas works:
legend({
color: d3.scaleSequential([1, 10], d3.interpolateReds),
title: "Title"
})
function legend({
color,
title,
tickSize = 6,
width = 320,
height = 44 + tickSize,
marginTop = 18,
marginRight = 0,
marginBottom = 16 + tickSize,
marginLeft = 0,
ticks = width / 64,
tickFormat,
tickValues
} = {}) {
const svg = d3.select(".scatter").append("svg")
.attr("width", width)
.attr("height", height)
.attr("viewBox", [0, 0, width, height])
.style("overflow", "visible")
.style("display", "block");
svg.append("image")
.attr("x", marginLeft)
.attr("y", marginTop)
.attr("width", width - marginLeft - marginRight)
.attr("height", height - marginTop - marginBottom)
.attr("preserveAspectRatio", "none")
.attr("xlink:href", ramp(color.interpolator()).toDataURL());
}
function ramp(color, n = 256) {
const canvas = document.createElement('canvas');
const context = canvas.getContext("2d");
d3.select(canvas).attr("width", n)
.attr("height", 1);
for (let i = 0; i < n; ++i) {
context.fillStyle = color(i / (n - 1));
context.fillRect(i, 0, 1, 1);
}
return canvas;
}
<script type="text/javascript" src="https://d3js.org/d3.v5.js"></script>
<div class="scatter">
</div>
By the way, there is no such element as <legend-svg>.
PS: This is the second question from you I'm answering on this subject. As you're new to JavaScript and D3, here is an advice: do not try to use that Observable notebook, that's way too complicated for your purposes. Just create the SVG, the canvas and a basic axis yourself, from scratch, it will be way easier.
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);
I'm having trouble translating a D3 example with a zoom behavior from v3 to v5. My code is based on this example: https://bl.ocks.org/mbostock/2206340 by Mike Bostock. I use react and I get these errors "d3.zoom(...).translate is not a function" and "d3.zoom(...).scale is not a function". I looked in the documentation, but could not find scale or translate just scaleBy and translateTo and translateBy. I can't figure out how to do it either way.
componentDidMount() {
this.drawChart();
}
drawChart = () => {
var width = window.innerWidth * 0.66,
height = window.innerHeight * 0.7,
centered,
world_id;
window.addEventListener("resize", function() {
width = window.innerWidth * 0.66;
height = window.innerHeight * 0.7;
});
var tooltip = d3
.select("#container")
.append("div")
.attr("class", "tooltip hidden");
var projection = d3
.geoMercator()
.scale(100)
.translate([width / 2, height / 1.5]);
var path = d3.geoPath().projection(projection);
var zoom = d3
.zoom()
.translate(projection.translate())
.scale(projection.scale())
.scaleExtent([height * 0.197, 3 * height])
.on("zoom", zoomed);
var svg = d3
.select("#container")
.append("svg")
.attr("width", width)
.attr("class", "map card shadow")
.attr("height", height);
var g = svg.append("g").call(zoom);
g.append("rect")
.attr("class", "background")
.attr("width", width)
.attr("height", height);
var world_id = data2;
var world = data;
console.log(world);
var rawCountries = topojson.feature(world, world.objects.countries)
.features,
neighbors = topojson.neighbors(world.objects.countries.geometries);
console.log(rawCountries);
console.log(neighbors);
var countries = [];
// Splice(remove) random pieces
rawCountries.splice(145, 1);
rawCountries.splice(38, 1);
rawCountries.map(country => {
//console.log(parseInt(country.id) !== 010)
// Filter out Antartica and Kosovo
if (parseInt(country.id) !== parseInt("010")) {
countries.push(country);
} else {
console.log(country.id);
}
});
console.log(countries);
g.append("g")
.attr("id", "countries")
.selectAll(".country")
.data(countries)
.enter()
.insert("path", ".graticule")
.attr("class", "country")
.attr("d", path)
.attr("data-name", function(d) {
return d.id;
})
.on("click", clicked)
.on("mousemove", function(d, i) {
var mouse = d3.mouse(svg.node()).map(function(d) {
return parseInt(d);
});
tooltip
.classed("hidden", false)
.attr(
"style",
"left:" + mouse[0] + "px;top:" + (mouse[1] - 50) + "px"
)
.html(getCountryName(d.id));
})
.on("mouseout", function(d, i) {
tooltip.classed("hidden", true);
});
function getCountryName(id) {
var country = world_id.filter(
country => parseInt(country.iso_n3) == parseInt(id)
);
console.log(country[0].name);
console.log(id);
return country[0].name;
}
function updateCountry(d) {
console.log(world_id);
var country = world_id.filter(
country => parseInt(country.iso_n3) == parseInt(d.id)
);
console.log(country[0].name);
var iso_a2;
if (country[0].name === "Kosovo") {
iso_a2 = "xk";
} else {
iso_a2 = country[0].iso_a2.toLowerCase();
}
// Remove any current data
$("#countryName").empty();
$("#countryFlag").empty();
$("#countryName").text(country[0].name);
var src = "svg/" + iso_a2 + ".svg";
var img = "<img id='flag' class='flag' src=" + src + " />";
$("#countryFlag").append(img);
}
// Remove country when deselected
function removeCountry() {
$("#countryName").empty();
$("#countryFlag").empty();
}
// When clicked on a country
function clicked(d) {
if (d && centered !== d) {
centered = d;
updateCountry(d);
} else {
centered = null;
removeCountry();
}
g.selectAll("path").classed(
"active",
centered &&
function(d) {
return d === centered;
}
);
console.log("Clicked");
console.log(d);
console.log(d);
var centroid = path.centroid(d),
translate = projection.translate();
console.log(translate);
console.log(centroid);
projection.translate([
translate[0] - centroid[0] + width / 2,
translate[1] - centroid[1] + height / 2
]);
zoom.translate(projection.translate());
g.selectAll("path")
.transition()
.duration(700)
.attr("d", path);
}
// D3 zoomed
function zoomed() {
console.log("zoomed");
projection.translate(d3.event.translate).scale(d3.event.scale);
g.selectAll("path").attr("d", path);
}
};
render() {
return (
<div className="container-fluid bg">
<div class="row">
<div className="col-12">
<h2 className="header text-center p-3 mb-5">
Project 2 - World value survey
</h2>
</div>
</div>
<div className="row mx-auto">
<div className="col-md-8">
<div id="container" class="mx-auto" />
</div>
<div className="col-md-4">
<div id="countryInfo" className="card">
<h2 id="countryName" className="p-3 text-center" />
<div id="countryFlag" className="mx-auto" />
</div>
</div>
</div>
</div>
);
}
I won't go into the differences between v3 and v5 partly because it has been long enough that I have forgotten much of the specifics and details as to how v3 was different. Instead I'll just look at how to implement that example with v5. This answer would require adaptation for non-geographic cases - the geographic projection is doing the visual zooming in this case.
In your example, the zoom keeps track of the zoom state in order to set the projection properly. The zoom does not set a transform to any SVG element, instead the projection reprojects the features each zoom (or click).
So, to get started, with d3v5, after we call the zoom on our selection, we can set the zoom on a selected element with:
selection.call(zoom.transform, transformObject);
Where the base transform object is:
d3.zoomIdentity
d3.zoomIdentity has scale (k) of 1, translate x (x) and y (y) values of 0. There are some methods built into the identity prototype, so a plain object won't do, but we can use the identity to set new values for k, x, and y:
var transform = d3.zoomIdentity;
transform.x = projection.translate()[0]
transform.y = projection.translate()[1]
transform.k = projection.scale()
This is very similar to the example, but rather than providing the values to the zoom behavior itself, we are building an object that describes the zoom state. Now we can use selection.call(zoom.transform, transform) to apply the transform. This will:
set the zoom's transform to the provided values
trigger a zoom event
In our zoom function we want to take the updated zoom transform, apply it to the projection and then redraw our paths:
function zoomed() {
// Get the new zoom transform
transform = d3.event.transform;
// Apply the new transform to the projection
projection.translate([transform.x,transform.y]).scale(transform.k);
// Redraw the features based on the updaed projection:
g.selectAll("path").attr("d", path);
}
Note - d3.event.translate and d3.event.scale won't return anything in d3v5 - these are now the x,y,k properties of d3.event.transform
Without a click function, we might have this, which is directly adapted from the example in the question. The click function is not included, but you can still pan.
If we want to include a click to center function like the original, we can update our transform object with the new translate and call the zoom:
function clicked(d) {
var centroid = path.centroid(d),
translate = projection.translate();
// Update the translate as before:
projection.translate([
translate[0] - centroid[0] + width / 2,
translate[1] - centroid[1] + height / 2
]);
// Update the transform object:
transform.x = projection.translate()[0];
transform.y = projection.translate()[1];
// Apply the transform object:
g.call(zoom.transform, transform);
}
Similar to the v3 version - but by applying the zoom transform (just as we did initially) we trigger a zoom event, so we don't need to update the path as part of the click function.
All together that might look like this.
There is on detail I didn't include, the transition on click. As we triggering the zoomed function on both click and zoom, if we included a transition, panning would also transition - and panning triggers too many zoom events for transitions to perform as desired. One option we have is to trigger a transition only if the source event was a click. This modification might look like:
function zoomed() {
// Was the event a click?
var event = d3.event.sourceEvent ? d3.event.sourceEvent.type : null;
// Get the new zoom transform
transform = d3.event.transform;
// Apply the new transform to the projection
projection.translate([transform.x,transform.y]).scale(transform.k);
// Redraw the features based on the updaed projection:
(event == "click") ? g.selectAll("path").transition().attr("d",path) : g.selectAll("path").attr("d", path);
}
I am loading data from a google spreadsheet that contains the GDP of selected countries from the 1955 to 2012. From this I want to draw a treemap. So far so good.
I've loaded the data through out internal link and formatted into an object that d3 can handle, then got the layout to draw on the screen-all well and good. I've based it on the Mike Bostock tutorial at http://bl.ocks.org/mbostock/4063582.
The problem comes when I try to transition from a set of data from say 1955 to 2010. I'm confident that the function I'm using to generate the treemap layout is working because the initial display is correct. I pass it a date and it creates the treemap structure.
However when I trigger a change the transition seems to occur and the individual squares change size. But when I examine them I realise that they are all wrong and that I seem to have mapped the new set of value onto the wrong countries.
The newstructure looks visually correct but all the names are wrong. So I get things like cyprus having the largest GDP in 2012. Its as if I've got a list in alphabetical order thats having another set of values in order of magnitude applied to the rather that the new value for say the US being mapped the old value.
Going around in circles here as I'm still faily new to d3 so all help gratefully received.
Code looks like this:
/*global app:true JST:true d3:true*/
(function (window, $) {
'use strict';
var menuItems = [];
var menuType='measure';
var checboxItems= ['advanced','emerging'];
var ddID = '0';
var model=[];
var yearValue="2012"
var group="gdp";
var treeStruc={
name:[],
children:[]
}
var margin = {top: 25, right: 5, bottom: 5, left: 5},
width = 965 - margin.left - margin.right,
height = 650 - margin.top - margin.bottom;
var color = d3.scale.category10();
app.spreadsheet.get(function (data) {
// TODO: process the data
menuItems = data.measures
//console.log(data);
//console.log('menuItems', menuItems);
//crete dropdown and use toggle to swich display on and off
$('#dropDown').click(function () {
$('ul.ddMenuList').toggle();
});
//populate the dropdown menu
for (var k = 0; k <menuItems.length; k++) {
$('#ddList').append('<li id="dd_' + k + '"><a href="#">'+menuItems[k].menulist +'</li>');
};
//add functionality to dropDown menu
$('#ddList li').bind('click', function () {
ddID = this.id.split('_')[1];
var text = $(this).text();
//console.log ("ID=",ddID);
//console.log (text, "Measure=",menuItems[ddID].type);
$('#ddTitle').empty();
$('#ddTitle').append(text);
createCheckboxes()
});
function createCheckboxes() {
//decide which check boxes to populate
if (menuItems[ddID].type==="measure") {
group=menuItems[ddID].type
checboxItems=[];
$.each(menuItems, function (i) {
if (menuItems[i].type==="group"){
checboxItems.push (menuItems[i].checkbox);
}
//console.log (checboxItems);
});
}
else {
group=menuItems[ddID].type
checboxItems=[];
$.each(menuItems, function (i) {
if (menuItems[i].type==="measure"){
checboxItems.push (menuItems[i].checkbox);
}
//console.log (checboxItems);
});
}
//Populate the check boxes
console.log ("Populating check boxes");
$('#cbHolder').empty();
$('#cbHolder').append('<form>');
$.each(checboxItems, function (i) {
$('#cbHolder').append('<input type="checkbox" id="cb_'+i+'">'+checboxItems[i]);
$('#cbHolder').append('</form>');
//console.log ("checkboxItems",checboxItems[i]);
});
changed3 ()
}
//creates an object containing just the advanced countries
treeStruc={name:[],children:[]};
console.log ("group=",group);
$.each(checboxItems, function (k) {
console.log("Parent",checboxItems[k])
model=jQuery.grep(data.stats,function(e,i){return e[checboxItems[k]];});
console.log('model', model);
treeStruc.children.push({"name":checboxItems[k],"children":[]});
//Construct the children of 1 big group to be completed to be updated for each sheet
$.each(model, function (i) {
treeStruc.children[k].children.push({'name':model[i].countryname,'size':model[i] [group]});
});
});
console.log('treeStruc', treeStruc)
Handlebars.createOptionsHelper(data.options);
drawd3 ();
});
function generateTreemapLayout(filter){
return d3.layout.treemap()
.size([width, height])
.sticky(true)
.value(function(d) {
if(d.size[filter] < 0){
return 0;
}
return d.size[filter];
});
}
function drawd3() {
console.log ("function drawd3");
var treemap = generateTreemapLayout('y'+yearValue)
var div = d3.select("#d3Object").append("div")
.style("position", "relative")
.style("width", (width + margin.left + margin.right) + "px")
.style("height", (height + margin.top + margin.bottom) + "px")
.style("left", margin.left + "px")
.style("top", margin.top + "px");
var node = div.datum(treeStruc).selectAll(".node")
.data(treemap.nodes)
.enter().append("div")
.attr("class", "node")
.call(position)
.attr("id",function(d){
return d.name;
})
.style("background", function(d) { return d.children ? color(d.name) : null; })
.text(function(d) { return d.children ? null : d.name; });
};
function position() {
this.style("left", function(d) { return d.x + "px"; })
.style("top", function(d) { return d.y + "px"; })
.style("width", function(d) { return Math.max(0, d.dx - 1) + "px"; })
.style("height", function(d) { return Math.max(0, d.dy - 1) + "px"; });
}
function changed3() {
console.log ("function changed3");
//make a new treemap layout
var treemap = generateTreemapLayout('y'+1955);
console.log('treeStruc',treeStruc);
//redraw the treemap using transition instead of enter
var node = d3.select("#d3Object")
.datum(treeStruc).selectAll(".node")
.data(treemap.nodes)
.transition()
.duration(1500)
.call(position)
}
}(this, jQuery));
Many thanks to Tom Pearson my work colleague for this. The problem lies in where the data is bound to the item on the page. When you come to re draw the treemap because the data isn't bound to the div with a nique identifier like the object name it re maps the data to the first item o the list as it where. This means that something like China's gets given Belgium's information. simple solution is as follows Instead of
.data(treemap.nodes)
use
.data(treemap.nodes,function(d){
return d.name;
})
The are two instances of this in the original drawd3 function them in the changed3 function. Hope that helps anyone stuck with something similar
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.