I am trying to draw a huge (60k) number of (x, y) points on an HTML5 canvas and simulate streaming data points with D3.js in Chrome and Firefox, and finding that the browser freezes and crashes after about 10 seconds.
I am generating the data set with random values as follows:
var data = d3.range(60000).map(function() { return Math.random() * 500; });
Would it help to break generation of data into sections? I feel that this might be caused by trying to store such a large data set at one time as I have shown.
Is there any way that I can prevent this from happening? Such as drawing and saving smaller sections as tiled images?
Added code:
var margin = {top: 20, right: 20, bottom: 20, left: 40},
w = 100 - margin.left - margin.right,
h = 100 - margin.top - margin.bottom;
var canvas = d3.select("canvas")
.node();
var context = canvas.getContext('2d');
var scale = d3.scale.linear()
. range([0,w])
.domain([0,h]);
data = d3.range(60000).map(function(){return Math.random()*500});
data.forEach(function(d,i) {
context.strokeStyle="red";
context.lineWidth="1";
context.lineTo(scale(++k),scale(d));
context.stroke();
});
Since you are asking a completely different question in the comment section I thought of put it another answer.
Comments and working code inline.
var margin = {top: 20, right: 20, bottom: 20, left: 40},
w = 100 - margin.left - margin.right,
h = 100 - margin.top - margin.bottom;
var canvas = d3.select("canvas")
.node();
var context = canvas.getContext('2d');
var data = d3.range(11).map(function(){return Math.random()*10})
var x = d3.scale.linear().domain([0, 10]).range([0, 700]);
var y = d3.scale.linear().domain([0, 10]).range([10, 290]);
var line = d3.svg.line()
.interpolate("cardinal")
.x(function(d,i) {console.log(x(i));return x(i);})
.y(function(d) {return y(d);})
//making a dummy SVG
var path = d3.select("body").append("svg").append("path")
.attr("d", line(data))
.attr("stroke", "steelblue")
.attr("stroke-width", "2")
.attr("fill", "none").remove();
d3.select("body svg").remove();
//going from 0 to the paths total length and storing all the points
var points = [];
for(var i =0; i < path.node().getTotalLength(); i++){
points.push(path.node().getPointAtLength(i));//store point # that length
}
var id = window.setInterval(function(){
console.log("Doing")
var point = points.shift();//take each point
context.strokeStyle="red";
context.lineWidth="1";
context.lineTo(point.x,point.y);
context.stroke();
if(points.length <= 0){
console.log("Finished")
window.clearInterval(id);//clear the interval since the drawing is complete
}
}, 10)
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" href="style.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.10/d3.js"></script>
</head>
<body>
<canvas id="myCanvas" width="500" height="500" style="border:1px solid #000000;">
</canvas>
<script src="script.js"></script>
</body>
</html>
Working code on Plunker.
The problem is here:
data.forEach(function(d,i) {
context.strokeStyle="red";
context.lineWidth="1";
context.lineTo(scale(++k),scale(d));
context.stroke();//this should be out of the for loop you should be doing it once not everytime
});
Something like this:
data.forEach(function(d,i) {
context.strokeStyle="red";
context.lineWidth="1";
var j = scale(d);
var m = scale(d++);
context.lineTo(j,m);
});
context.stroke();
Working code here
Hope this helps!
Related
I am trying to create a simple plot that is zoomable, pannable, and dynamically resizes with the window. In this case I only want it be to zoomable and pannable on the x axis. I'm using d3 v4.
The main issue I have run into is that the domain of the bottom axis will shift/slide when the page is being resized after the plot has been transformed. On resizing without having first transformed the plot, the bottom axis behaves as desired and maintains its domain, just "compressing" and "expanding" on resize.
To see the desired behavior, you can resize the plot without first transforming the plot by zooming or panning, and you'll see that the bottom axis maintains its domain.
To see the undesirable behavior, you can pan or zoom the plot and then resize the page. You will notice that the domain of the bottom axis will slide/shift.
I suspect there is something I'm doing wrong with setting my scales, however I am quite stuck.
The snippet below is a minimum working example of what I am working with:
window.addEventListener("resize", redraw);
let parentRect = $("#i3061007142472").parent()[0].getBoundingClientRect();
let pWidth = parentRect.width;
let pHeight = parentRect.height;
var margin = {
'top': 10,
'right': 30,
'bottom': 30,
'left': 60
},
width = pWidth - margin.left - margin.right,
height = pHeight - margin.top - margin.bottom;
var svg = d3.select("#i3061007142472")
.attr("width", width)
.attr("height", height + 20);
var zoom = d3.zoom();
zoom.on("zoom", zoom_handler_dataplot);
d3.select("#i3061007142472").call(zoom);
var x_i3061007142472 = d3.scaleTime()
.domain([new Date('0'), new Date('10')])
.range([60, width]);
x_i3061007142472.type = "time";
var svg_gX = svg.append("g")
.attr("class", "xAxis dataplot")
.attr("transform", `translate(0, ${height})`)
svg_gX.call(d3.axisBottom(x_i3061007142472).ticks(5));
var y_i3061007142472 = d3.scaleLinear()
.domain([0, 10])
.range([height, 0]);
y_i3061007142472.type = "linear";
let vd_xaxis_i3061007142472 = x_i3061007142472.copy();
let vd_yaxis_i3061007142472 = y_i3061007142472.copy();
var svg_gY = svg.append("g")
.attr("class", "yAxis dataplot")
.style("padding-left", "50px")
.attr("transform", "translate(" + 60 + ", 0)")
.call(d3.axisLeft(y_i3061007142472).ticks(5));
allow_pan_dir = "x";
domain = x_i3061007142472.domain();
function zoom_handler_dataplot() {
if (allow_pan_dir != "none") {
if (allow_pan_dir == "x" || allow_pan_dir == "both") {
vd_xaxis_i3061007142472 = d3.event.transform.rescaleX(x_i3061007142472);
domain = vd_xaxis_i3061007142472.domain()
svg_gX.call(d3.axisBottom(vd_xaxis_i3061007142472));
} else {
vd_xaxis_i3061007142472 = x_i3061007142472;
}
if (allow_pan_dir == "y" || allow_pan_dir == "both") {
vd_yaxis_i3061007142472 = d3.event.transform.rescaleY(y_i3061007142472);
svg_gY.call(d3.axisLeft(vd_yaxis_i3061007142472));
} else {
vd_yaxis_i3061007142472 = y_i3061007142472;
}
}
}
function redraw() {
console.warn("calling redraw");
console.warn(allow_pan_dir);
chartWidth = parseInt(d3.select(".dataplot_i3061007142472").style("width"));
chartHeight = parseInt(d3.select(".dataplot_i3061007142472").style("height"));
height = chartHeight - margin.top - margin.bottom + 20;
x_i3061007142472.range([60, chartWidth]);
y_i3061007142472.range([chartHeight - 30 - 10 + 20, 0]);
if (allow_pan_dir == "x" || allow_pan_dir == "both") {
vd_xaxis_i3061007142472 = d3.zoomTransform(d3.select("#i3061007142472").node()).rescaleX(x_i3061007142472);
svg_gX.call(d3.axisBottom(vd_xaxis_i3061007142472));
} else {
vd_xaxis_i3061007142472 = x_i3061007142472;
}
if (allow_pan_dir == "y" || allow_pan_dir == "both") {
vd_yaxis_i3061007142472 = d3.zoomTransform(d3.select("#i3061007142472").node()).rescaleY(y_i3061007142472);
svg_gY.call(d3.axisLeft(vd_yaxis_i3061007142472));
} else {
vd_yaxis_i3061007142472 = y_i3061007142472;
}
let currTransform = d3.zoomTransform(d3.select('#i3061007142472').node());
d3.select("#i3061007142472").call(zoom.transform, currTransform);
}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="./styles.css ">
<title>Document</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.0.0/d3.min.js" integrity="sha512-il/oXcqETt5LGGmWWbOZLxrDgQQXlr7+ZI37ksA//3e9mxFloAOzlkF8SqtMOlWL6zTCk8hPYUQnreiQvT4elQ==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<script src="https://code.jquery.com/jquery-3.6.3.min.js" integrity="sha256-pvPw+upLPUjgMXY0G+8O0xUf+/Im1MZjXxxgOcBQBXU=" crossorigin="anonymous"></script>
</head>
<body>
<div id="my_plot" style="display: flex; flex: 1;">
<svg id="i3061007142472" class="dataplot_i3061007142472 dataplot"></svg>
</div>
<script src="./app.js"></script>
</body>
</html>
I tried changing the domain on resize to match the transformed, tried using an intermediary scale, tried modifying the reference scale.
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.
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>
I'm working on a very basic bar plot with dimple.js. When I render in the browser, it shows only the axes and axis labels, but no bars. Any help appreciated. I am using python to create a localhost.
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<link href="main.css" rel="stylesheet">
<script src="http://d3js.org/d3.v3.min.js"></script>
<script src="http://dimplejs.org/dist/dimple.v2.0.0.min.js"></script>
</head>
<body>
<h2> Rise of the Machines: R&D on Unmanned Aerial Aircrafts</h2>
<script src="index.js"></script>
</body>
</html>
index.js
function draw(data) {
/*
D3.js setup code
*/
var margin = 75;
var width = 1400 - margin;
var height = 600 - margin;
/* Find body tag, append svg, and add chart group tag*/
var svg = d3.select("body")
.append("svg")
.attr("width", width + margin)
.attr("height", height + margin)
.append("g")
.attr("class","chart");
/*
Dimple.js Chart construction code
*/
var myChart = new dimple.chart(svg, data);
x = myChart.addCategoryAxis("x", "");
y = myChart.addMeasureAxis("y", "Share of Global Spending (%)");
myChart.addSeries(null, dimple.plot.bar);
myChart.draw();
};
d3.csv("data.csv", draw);
You'd not tell to dimple where to get the data:
x = myChart.addCategoryAxis("x", ""); //<---- X values
y = myChart.addMeasureAxis("y", "Share of Global Spending (%)"); // <--- Y values
example
var data = [
{ "Word":"Hello", "Awesomeness":2000 },
{ "Word":"World", "Awesomeness":3000 }
];
chart.addCategoryAxis("x", "Word");
chart.addMeasureAxis("y", "Awesomeness");
another example
var data = [
{ "data1":"Hello", "data2":2000 },
{ "data1":"World", "data2":3000 }
];
chart.addCategoryAxis("x", "data1");
chart.addMeasureAxis("y", "data2");
You must point to data origin, from csv column name. With your csv:
Xdata, Yvalue
1,10
2,20
3,30
4,20
Your code:
chart.addCategoryAxis("x", "Xdata");
chart.addMeasureAxis("y", "Yvalue");
On the dimple website, it seems to be recommended procedure to use .tsv (tab seperated values) as opposed to comma seperated values.
Here is a sample (vertical) bar chart code taken from the site
<html>
<div id="chartContainer">
<script src="/lib/d3.v3.4.8.js"></script>
<script src="http://dimplejs.org/dist/dimple.v2.2.0.min.js"></script>
<script type="text/javascript">
var svg = dimple.newSvg("#chartContainer", 590, 400);
d3.tsv("/data/example_data.tsv", function (data) {
var myChart = new dimple.chart(svg, data);
myChart.setBounds(60, 30, 510, 330)
myChart.addCategoryAxis("x", ["Price Tier", "Channel"]);
myChart.addMeasureAxis("y", "Unit Sales");
myChart.addSeries("Channel", dimple.plot.bar);
myChart.addLegend(65, 10, 510, 20, "right");
myChart.draw();
});
</script>
</div>
</html>
Hopefully, this will guide you in the right direction.
I'm doing some tests with d3.js regarding zooming. At the moment, I have successfully implemented geometric zoom in my test, but it has a drawback: the elements under the zoomed g are being scaled. As I understood it, this could be solved by using semantic zooming.
The problem is that I need scale in my test, as I'm syncing it with a jQuery.UI slider value.
On the other hand, I would like the text elements being resized to maintain their size after a zoom operation.
I have an example of my current attempt here.
I'm having trouble changing my code to fit this purpose. Can any one share some insight/ideas?
For your solution I have merged 2 examples:
Semantic Zooming
Programmatic Zooming
Code snippets:
function zoom() {
text.attr("transform", transform);
var scale = zoombehavior.scale();
//to make the scale rounded to 2 decimal digits
scale = Math.round(scale * 100) / 100;
//setting the slider to the new value
$("#slider").slider( "option", "value", scale );
//setting the slider text to the new value
$("#scale").val(scale);
}
//note here we are not handling the scale as its Semantic Zoom
function transform(d) {
//translate string
return "translate(" + x(d[0]) + "," + y(d[1]) + ")";
}
function interpolateZoom(translate, scale) {
zoombehavior
.scale(scale)//we are setting this zoom only for detecting the scale for slider..we are not zoooming using scale.
.translate(translate);
zoom();
}
var slider = $(function() {
$("#slider").slider({
value: zoombehavior.scaleExtent()[0],//setting the value
min: zoombehavior.scaleExtent()[0],//setting the min value
max: zoombehavior.scaleExtent()[1],//settinng the ax value
step: 0.01,
slide: function(event, ui) {
var newValue = ui.value;
var center = [centerX, centerY],
extent = zoombehavior.scaleExtent(),
translate = zoombehavior.translate(),
l = [],
view = {
x: translate[0],
y: translate[1],
k: zoombehavior.scale()
};
//translate w.r.t the center
translate0 = [(center[0] - view.x) / view.k, (center[1] - view.y) / view.k];
view.k = newValue;//the scale as per the slider
//the translate after the scale(so we are multiplying the translate)
l = [translate0[0] * view.k + view.x, translate0[1] * view.k + view.y];
view.x += center[0] - l[0];
view.y += center[1] - l[1];
interpolateZoom([view.x, view.y], view.k);
}
});
});
I am zooming w.r.t. 250,250 which is the center of the clip circle.
Working code here (have added necessary comments)
Hope this helps!
To do what you want, you need to refactor the code first a little bit. With d3, it is good practice to use data() to append items to a selection, rather than using for loops.
So this :
for(i=0; i<7; i++){
pointsGroup.append("text")
.attr("x", function(){
var plusOrMinus = Math.random() < 0.5 ? -1 : 1;
var randx = Math.random();
return Math.floor(plusOrMinus*randx*75)+centerx;
})
.attr("y", function(){
var plusOrMinus = Math.random() < 0.5 ? -1 : 1;
var randy = Math.random();
return Math.floor(plusOrMinus*randy*75)+centery;
})
.html("star")
.attr("class", "point material-icons")
.on("click", function(){console.log("click!");});
}
Becomes this
var arr = [];
for(i=0; i<7; i++){
var plusOrMinus = Math.random() < 0.5 ? -1 : 1;
var randx = Math.random();
var x = Math.floor(plusOrMinus*randx*75)+centerx;
var plusOrMinus = Math.random() < 0.5 ? -1 : 1;
var randy = Math.random();
var y = Math.floor(plusOrMinus*randy*75)+centery;
arr.push({"x":x,"y":y});
}
pointsGroup.selectAll("text")
.data(arr)
.enter()
.append("text")
.attr("x", function(d,i){
return d.x;// This corresponds to arr[i].x
})
.attr("y", function(d,i){
return d.y;// This corresponds to arr[i].y
})
.html("star")
.attr("class", "point material-icons")
.on("click", function(){console.log("click!");});
This way, you can access individual coordinates using for instance.attr("x",function(d,i){ //d.x is equal to arr.x, i is item index in the selection});
Then, to achieve your goal, rather than changing the scale of your items, you should use a linear scale to change each star position.
First, add linear scales to your fiddle and apply them to the zoom:
var scalex = d3.scale.linear();
var scaley = d3.scale.linear();
var zoom = d3.behavior.zoom().x(scalex).y(scaley).scaleExtent([1, 5]).on('zoom', onZoom);
And finally on zoom event apply the scale to each star's x and y
function onZoom(){
d3.selectAll("text")
.attr("x",function(d,i){
return scalex(d.x);
})
.attr("y",function(d,i){
return scaley(d.y);
});
}
At this point, zoom will work without the slider. To add the slider, simply change manually the zoom behavior scale value during onSlide event, then call onZoom.
function onSlide(scale){
var sc = $("#slider").slider("value");
zoom.scale(sc);
onZoom();
}
Note: I used this config for the slider:
var slider = $(function() {
$( "#slider" ).slider({
value: 1,
min: 1,
max: 5,
step: 0.1,
slide: function(event, ui){
onSlide(5/ui.value);
}
});
});
Please note that at this point the zoom from the ui is performed relative to (0,0) at this point, and not your "circle" window center. To fix it I simplified the following function from the programmatic example, that computes valid translate and scale to feed to the zoom behavior.
// To handle center zooming
var width = 500;
var height = 600;
function zoomClick(sliderValue) {
var center = [width / 2, height / 2],
extent = zoom.scaleExtent(),
translate = zoom.translate();
var view = {
x: zoom.translate()[0],
y: zoom.translate()[1],
k: zoom.scale()
};
var target_zoom = sliderValue;
if (target_zoom < extent[0] || target_zoom > extent[1]) {
return false;
}
var translate0 = [(center[0] - view.x) / view.k, (center[1] - view.y) / view.k];
view.k = target_zoom;
var l = [];
l = [translate0[0] * view.k + view.x, translate0[1] * view.k + view.y];
view.x += center[0] - l[0];
view.y += center[1] - l[1];
// [view.x view.y] is valid translate
// view.k is valid scale
// Then, simply feed them to the zoom behavior
zoom
.scale(view.k)
.translate([view.x, view.y]);
// and call onZoom to update points position
onZoom();
}
then just change onSlide to use this new function every time slider moves
function onSlide(scale){
var sc = $("#slider").slider("value");
zoomClick(sc);
}
Full snippet
function onZoom(){
d3.selectAll("text")
.attr("x",function(d,i){
return scalex(d.x);
})
.attr("y",function(d,i){
return scaley(d.y);
});
}
function onSlide(scale){
var sc = $("#slider").slider("value");
zoomClick(sc);
}
var scalex = d3.scale.linear();
var scaley = d3.scale.linear();
var zoom = d3.behavior.zoom().x(scalex).y(scaley).scaleExtent([1, 5]).on('zoom', onZoom);
var svg = d3.select("body").append("svg")
.attr("height", "500px")
.attr("width", "500px")
.call(zoom)
.on("mousedown.zoom", null)
.on("touchstart.zoom", null)
.on("touchmove.zoom", null)
.on("touchend.zoom", null);
var centerx = 250,
centery = 250;
var circleGroup = svg.append("g")
.attr("id", "circleGroup");
var circle = circleGroup.append("circle")
.attr("cx", "50%")
.attr("cy", "50%")
.attr("r", 150)
.attr("class", "circle");
var pointsParent = svg.append("g").attr("clip-path", "url(#clip)").attr("id", "pointsParent");
var pointsGroup = pointsParent.append("g")
.attr("id", "pointsGroup");
var arr = [];
for(i=0; i<7; i++){
var plusOrMinus = Math.random() < 0.5 ? -1 : 1;
var randx = Math.random();
var x = Math.floor(plusOrMinus*randx*75)+centerx;
var plusOrMinus = Math.random() < 0.5 ? -1 : 1;
var randy = Math.random();
var y = Math.floor(plusOrMinus*randy*75)+centery;
arr.push({"x":x,"y":y});
}
pointsGroup.selectAll("text")
.data(arr)
.enter()
.append("text")
.attr("x", function(d,i){
return d.x;// This corresponds to arr[i].x
})
.attr("y", function(d,i){
return d.y;// This corresponds to arr[i].y
})
.html("star")
.attr("class", "point material-icons")
.on("click", function(){console.log("click!");});
zoom(pointsGroup);
var clip = svg.append("defs").append("svg:clipPath")
.attr("id", "clip")
.append("svg:circle")
.attr("id", "clip-circ")
.attr("cx", centerx)
.attr("cy", centery)
.attr("r", 149);
var slider = $(function() {
$( "#slider" ).slider({
value: 1,
min: 1,
max: 5,
step: 0.1,
slide: function(event, ui){
onSlide(5/ui.value);
}
});
});
// To handle center zooming
var width = 500;
var height = 600;
function zoomClick(sliderValue) {
var target_zoom = 1,
center = [width / 2, height / 2],
extent = zoom.scaleExtent(),
translate = zoom.translate();
var view = {x: zoom.translate()[0], y: zoom.translate()[1], k: zoom.scale()};
target_zoom = sliderValue;
if (target_zoom < extent[0] || target_zoom > extent[1]) { return false; }
var translate0 = [];
translate0 = [(center[0] - view.x) / view.k, (center[1] - view.y) / view.k];
view.k = target_zoom;
var l = [];
l = [translate0[0] * view.k + view.x, translate0[1] * view.k + view.y];
view.x += center[0] - l[0];
view.y += center[1] - l[1];
zoom
.scale(view.k)
.translate([view.x, view.y]);
onZoom();
}
body {
font: 10px sans-serif;
}
text {
font: 10px sans-serif;
}
.circle{
stroke: black;
stroke-width: 2px;
fill: white;
}
.point{
fill: goldenrod;
cursor: pointer;
}
.blip{
fill: black;
}
#slider{
width: 200px;
margin: auto;
}
<!DOCTYPE html>
<html>
<head>
<link href="https://fonts.googleapis.com/icon?family=Material+Icons"
rel="stylesheet">
<link href="https://code.jquery.com/ui/1.11.4/themes/smoothness/jquery-ui.css" rel="stylesheet" type="text/css" />
<script src="https://code.jquery.com/jquery-1.11.3.js"></script>
<script src="https://code.jquery.com/ui/1.11.4/jquery-ui.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.6/d3.min.js"></script>
<meta charset=utf-8 />
<title>d3.JS slider zoom</title>
</head>
<body>
<div id="slider"></div>
</body>
</html>