Inspired by https://www.nytimes.com/interactive/2018/03/19/upshot/race-class-white-and-black-men.html
I am trying to create a animation that will have markers move from one point to multiple levels in another point in the y axis. Thanks to detailed pages by Amelia and Mike Bostock in both bl.ocks.org and stackoverflow. I have got so far to get the circles and animate it. But, I am not able to make each marker loop over by the pathlevel and do the transition
Pathlevel, here, indicates whether they are high, middle or low (1,2,3).
The entire code in using d3.v4 has been pasted below. what I am missing?
Thanks for your help.
<!DOCTYPE html>
<meta charset="utf-8">
<title>SANKEY Experiment</title>
<style>
</style>
<body>
<script type="text/javascript" src="d3.v4.js"></script>
<script>
//ref: very important for path animation: https://stackoverflow.com/questions/21226523/trace-path-with-dom-object/21228701#21228701
//ref: clustered force layout: https://bl.ocks.org/mbostock/7881887
//ref: data manipulation: http://learnjsdata.com/iterate_data.html
var margin = {top: 30, right: 10, bottom: 30, left: 20},
width = 500 - margin.left - margin.right,
height = 500 - margin.top - margin.bottom;
var series = [{x:5,y:10},{x:150,y:10}],
series2 = [{x:5,y:10},{x:50,y:15},{x:100,y:30},{x:150,y:30}],
series3 = [{x:5,y:10},{x:50,y:22},{x:100,y:50},{x:150,y:50}];
var data = [{"dim":"a","pos":"high","pathlevel":1,"x1":1,"y1":10,"x2":150,"y2":8},
{"dim":"b","pos":"high","pathlevel":1,"x1":1,"y1":10,"x2":150,"y2":8},
{"dim":"a","pos":"mid","pathlevel":2,"x1":1,"y1":10,"x2":150,"y2":28},
{"dim":"b","pos":"mid","pathlevel":2,"x1":1,"y1":10,"x2":150,"y2":28},
{"dim":"a","pos":"low","pathlevel":3,"x1":1,"y1":10,"x2":150,"y2":48},
{"dim":"b","pos":"low","pathlevel":3,"x1":1,"y1":10,"x2":150,"y2":48}]
var x = d3.scaleLinear()
.domain([5,150])
.range([0,width]);
var y = d3.scaleLinear()
.domain([10,50])
.range([0,height]);
var line = d3.line()
.curve(d3.curveCardinal)
.x(function(d) { return x(d.x); })
.y(function(d) { return y(d.y); });
var svg = d3.select("body").append("svg").attr("height",600).attr("width",600);
var chart = svg.append("g").attr("transform","translate("+margin.left+","+margin.top+")");
var update = function(series,k){
chart.append("path")
.attr("class","route"+k)
.attr("fill","none")
.attr("stroke","blue")
.attr("d",line(series));
}
update(series,1);
update(series2,2);
update(series3,3);
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//create transistions along the path //////////////////////////////////////////////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
var colorScale = d3.scaleOrdinal()
.domain(['a','b'])
.range(['orange','darkblue']);
//Get path start point for placing marker
var path = d3.select('.route1')
var startPoint = path.attr("d").split(",")[1];
//path example["M12.885906040268456", "84.48979591836735C12.885906040268456", "84.48979591836735", "241.07382550335572", "84.48979591836735",
//"318.9261744966443", "84.48979591836735C396.7785234899329", "84.48979591836735", "480", "84.48979591836735", "480", "84.48979591836735"]
//selecting class route which represents the path. d represents the path that is held by the path object. in that we split by comma and take the first
console.log(startPoint);
var glider = function(data,p){//p for path level
var marker = chart.selectAll(".marker")
.data(data)
.enter().append('circle')
.attr("class","marker")
.attr("fill",function(d){ return colorScale(d.dim);})
//.attr("x",function(d,i){return x(d.x1)+i*10;})
//.attr("y",function(d,i){return y(d.y1)+i*10;})
.attr("r",5);
//.attr("width",10)
//.attr("height",10);
var simulation = d3.forceSimulation()
.force("x",d3.forceX().strength(0.05))
.force("y",d3.forceY().strength(0.01))
.force('charge', d3.forceManyBody().strength(20))
.force("collide",d3.forceCollide(function(d){return y(d.y1)+4;}))
.alphaTarget(.03)
.restart();
simulation.nodes(data)
.on('tick',ticked);
function ticked(){
marker
.attr("cx",function(d){ return d.x;})
.attr("cy",function(d){ return d.y;})
}//end of ticked
//marker.transition().duration(3000).delay(200)
// .attr("x",function(d,i){return x(d.x2)+i*10;});
function translateAlong(path) {
var l = path.getTotalLength();
return function (d) {
return function (t) {
var p = path.getPointAtLength(t * l);
return "translate(" + p.x + "," + p.y + ")";//Move marker
}
}
}//end of translateAlong
console.log(marker);
function transition(){
var path2 = d3.select('.route'+p);
marker.attr("transform", "translate(" + startPoint + ")").transition().duration(3000).delay(function(d,i) { return i * 100; })
.attrTween("transform",translateAlong(path2.node()));
//.attr("x",function(d,i){return x(d.x2)+i*10;});
}//end of transition
transition();
}
/*var check = d3.map(data, function(d){return d.pathlevel;}).keys(); //for getting unique values from a column
check.forEach(function(i){
datapoints = data.filter(function(d){return d.pathlevel==i});
console.log(i);
glider(datapoints,i);
});*/
data1 = data.filter(function(d){return d.pathlevel==1});
data2 = data.filter(function(d){return d.pathlevel==2});
data3 = data.filter(function(d){return d.pathlevel==3});
//glider(data1,1);
//glider(data2,2);
glider(data3,3);
//glider(data,2);
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
/*function createPathTween(d, i, a) {
var path = this.parentNode.getElementsByTagName("path")[1];
//i.e., go from this <circle> -> parent <g> -> array of child <path> elements
//-> first (and only) element in that array
//console.log(path);
var l = path.getTotalLength();
return function(t) {
var p = path.getPointAtLength(t * l);
console.log(p);
return "translate(" + p.x + "," + p.y + ")";
};
}//end of tweenpath*/
</script>
</body>
Related
I am trying to insert a x3d object via ajax callback but the object doesn't appear. I copied the source code of the page then placed it on a new page then the x3d object showed. Am I missing something here? Is there a work around for this? thanks.
html:
<div id="divPlot" style="border: 1px solid black"></div>
<button onclick="add_x3d()">Click</button>
<script>
d3.select('html').style('height','100%').style('width','100%');
d3.select('body').style('height','100%').style('width','100%');
d3.select('#divPlot').style('width', "450px").style('height', "450px");
function add_x3d() {
scatterPlot3d(d3.select('#divPlot'));
}
</script>
javascript:
function scatterPlot3d(parent){
var rows = [
{"SITE":"1","SIGNAME":"A","X":10,"Y":10,"Z":111},
{"SITE":"1","SIGNAME":"B","X":200,"Y":10,"Z":222},
{"SITE":"2","SIGNAME":"A","X":10,"Y":40,"Z":333},
{"SITE":"2","SIGNAME":"B","X":200,"Y":40,"Z":444},
{"SITE":"3","SIGNAME":"A","X":10,"Y":70,"Z":555},
{"SITE":"3","SIGNAME":"B","X":200,"Y":70,"Z":666},
{"SITE":"4","SIGNAME":"A","X":10,"Y":100,"Z":777},
{"SITE":"4","SIGNAME":"B","X":200,"Y":100,"Z":888}
];
var x3d = parent
.append("x3d")
.style("width", parseInt(parent.style("width")) + "px")
.style("height", parseInt(parent.style("height")) + "px")
.style("border", "none");
var scene = x3d.append("scene");
scene.append("orthoviewpoint")
.attr("centerOfRotation", [5, 5, 5])
.attr("fieldOfView", [-5, -5, 15, 15])
.attr("orientation", [-0.5, 1, 0.2, 1.12 * Math.PI / 4])
.attr("position", [8, 4, 15]);
// Used to make 2d elements visible
function makeSolid(selection, color) {
selection.append("appearance")
.append("material")
.attr("diffuseColor", color || "black");
return selection;
}
function constVecWithAxisValue(otherValue, axisValue, axisIndex) {
var result = [otherValue, otherValue, otherValue];
result[axisIndex] = axisValue;
return result;
}
var XAxisMin = d3.min(rows, function(d){return d.X;});
var XAxisMax = d3.max(rows, function(d){return d.X;});
var XAxisDel = XAxisMax-XAxisMin;
var YAxisMin = d3.min(rows, function(d){return d.Y;});
var YAxisMax = d3.max(rows, function(d){return d.Y;});
var YAxisDel = YAxisMax-YAxisMin;
var ZAxisMin = d3.min(rows, function(d){return d.Z;});
var ZAxisMax = d3.max(rows, function(d){return d.Z;});
var ZAxisDel = ZAxisMax-ZAxisMin;
function AxisMin(axisIndex) {
return [XAxisMin, ZAxisMin, YAxisMin][axisIndex];
}
function AxisMax(axisIndex) {
return [XAxisMax, ZAxisMax, YAxisMax][axisIndex];
}
function AxisDel(axisIndex) {
return [XAxisDel, ZAxisDel, YAxisDel][axisIndex];
}
function axisName(name, axisIndex) {
return AxisKeys[axisIndex] + name;
}
function get2DVal(){
if (XAxisDel >= YAxisDel){
return XAxisDel;
} else {
return YAxisDel;
}
}
function ConvAxisRange(inputVal, axisIndex) {
var val;
if (axisIndex === 0 || axisIndex === 2) {
val = d3.scale.linear()
.domain([0, delta2D])
.range(AxisRange);
} else {
val = d3.scale.linear()
.domain([0, ZAxisDel])
.range(AxisRange);
}
return val(inputVal);
}
function ConvAxisRange2D(inputVal) {
var val = d3.scale.linear()
.domain([0, delta2D])
.range(AxisRange);
return val(inputVal);
}
var AxisKeys = ["X", "HEIGHT", "Y"];
var AxisRange = [0, 10];
var scales = [];
var AxisLen;
var duration = 300;
var delta2D = get2DVal();
var ArrayOfColors = ["#0000FF", "#00FFFF", "#00FF00", "#FFFF00", "#FF0000"];
var colorScale = d3.scale.linear()
.domain([0, ZAxisDel*0.25, ZAxisDel*0.50, ZAxisDel*0.75, ZAxisDel])
.range(ArrayOfColors);
function initializeAxis(axisIndex) {
var key = AxisKeys[axisIndex];
drawAxis(axisIndex, key, duration);
var scaleDel = AxisDel(axisIndex);
var rotation = [[0, 0, 0, 0], [0, 0, 1, Math.PI / 2], [0, 1, 0, -Math.PI / 2]];
var newAxisLine = scene.append("transform")
.attr("class", axisName("Axis", axisIndex))
.attr("rotation", (rotation[axisIndex]))
.append("shape");
newAxisLine
.append("appearance")
.append("material")
.attr("emissiveColor", "lightgray");
newAxisLine
.append("polyline2d")
// Line drawn along y axis does not render in Firefox, so draw one
// along the x axis instead and rotate it (above).
.attr("lineSegments", "[" + ConvAxisRange(scaleDel, axisIndex) + " 0, 0 0]");
// axis labels
var newAxisLabel = scene.append("transform")
.attr("class", axisName("AxisLabel", axisIndex))
.attr("translation", constVecWithAxisValue(0, ConvAxisRange(scaleDel*1.15, axisIndex), axisIndex));
var newAxisLabelShape = newAxisLabel
.append("billboard")
.attr("axisOfRotation", "0 0 0") // face viewer
.append("shape")
.call(makeSolid);
var labelFontSize = 0.6;
newAxisLabelShape
.append("text")
.attr("class", axisName("AxisLabelText", axisIndex))
.attr("solid", "true")
.attr("string", key)
.append("fontstyle")
.attr("size", labelFontSize)
.attr("family", "SANS")
.attr("justify", "END MIDDLE");
}
// Assign key to axis, creating or updating its ticks, grid lines, and labels.
function drawAxis(axisIndex, key, duration) {
var scale;
if (axisIndex === 0 || axisIndex === 2) {
scale = d3.scale.linear()
.domain([AxisMin(axisIndex), AxisMax(axisIndex)]) // demo data range
.range([0, ConvAxisRange2D(AxisDel(axisIndex))]);
} else {
scale = d3.scale.linear()
.domain([AxisMin(axisIndex), AxisMax(axisIndex)]) // demo data range
.range(AxisRange);
}
scales[axisIndex] = scale;
var numTicks = 5;
var tickSize = 0.1;
var tickFontSize = 0.5;
// ticks along each axis
var ticks = scene.selectAll("." + axisName("Tick", axisIndex))
.data(scale.ticks(numTicks));
var newTicks = ticks.enter()
.append("transform")
.attr("class", axisName("Tick", axisIndex));
newTicks.append("shape").call(makeSolid)
.append("box")
.attr("size", tickSize + " " + tickSize + " " + tickSize);
// enter + update
ticks.transition().duration(duration)
.attr("translation", function(tick) {
return constVecWithAxisValue(0, scale(tick), axisIndex);
});
ticks.exit().remove();
// tick labels
var tickLabels = ticks.selectAll("billboard shape text")
.data(function(d) {
return [d];
});
var newTickLabels = tickLabels.enter()
.append("billboard")
.attr("axisOfRotation", "0 0 0")
.append("shape")
.call(makeSolid);
newTickLabels.append("text")
.attr("string", scale.tickFormat(10))
.attr("solid", "true")
.append("fontstyle")
.attr("size", tickFontSize)
.attr("family", "SANS")
.attr("justify", "END MIDDLE");
tickLabels // enter + update
.attr("string", scale.tickFormat(10));
tickLabels.exit().remove();
}
function plotData() {
if (!rows) {
console.log("no rows to plot.");
return;
}
var x = scales[0], z = scales[1], y = scales[2];
var sphereRadius = 0.2;
// Draw a sphere at each x,y,z coordinate.
var datapoints = scene.selectAll(".datapoint").data(rows);
datapoints.exit().remove();
var newDatapoints = datapoints.enter()
.append("transform")
.attr("class", "datapoint")
.attr("scale", [sphereRadius, sphereRadius, sphereRadius])
.append("shape");
newDatapoints
.append("appearance")
.append("material");
newDatapoints
.append("sphere");
// Does not work on Chrome; use transform instead
//.attr("radius", sphereRadius)
datapoints.selectAll("shape appearance material")
.attr("diffuseColor", function(row){
return colorScale(row.Z-ZAxisMin);
});
datapoints.attr("translation", function(row) {
return x(row.X) + " " + z(row.Z) + " " + y(row.Y);
});
}
function initializePlot() {
initializeAxis(0);
initializeAxis(1);
initializeAxis(2);
}
initializePlot();
}
You cannot add the whole x3d element and a scene dynamically per se since x3dom is initialized with an window.onload event. This should be part of your HTML document beforehand. Then you can add the elements (views, shapes etc) to the scene.
But I heard sometime ago something about a reload function (https://github.com/x3dom/x3dom/blob/1.7.1/src/Main.js#L327) in the mailing list, sadly this is not well documented:
/** Initializes an <x3d> root element that was added after document load. */
x3dom.reload = function() {
onload();
};
This should be doing what you want.
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"
);
I am yet a noob for d3 and javascript,while I was writing some code to deal with drag the dots and zoom the coordinate, something weird happened, the dots call the drag function,an outer group() element call the zoom, but when I dragging the dot, the outer group change its translate attribute.Code comes below:
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="content-type" content="text/html; charset=utf-8" />
<title>zoom test</title>
</head>
<body></body>
<script type="text/javascript" src="http://d3js.org/d3.v3.min.js">
</script>
<script type="text/javascript" charset="utf-8">
function addElem(container,elem){
return container.append(elem);
}
function createLinearScale(dx,dy,rx,ry){
return d3.scale.linear()
.domain([dx,dy])
.range([rx,ry])
}
function appendWithData(container,selector,type,id,classed,dataset){
var result = container.selectAll(selector)
.data(dataset)
.enter()
.append(type);
if(id)
result.attr("id",id);
if(classed)
result.classed(classed,true);
return result;
}
function getElem(selector){
return d3.select(selector);
}
function getAxis(){
return d3.svg.axis();
}
function drag(){
return d3.behavior.drag();
}
function getThis(){
return d3.select("this");
}
function zoom(){
return d3.behavior.zoom();
}
function arrayDelete(array,target,condition){
for (var i = 0, l = array.length; i < l; i ++) {
var v = arr[i];
if((target = v) && condition){
array.splice(i,1);
}
}
}
</script>
<script type="text/javascript" charset="utf-8">
/**
* Set frame for page
*
*/
var body = getElem("body");
var svg = addElem(body,"svg");
var outer = addElem(svg,"g");
var target = addElem(outer,"g");
var x_axis = addElem(target,"g");
var y_axis = addElem(target,"g");
var dots = addElem(target,"g");
var data = [
[ 5, 20 ],
[ 480, 90 ],
[ 250, 50 ],
[ 100, 33 ],
[ 330, 95 ],
[ 410, 12 ],
[ 475, 44 ],
[ 25, 67 ],
[ 85, 21 ],
[ 220, 88 ]
];
/**
* Add axis to chart
*
*/
var height = 500;
var width = 960;
var x_scale = createLinearScale(0, d3.max(data,function(d){return d[0];}) + 50, 0, width - 10);
var y_scale = createLinearScale(0, d3.max(data, function(d){return d[1];}) + 50, height - 10, 0);
var ax_scale = createLinearScale(0, width - 10, 0, d3.max(data,function(d){return d[0];}) + 50);
var ay_scale = createLinearScale(height -10, 0, 0, d3.max(data, function(d){ return d[1];}) + 50);
var xaxis = getAxis().scale(x_scale).orient("bottom").ticks(30);
var yaxis = getAxis().scale(y_scale).orient("right").ticks(10);
x_axis.attr("transform",function(d){return "translate(0," + (height - 10) + ")"}).call(xaxis);
y_axis.attr("transform",function(d){return "translate(0,0)"}).call(yaxis);
/**
* Add dots * */
var dot = appendWithData(dots,"circle","circle","","datum",data);
var text = appendWithData(dots,"text","text","","index",data);
dot.data(data).attr({
"id" : function(d,i){return "datum" +i;},
"tag": function(d,i){return i + "";},
"cx" : function(d){return x_scale(d[0]).toFixed(0)},
"cy" : function(d){return y_scale(d[1]).toFixed(0)},
"fill" : function(d){return "rgb(0," + (d[1] * 5) % 255 + ",53)"},
"r" : 10 });
text.data(data).attr({
"id" : function(d,i){return "index" + i;},
"tag": function(d,i){return i + ""},
"y" : function(d){return y_scale(d[1]).toFixed(0)},
"x" : function(d){return x_scale(d[0]).toFixed(0)},
"transform" : "translate(15,-15)"
})
.text(function(d){return d[0] + "," + d[1]});
var flag = 1;
var drag = drag();
function dragstart(){
console.log("dragstart")
var cur = d3.select(this);
console.log("drag");
cur.transition().ease("elastc").attr({
"r" : 15
});
}
function dragging(){
flag = 0;
var cur = d3.select(this);
var tag = cur.attr("tag");
cir = d3.select("circle[tag='" + tag + "']");
txt = d3.select("text[tag='" + tag + "']");
console.log(cur);
console.log(txt);
var cur_x = d3.event.x;
var cur_y = d3.event.y;
//target.attr("transform","translate(0,0)");
cir.attr({
"cx" : function(d){return cur_x.toFixed(0)},
"cy" : function(d){return cur_y.toFixed(0)},
//"fill" : function(d){return "rgb(0," + (y_scale(cur_y) * 5) % 255 + ",53)"},
});
txt.attr({
"x" : function(d){return cur_x.toFixed(0)},
"y" : function(d){return cur_y.toFixed(0)},
})
.text(new Number(ax_scale(cur_x)).toFixed(0) + "," + new Number(ay_scale(cur_y)).toFixed(0));
}
function dragged(){
var cur = d3.select(this);
cur.transition().ease("elastc").attr({
"r" : 10
});
flag = 1;
}
drag.on("dragstart",dragstart)
.on("drag",dragging)
.on("dragend",dragged);
dot.call(drag);;
var zoom = zoom();
function zoomed(){
//if(flag){
console.log("zoomed");
outer.attr("transform","translate(" + d3.event.translate + ") scale(" + d3.event.scale + ")");
//}
}
zoom.on("zoom",zoomed);
outer.call(zoom);
</script>
</html>
As you see, I set a toggle(the variable called flag) to solve the trigger problem,but it cause a new problem, I can't zoom with wheel when the mouse is aloft a blank space.
If I understand the question correctly, you want the zooming on the outer while you want dragging on the dots. In that case, there were two problems with your program:
Empty g as the outer: A g element is just an empty container and is unable to capture events inside itself. If you want to capture mouse actions anywhere inside the g, you need to include an invisible background rect inside it with pointer-events: all. Also, you want this element to appear before all other elements in the DOM so that it is indeed in the background:
var bgRect = addElem(outer, "rect");
bgRect.attr('fill', 'none')
.attr('stroke', 'none')
.attr('width', width)
.attr('height', height);
var target = addElem(outer, "g");
// ...
Update: See this question as well: d3.js - mouseover event not working properly on svg group
Transitioning on zooming: The "expected" zoom behavior is that the point under the mouse pointer stays static while everything around the pointer zooms in/out. Hence, the container needs to both scale and transition.
function zoomed(){
console.log("zoomed");
outer.attr("transform","translate(" + d3.event.translate + ") scale(" + d3.event.scale + ")");
}
However, in your case, you do not want the container to transition so that the axis stays firmly rooted at the origin (0, 0). So:
function zoomed(){
console.log("zoomed");
outer.attr("transform","translate(" + [0,0] + ") scale(" + d3.event.scale + ")");
}
Working demo with these changes: http://jsfiddle.net/S3GsC/
i'm struggling with positioning and scaling of a voronoi diagram on a leaflet map.
the voronoi polygons are correctly displayed after they're appended on the map, but
after resizing they don't scale and translate correctly. i tried to reset the path after
panning and zooming to the feature element. but it looks like the new values are passed to their parent's element. If i set the path on feature.selectAll('path).attr('d',path) the scaling and translation is absolutly correct, but it show's the voronoi means instead the voronoi polygons. Any Idea?
Best regards,
Flo
this.id = p_id;
this.data = p_data;
d3.select('#'+this.id).remove();
var svg = d3.select(map.getPanes().overlayPane).append("svg").attr('id', this.id);
var g = svg.append("g").attr("class", "leaflet-zoom-hide").attr("id", "cells");
var pointdata = this.data.features;
var positions = [];
pointdata.forEach(function(d){
positions.push(project(d.geometry.coordinates));
});
var polygons = d3.geom.voronoi(positions);
var bounds = d3.geo.bounds(this.data),
path = d3.geo.path().projection(project);
var feature = g.selectAll('g').data(pointdata).enter().append('g');
feature.append('path')
.attr('class', 'cell')
.attr({
"d":function(d, i) { return "M" + polygons[i].join("L") + "Z"},
stroke:"#43676b",
fill: "rgba(255,140,10,0.3)"
});
map.on("viewreset", reset);
reset();
function reset() {
scale = Math.pow(2, map.getZoom() - map.options.zoom);
var padding = 25;
var bottomLeft = project(bounds[0]),
topRight = project(bounds[1]);
bottomLeft = [bottomLeft[0]-padding, bottomLeft[1]+padding]
topRight = [topRight[0]+padding, topRight[1]-padding]
console.log(polygons);
svg.attr("width", topRight[0] - bottomLeft[0]).attr("height", bottomLeft[1] - topRight[1]).style("margin-left", bottomLeft[0] + "px").style("margin-top", topRight[1] + "px");
g.attr("transform", "translate(" + -bottomLeft[0] + "," + -topRight[1] + ")");
feature.attr('d', path);
}
function project(x) {
var point = map.latLngToLayerPoint(new L.LatLng(x[1], x[0]));
return [point.x, point.y];
}
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.