Relative positioning of generated svg objects in D3 - javascript

Let me start with the context:
I'm using D3 to generate randomized worksheets for my wife's students who are learning their numbers from 11 to 99. It creates columns of 10 circles to represent the tens place, and a single column of 1-9 circles for the units place.
I've been able to successfully build all of the objects I need, and it randomizes on refresh, which is perfect for my needs at this point, but I'm having trouble with alignment.
Here's the (somewhat messy) example: CodePen - Montessori Number Generator
The concept is based on this example
I'd prefer the columns to be centred in the space, but I've not yet been able to get the math to work out for it (I've ended up aligning the tens to the left and the units to the right).
The crucial equation I'm trying to figure out is for the cx value on the generated circles.
For the Tens group:
var tens = d3.range(10).map(function(r, d) {return {
radius: 15,
cx: svgWidth / 2 - r*50 + 9 * (r + 4),
cy: d * 30 + 72 }})
And the cx value for the Units is calculated using the width of the Tens group:
var w = d3.select('#tens')[0][0].getBBox().width;
for (var j = 0; j < units.length; j++) {
gUnits.append("circle")
.attr("cx", ( (svgWidth + w ) / 2 + w/45 ) )
.attr("cy", units[j].cy)
.attr("r", units[j].radius)
.attr("fill", "white")
.style("stroke", "black")
.style("stroke-width", 3);
}
The example works reasonably well for mid-range numbers (40-60 range), but smaller numbers cause overlap, and larger numbers get pushed off the side of the canvas, and none are actually perfectly centred.
It's also possible that I've gone about this all wrong, and there's a much simpler solution that I'm just not seeing.
If there is, the other big two requirements that I need to meet for my wife is this: there must be some space between the tens group and the units group, and the final number can't be a multiple of 10 (i.e. the units group cannot be zero).
Thanks for any thoughts!

Here is how I would do it.
//generate a number between 11 and 99
var randNumber = Math.round(Math.random() * 90 + 11)
//Get difference when divide rand number by 10
var circleUnits = Math.round(randNumber%10);
//get how many 10's you can fit in what is left over
var circleTens = Math.round(randNumber-circleUnits)/10 ;
//use this data to generate arrays to work with
var circleUnitsData = d3.range(circleUnits).map(function(j, i) {return {
count : i,
radius: 15
}})
var circleTensData = d3.range(circleTens).map(function(j, i) {return {
count : i+1,
radius: 15
}})
Here is how I generated the circles for the tens. Because there are always going to be 10 circles in each column (that's why I divided it by 10 above) :
var nodeRadius = 15;
var tensContainer = gTens.selectAll('circle') //put this outside the foreach so you don't overwrite any other circles
var difference = 5; //gap between circles
circleTensData.forEach(function(d ,i){
tensContainer.data([1,2,3,4,5,6,7,8,9,10])
.enter()
.append('circle')
.attr("cx",function(e,j) {
// console.log(e)
return 100 + i * nodeRadius*2 + (i*difference) //use i here to get what set of 10 it is in
})
.attr("cy", function(e,j) {
return 100 + j * nodeRadius*2 + (j*difference) //use j here to increment downwards
})
.attr("r", function(e) {
return nodeRadius //hard coded radius above
})
.attr("fill", "white")
.style("stroke", "black")
.style("stroke-width", 3);
})
Then the units are even easier. To work out the x position I multiplied the size of the 10's dataset by the width of the node plus the difference and added 40 to give it an offset. So every time you refresh, the units are 40 units to the right :
var circlesUnits = gUnits.selectAll('circle')
.data(circleUnitsData)
.enter()
.append('circle')
.attr("cx", function(d) {
var i = circleTensData.length;
console.log(400 + i * nodeRadius*2 + (i*difference))
return 140 + i * nodeRadius*2 + (i*difference)
})
.attr("cy", function(d, i) {
return 100 + i * nodeRadius*2 + (i *difference)
})
.attr("r", function(d) {
return d.radius
})
.attr("fill", "white")
.style("stroke", "black")
.style("stroke-width", 3);
On refresh I added an alert to show you the random number.
If I have misread the question, or have any questions, please feel free to comment and I will adjust. Hopefully this helps you out :)
Update fiddle : https://jsfiddle.net/thatOneGuy/8hc0sfbg/1/
EDIT
You say you don't want the number to end in 0. The following will work :
function getRand() {
var randNumber = Math.round(Math.random() * 90 + 11) // number between 0-99
if (randNumber % 10 == 0) { //if ends in 0 run again
return getRand();
}
return randNumber;
}
I have wrapped the code in a function so you can call it when you want. So the above code will run when calling the new function. Click the button 'Click Me' to run the function
Updated fiddle : https://jsfiddle.net/thatOneGuy/8hc0sfbg/5/
EDIT
You wish to center the circles. I would remove any starting movements that you set, i.e when setting the cx and cy like so :
return 100 + i * nodeRadius*2 + (i*difference)
Just get rid of the 100 which resets it. I would then translate it half of the width and remove half the width of the tens and units bounding box. Which should center it. Something like so :
var circleContainer = d3.select('#circleContainer')
var circleTensContainer = d3.select('#tens')
var circleUnitsContainer = d3.select('#units')
var tensBBox = circleTensContainer.node().getBBox();
var unitsBBox = circleUnitsContainer.node().getBBox();
var containerSize = 40 + tensBBox.width + unitsBBox.width;
var thisMovX = svgWidth/2 - containerSize/2;
circleContainer.attr('transform', 'translate(' + thisMovX + ',0)' )
You can do something similar if you want the y centered too. I have also changed where you draw the lines too. Hope this helps :)
Updated fiddle : https://jsfiddle.net/thatOneGuy/8hc0sfbg/8/

Why you don't use D3 powerful data management?
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.17/d3.min.js" charset="utf-8"></script>
<style>
svg {
width:480px;
height:600px;
margin:auto 50%;
}
circle { stroke:#ccc;stroke-width:1px; }
.units { fill:#09C; }
.tens { fill:#F00; }
</style>
</head>
<body>
<script>
var tens = [], units=[], radio=40;
for (i=0;i<Math.random()* 90 + 10;i++) tens.push(i); // [10-99]
for (i=0;i<Math.random()* 10;i++) units.push(i); // [0-9]
var svg = d3.select("body").append("svg")
var units = svg.selectAll("circle.units") // draw units
.data(units, function (d) {return d; })
units.enter().append("circle")
.attr("class", "units")
.attr("cx", function (d) {return radio + Math.floor( d / 10 )*radio })
.attr("cy", function (d) {return radio + d % 10 * radio} )
.attr("r", radio/2)
var tens = svg.selectAll("circle.tens") // draw tens
.data(tens, function (d) {return d; })
tens.enter().append("circle")
.attr("class", "tens")
.attr("cx", function (d) {return radio + Math.floor( d / 10 )*radio })
.attr("cy", function (d) {return radio + d % 10 * radio} )
.attr("r", radio/2)
.attr("transform","translate("+radio*2+", 0)")
svg.append('line') //draw lines
.attr('x1', 0)
.attr('y1', 480)
.attr('x2', 80)
.attr('y2', 480)
.attr('stroke', 'black')
.attr('stroke-width', 3);
svg.append('line')
.attr('x1', 110)
.attr('y1', 480)
.attr('x2', 280)
.attr('y2', 480)
.attr('stroke', 'black')
.attr('stroke-width', 3);
</script>
</body>
</html>
Here de jsfiddle: https://jsfiddle.net/50j9wv2w/

Related

How to make drawing data on a d3 map more efficient JavaScript

I am creating a project at the moment where I need to draw large amounts of data on a d3 map. My current solution operates but is quite sluggish at times especially on less powerful CPU's. I am looking for a way to make my generated data points more efficient. I attempted to simulate a fire on a map.
Do y'all have any suggestions on how to make this generation more efficient and reduce lag?
function drawPlot(data) {
//draw the plot again
var locations = svg.selectAll(".location")
.data(data);
//Add placeholder image in tooltip
var string = "<img src= "+ yourImagePath +" />";
// if filtered dataset has more circles than already existing, transition new ones in
var dots = locations.enter()
.append("circle")
.attr("class", "location orange flame")
.attr("cx", function(d){ return projection([parseFloat(d.longitude) + ((Math.random() > 0.5) ? ((-1) * Math.random()) / 2 : Math.random() / 2),parseFloat(d.latitude)+0.5])[0] })
.attr("cy", function(d){ return projection([d.longitude,parseFloat(d.latitude) + 0.5 + ((Math.random() > 0.5) ? ((-1) * Math.random()) / 2 : Math.random() / 2)])[1] })
.style("fill", function(d) { return myScale2(d.incident_date_created)})
.style("stroke", function(d) { return myScale2(d.incident_date_created)})
.style("opacity", function(d) {return 2*myScale1(d.fire_duration)})
dots.attr("r", function(d){if(formatDate(d.incident_date_created)==handle.attr("text")){return myScale(20*d.incident_acres_burned)} else{return 0}})
.transition()
.duration(function (d) {
if (d.fire_duration < 200) {
d.fire_duration = 200
} else if (d.fire_duration > 1000) {
d.fire_duration = 1000
}
return d.fire_duration* SPEED_CONSTANT / MULTI
})
.attr("r", 25)
.transition()
.attr("r", 0)
Thanks :)

d3 chord: center text on circle

I use the d3 chord diagram example of Andrew and want to center all text labels within the curved slice. I tried many things but was never able to center the texts. Do you know what wizzard trick there is needed?
var width = 720,
height = 720,
outerRadius = Math.min(width, height) / 2 - 10,
innerRadius = outerRadius - 24;
var formatPercent = d3.format(".1%");
var arc = d3.svg.arc()
.innerRadius(innerRadius)
.outerRadius(outerRadius);
var layout = d3.layout.chord()
.padding(.04)
.sortSubgroups(d3.descending)
.sortChords(d3.ascending);
var path = d3.svg.chord()
.radius(innerRadius);
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height)
.append("g")
.attr("id", "circle")
.attr("transform", "translate(" + width / 2 + "," + height / 2 + ")");
svg.append("circle")
.attr("r", outerRadius);
d3.csv("ex_csv.csv", function(cities) {
d3.json("ex_json.json", function(matrix) {
// Compute the chord layout.
layout.matrix(matrix);
// Add a group per neighborhood.
var group = svg.selectAll(".group")
.data(layout.groups)
.enter().append("g")
.attr("class", "group")
.on("mouseover", mouseover);
// Add the group arc.
var groupPath = group.append("path")
.attr("id", function(d, i) { return "group" + i; })
.attr("d", arc)
.style("fill", function(d, i) { return cities[i].color; });
// Add a text label.
var groupText = group.append("text")
.attr("x", 6)
.attr("dy", 15);
groupText.append("textPath")
.attr("xlink:href", function(d, i) { return "#group" + i; })
.text(function(d, i) { return cities[i].name; });
// Remove the labels that don't fit. :(
groupText.filter(function(d, i) { return groupPath[0][i].getTotalLength() / 2 - 16 < this.getComputedTextLength(); })
.remove();
// Add the chords.
var chord = svg.selectAll(".chord")
.data(layout.chords)
.enter().append("path")
.attr("class", "chord")
.style("fill", function(d) { return cities[d.source.index].color; })
.attr("d", path);
}
});
});
</script>
As an aside, I would suggest looking to upgrade to v4, documentation for v2 is nearly non-existent and is very hard to help with.
You can set both the text-anchor and the startOffset property to achieve what you are looking for.
First, you'll want to set text-anchor to middle as it is easier to specify the middle point than to find the middle point and work back to find where the text should start.
Second you'll need to set a startOffset. Note that if you use 50%, the text will not appear where you want, as the total length of the text path is all sides of the closed loop (chord anchor) you are appending to. Setting it to 25 % would work if you did not have a different outer and inner radius. But, as you have an outer radius that is 24 pixels greater than the inner radius you can try something like this to calculate the number of pixels you need to offset the center of the text:
groupText.append("textPath")
.attr("xlink:href", function(d, i) { return "#group" + i; })
.text(function(d, i) { return cities[i].name; })
.attr("startOffset",function(d,i) { return (groupPath[0][i].getTotalLength() - 48) / 4 })
.style("text-anchor","middle");
I subtract 48 because the sides of the anchor are 24 pixels each (the difference in the radii). I divide by four because the path doubles back on itself. If it was a general line I would just divide by two.
This approach is a little simplistic as the outer circumference is not the same as the inner circumference of each chord anchor, so I am off by a little bit, but it should be workable.
For labels that are on the cusp of being displayed, this will be awkward: the inner radius is shorter, so the formula for deteriming if a string is short enough to be displayed may be wrong - which may lead to some characters climbing up the side of the anchor (your example also 16 pixels as the difference in radii to calculate if text is too long, rather than 24).
This is the end result:
Here is a demonstration.

dimplejs scatterplot showing up differently second time generated on page

I have a function that creates three scatterplots using dimplejs on the same page. I pass in a number of variables you can see in the code below. The first scatterplot generates perfect, but the second and third have issues with the X Axis dates not turning vertical, and the y axis description gets moved closer to the axis and stuck behind the details.
Here is a jfiddle of the data to visualize what it looks like...the first one is good, the second not so much. http://jsfiddle.net/4VU7w/
Is this an issue with dimplejs?
....blah blah blah...
print_dimple_scatterplot_time($speco_arrival_times,$div_to_display_in,'date', 'Arrival Time',
'Date of Arrival','Truck Arrival Time','Inbound Trucks to '.$site_name,'Circles Represent Truck Arrival Times');
function print_dimple_scatterplot_time($source_array,$location_div,$x_key_field_name, $y_key_field_name,
$xAxis_name,$yAxis_name,$chart_title,$legend_desc){
/*****Example
$x_key_field_name = 'date'
$y_key_field_name = 'Arrival Time'
$legend_desc = 'Circles Represent Truck Arrival Time'
*******/
$insert_string = json_encode($source_array);
$to_print_string = <<<EOT
<script type="text/javascript">
var data = {$insert_string};
var svg = dimple.newSvg("#{$location_div}", 900, 400);
// Draw a standard chart using the aggregated data
var chart = new dimple.chart(svg, data);
chart.setBounds(100, 50, 800, 250) //(x,y,width,height)
var x = chart.addCategoryAxis("x", "{$x_key_field_name}");
x.addOrderRule("date");
x.showGridlines = true; //add vertical grid lines
var y = chart.addTimeAxis("y", "{$y_key_field_name}", "%H:%M", "%H:%M");
y.timePeriod = d3.time.hours;
y.timeInterval = 1;
y.showGridlines = true; //add horizontal grid lines
y.overrideMin = d3.time.format("%H:%M").parse("00:00");
y.overrideMax = d3.time.format("%H:%M").parse("23:59");
// Override color
chart.defaultColors = [
new dimple.color("#0000A0")
];
var s = chart.addSeries("date", dimple.plot.bubble);
chart.draw();
//added so zeros don't show up.
s.shapes.style("opacity", function (d) {
return (d.y === "" ? 0 : 0.6);
});
x.shapes.selectAll("text").attr("transform",
function (d) {
return d3.select(this).attr("transform") + " translate(0, 0) rotate(0)"; //translate(x left and right, y up and down)
});
x.titleShape.text("{$xAxis_name}"); //name x axis
x.titleShape.attr("y",chart.height+125); //move where x-axis is
y.titleShape.text("{$yAxis_name}"); //name y axis
y.titleShape.attr("y", 175); //move where y-axis is
//Add Title
svg.append("text")
.attr("x", 275 )//(width / 2))
.attr("y", 20) //0 - (margin.top / 2))
.attr("text-anchor", "middle")
.style("font-size", "20px")
.style("text-decoration", "underline")
.text("{$chart_title}");
//Add Custom Legend
svg.append("circle")
.attr("cx", 500 )//(width / 2))
.attr("cy", 15) //0 - (margin.top / 2))
.attr("r",5);
//Add Customs Legend Text
svg.append("text")
.attr("x", 510 )//(width / 2))
.attr("y", 20) //0 - (margin.top / 2))
.attr("text-anchor", "left")
.style("font-size", "12px")
.style("text-decoration", "underline")
.text("{$legend_desc}");
</script>
EOT;
echo $to_print_string;
}
It looks a lot like this issue: https://github.com/PMSI-AlignAlytics/dimple/issues/34
I can't see in your code whether this is the case but if so the workaround is discussed in the issue.

Linear cx scaling with javascript objects in D3

I am attempting to plot a simple dataset consisting of an array of javascript objects. Here is the array in JSON format.
[{"key":"FITC","count":24},{"key":"PERCP","count":16},{"key":"PERCP-CY5.5","count":16},{"key":"APC-H7","count":1},{"key":"APC","count":23},{"key":"APC-CY7","count":15},{"key":"ALEXA700","count":4},{"key":"E660","count":1},{"key":"ALEXA647","count":17},{"key":"PE-CY5","count":4},{"key":"PE","count":38},{"key":"PE-CY7","count":18}]
Each object simply contains a String: "key", and a Integer: "count".
Now, I am plotting these in D3 as follows.
function key(d) {
return d.key;
}
function count(d) {
return parseInt(d.count);
}
var w = 1000,
h = 300,
//x = d3.scale.ordinal()
//.domain([count(lookup)]).rangePoints([0,w],1);
//y = d3.scale.ordinal()
//.domain(count(lookup)).rangePoints([0,h],2);
var svg = d3.select(".chart").append("svg")
.attr("width", w)
.attr("height", h);
var abs = svg.selectAll(".little")
.data(lookup)
.enter().append("circle")
.attr("cx", function(d,i){return ((i + 0.5)/lookup.length) * w;})
.attr("cy", h/2).attr("r", function(d){ return d.count * 1.5})
Here is what this looks like thus far.
What I am concerned about is how I am mapping my "cx" coordinates. Shouldn't the x() scaling function take care of this automatically, as opposed to scaling as I currently handle it? I've also tried .attr("cx", function(d,i){return x(i)}).
What I eventually want to do is label these circles with their appropriate "keys". Any help would be much appreciated.
Update:
I should mention that the following worked fine when I was dealing with an array of only the counts, as opposed to an array of objects:
x = d3.scale.ordinal().domain(nums).rangePoints([0, w], 1),
y = d3.scale.ordinal().domain(nums).rangePoints([0, h], 2);
Your code is doing what you want...I just added the text part. Here is the FIDDLE.
var txt = svg.selectAll(".txt")
.data(lookup)
.enter().append("text")
.attr("x", function (d, i) {
return ((i + 0.5) / lookup.length) * w;
})
.attr("y", h / 2)
.text(function(d) {return d.key;});
I commented out the scales, they were not being used...as already noted by you.

Animating circles with D3.js

So I have been messing around with D3.js for a couple of days now and I have basic circle generation / animation (that act as bubbles), and I was wondering how I could animate the circles on the x axsis, so they wobble back forth as the travel / transition to the top of the page. The current animation can be viewed at chrisrjones.com/bubbles-v1.html
Demo of Solution:
Note the lack of symmetry:
http://jsfiddle.net/blakedietz/R5cRK/1/embedded/result/
Approach:
Determine a mathematical function that would properly model the movement that you want.
In this case we want a sine wave. We can modify aspects of each bubbles characteristics to give a unique movement pattern to each bubble.
Build or find a solution that utilizes the key concepts needed for this problem.
I like to search on bl.ocks.org/mbostock for examples that have the foundational parts of the problem that I'm trying to solve. On the site I found this example:http://bl.ocks.org/mbostock/1371412
Modify the given example to more similarly mirror the specified outcome.
Solution:
Here is a quick demo of a solution. I'll return to this to give you a full walk through. Modifications can be made to make the bubble placement and sizing as well as wiggle random/semi-unique per each bubble.
w = 960,
h = 500;
var svg = d3.select("body").append("svg:svg")
.attr("width", w)
.attr("height", h);
var circle = svg.selectAll("circle")
.data(d3.range(70).map(function(datum,interval) {
return {
x: interval*20,
y: 0,
dx: 5,
dy: -3 * (Math.random()+1),
mu: Math.random()*2
};
}))
.enter().append("svg:circle")
.attr("r", 2.5)
.attr("fill","blue")
.attr("opacity",".5");
var text = svg.append("svg:text")
.attr("x", 20)
.attr("y", 20);
var start = Date.now(),
frames = 0;
d3.timer(function()
{
// Update the FPS meter.
var now = Date.now(), duration = now - start;
text.text(~~(++frames * 1000 / duration));
if (duration >= 1000) frames = 0, start = now;
// Update the circle positions.
circle
.attr("cx", function(d) { d.x += Math.random()*3*Math.sin(Math.random()*3*d.x + Math.random()*10); if (d.x > w) d.x -= w; else if (d.x < 0) d.x += w; return d.x; })
.attr("cy", function(d) { d.y += d.dy ; if (d.y > h) d.y -= h; else if (d.y < 0) d.y += h; return d.y; })
.attr("r",function(d)
{
return (d.y < 100) ? d3.select(this).attr("r") : d.mu*500/d.y;
});
});
You can do that using custom tween function for cx:
var circlesTransition = d3.selectAll("circle")
.transition()
.duration(5000)
.attr("cy", "0")
.attrTween('cx', function (d, i, a) {
return function (t) {
// Add salt, pepper and constants as per your taste
return a + (Math.random() - 0.5) * 10;
};
});

Categories

Resources