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.
Related
Edit
Just found the post plotting 50 million points with d3.js.
Sluggish interaction with zoom and pan are due to too many elements in the svg. The key is to use hierarchical levels of detail, just like the image pyramid. , to limit the maximum elements in svg.
Original post
I am trying to read some data points from csv/excel file and plot them using d3.js.
The data set contains 100,000s of rows, each row contains a time stamp and a value at that time.
Time stamp, pressure
12/17/2019 12:00:00 AM, 600
I followed this example to plot the time-pressure chart with zoom and pan.
There is no issue and worked perfectly.
One issue is that when working with large data set, say 500,000 of data points, the interaction with the chart is sluggish.
The chart with 500,000 data points shows an overall shape, and the details would only come up when zoomed in at large scale.
When zoomed in, all the data points are re-plotted and clipped out by the clip path. Would there be some room to improve the speed?
Updated Code
function draw(res){
//clear the current content in the div
document.getElementById("spectrum-fig").innerHTML = '';
var fullwidth = d3.select('#spectrum-fig').node().getBoundingClientRect().width;
fullwidth = fullwidth < 500? 500:fullwidth;
var fullheight = 500;
var resLevelOne = getWindowed(res, 1);
var resLevelTwo = getWindowed(res, 2);
var designMax= getMaxPressureKPa();
var resMax = getPsiTopTen(res);
const SMYSKPa = getSMYSPressureKPa();
const avePsi = getAvePsi(res);
var psiRange = d3.max(res, d=>d.psi) - d3.min(res, d=>d.psi);
var resSmallChart = getWindowed(res, 2);//
//filtSpectrum(res, 0.05*psiRange); //0.05 magic numbers
//var resSmallChart = res;
//margin for focus chart, margin for small chart
var margin = {left:50, right: 50, top: 30, bottom:170},
margin2 = {left:50, right: 50, top: 360, bottom:30},
width = fullwidth - margin.left - margin.right,
height = fullheight - margin.top - margin.bottom,
height2 = fullheight - margin2.top-margin2.bottom;
//x, y, for big chart, x2, y2 for small chart
var x = d3.scaleTime().domain(d3.extent(res, d => d.Time)).range([0, width]),
x2 = d3.scaleTime().domain(d3.extent(res, d => d.Time)).range([0, width]),
y = d3.scaleLinear().domain([0, SMYSKPa]).range([height, 0]),
y2 = d3.scaleLinear().domain([0, SMYSKPa]).range([height2, 0]);
//clear the content in Spectrum-fig div before drawring
//avoid multiple drawings;
var xAxis =d3.axisBottom(x).tickFormat(d3.timeFormat("%m-%d")),
xAxis2 = d3.axisBottom(x2).tickFormat(d3.timeFormat("%b")),
yAxis = d3.axisLeft(y);
var brush = d3.brushX() // Add the brush feature using the d3.brush function
.extent( [ [0,0], [width,height2] ] ) // initialise the brush area: start at 0,0 and finishes at width,height: it means I select the whole graph area
.on("brush end", brushed); // trigger the brushed function
var zoom = d3.zoom()
.scaleExtent([1, 100]) //defined the scale extend
.translateExtent([[0, 0], [width, height]])
.extent([[0, 0], [width, height]])
.on("zoom", zoomed); //at the zoom end trigger zoomed function
//line for big chart line
var line = d3.line()
.x(function(d) { return x(d.Time) })
.y(function(d) { return y(d.psi) });
//line2 for small chart line
var line2 = d3.line()
.x(function(d) { return x2(d.Time) })
.y(function(d) { return y2(d.psi) });
var svg = d3.select("#spectrum-fig")
.append("svg")
.attr("width", fullwidth)
.attr("height", fullheight);
svg.append("defs").append("clipPath")
.attr("id", "clip")
.append("rect")
.attr("width", width)
.attr("height", height);
var focus = svg.append("g")
.attr("class", "focus")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
var context = svg.append("g")
.attr("class", "context")
.attr("transform", "translate(" + margin2.left + "," + margin2.top + ")");
focus.append("g")
.attr("class", "axis axis--x")
.attr("transform", "translate (0," + height +")")
.call(xAxis);
focus.append("g")
.attr("class", "axis axis--y")
.call(yAxis);
focus.append("g")
.attr("transform", "translate (" + width + ", 0)")
.call(d3.axisRight(y).tickFormat('').tickSize(0));
focus.append("g")
.attr("transform", "translate (0, 0)")
.call(d3.axisTop(x).tickFormat('').tickSize(0));
// Add the line
focus.insert("path")
//.datum(res)
.attr("class", "line") // I add the class line to be able to modify this line later on.
.attr("fill", "none")
.attr('clip-path', 'url(#clip)')
.attr("stroke", "steelblue")
.attr("stroke-width", 1.5)
.attr("d", line(resLevelTwo));
context.insert("path")
//.datum(resSmallChart)
.attr("class", "line")
.attr("stroke", "steelblue")
.attr("stroke-width", 1.5)
.attr("fill", "none")
.attr("d", line2(resSmallChart));
context.append("g")
.attr("class", "axis axis--x")
.attr("transform", "translate(0," + height2 + ")")
.call(xAxis2);
context.append("g")
.attr("class", "brush")
.call(brush)
.call(brush.move, x.range());
svg.append("rect")
.attr("class", "zoom")
.attr('fill', 'none')
.attr('cursor', 'move')
.attr('pointer-events', 'all')
.attr("width", width)
.attr("height", height)
.attr("transform", "translate(" + margin.left + "," + margin.top + ")")
.call(zoom);
function getWindowed(arr, level){
var windowed = new Array();
var arrLength = arr.length;
var windowSize =Math.pow(16, level); //set the window size
for(let i = 0; i * windowSize < arrLength; i++ ){ //each to be the window size
let startIndex = i * windowSize;
let endIndex = (i+1) * windowSize;
endIndex = endIndex >= arrLength ? arrLength-1 : endIndex;
let localExtreme = findLocalExtreme(arr.slice(startIndex, endIndex));
if (localExtreme.Max.Time.getTime() === localExtreme.Min.Time.getTime()){ //anything include = need getTime
windowed.push(localExtreme.Max)
}else if(localExtreme.Max.Time < localExtreme.Min.Time){
windowed.push(localExtreme.Max);
windowed.push(localExtreme.Min);
}else{
windowed.push(localExtreme.Min);
windowed.push(localExtreme.Max);
}
}
let firstElement = {...arr[0]};
let lastElement = {...arr[arr.length-1]};
if(firstElement.Time.getTime() != windowed[0].Time.getTime()){ //insert to the position zero
windowed.unshift(firstElement);
}
if(lastElement.Time.getTime() != windowed[windowed.length-1].Time.getTime()){
windowed.push(lastElement);
}//insert to the end last member;
return windowed;
}
function findLocalExtreme(slicedArr){
if(slicedArr === undefined || slicedArr.length == 0){
throw 'error: no array members';
}
let slicedArrLength = slicedArr.length;
let tempMax = {...slicedArr[0]};
let tempMin = {...slicedArr[0]};
if(slicedArrLength === 1){
return {
Max: tempMax,
Min: tempMin
}
}
for (let i = 1; i < slicedArrLength; i++){
if (slicedArr[i].psi > tempMax.psi){
tempMax = {...slicedArr[i]};
}
if (slicedArr[i].psi < tempMin.psi){
tempMin = {...slicedArr[i]};
}
}
return {
Max: tempMax,
Min: tempMin
}
}
function getDataToDraw(timeRange){ //timeRange [0,1] , [startTime, endTime]
const bisect = d3.bisector(d => d.Time).left;
const startIndex = bisect(res, timeRange[0]);
const endIndex = bisect(res, timeRange[1]);
const numberInOriginal = endIndex-startIndex+1;
const windowSize =16;
const maxNumber = 8000;
let level = Math.ceil(Math.log(numberInOriginal/maxNumber ) / Math.log(windowSize));
if(level <=0 ) level =0;
console.log(endIndex, startIndex, endIndex-startIndex+1, level);
if(level === 0){
return res.slice(startIndex, endIndex);
}if(level === 1){
let start_i = bisect(resLevelOne, timeRange[0]);
let end_i =bisect(resLevelOne, timeRange[1]);
return resLevelOne.slice(start_i, end_i);
}else { //if level 2 or higher, never happen
let start_i = bisect(resLevelTwo, timeRange[0]);
let end_i =bisect(resLevelTwo, timeRange[1]);
return resLevelTwo.slice(start_i, end_i);
}
}
function brushed() {
if (d3.event.sourceEvent && d3.event.sourceEvent.type === "zoom") return; // ignore brush-by-zoom
var s = d3.event.selection || x2.range();
x.domain(s.map(x2.invert, x2));
focus.select(".line").attr("d", line(getDataToDraw(x.domain())));
focus.select(".axis--x").call(xAxis);
svg.select(".zoom").call(zoom.transform, d3.zoomIdentity
.scale(width / (s[1] - s[0]))
.translate(-s[0], 0));
}
function zoomed() {
if (d3.event.sourceEvent && d3.event.sourceEvent.type === "brush") return; // ignore zoom-by-brush
var t = d3.event.transform;
//console.log(t);
x.domain(t.rescaleX(x2).domain());
focus.select(".line").attr("d", line(getDataToDraw(t.rescaleX(x2).domain())));
focus.select(".axis--x").call(xAxis);
context.select(".brush").call(brush.move, x.range().map(t.invertX, t));
}
}
Here's my thoughts.
Re-plot seems a must-have, because how could you expect to have same position when you zoom in the points ?
However there's some frequency of the replot you can control. For example, people use debounce to decrease the number of firing below 50ms during any event (ex. pan especailly). Debounce is a general solution, you can check lodash library for some implementation.
.on("zoom", debounced(zoomed)) // lower the chance if you get 5 calls under 500ms
Also if there's any animation involved, you can defer the animation until the last stage of the zoom (or pan), which is similar to debounce concept. Or just simply disable animation.
Note: React does support another mode called concurrent, it's not enabled by default, not yet. However what it does is that, assuming each plot is captured by a small component, and it spends 1ms for render, then after it renders 16 components, it believes it spend too much time in this rendering, and give the response back to the browser to handle other things, ex. user input etc. This way you can start to scroll your page or move your mouse. And in the next cycle it can pick up the next 16 components. Assuming you have 1000 components, it'll take couple of cycles before it can finish all the rendering. And if you zooms again in the middle, it'll skip the first 16 components and move to the new render all over again. Hope you get the idea. It might help your problem with the latest React 18.
Refer to the post plotting 50 million points with d3.js.
Sluggish interaction with zoom and pan are due to too many elements in the svg. The key is to use hierarchical levels of detail, to limit the maximum elements in svg.
I have a project where i am trying to use transitions to display data for years 1800-1805
I have created a bar chart and could get the transition, but the problem here is that i am trying to sort the bar chart so that the data will be displayed in descending order. how ever when i sort the data and do the transition instead of changing the "y" attribute values of rectangles my code is replacing the existing rectangles to the sorted ones for every single transition.
I want my code in such a way that the rectangles should move its position to its new position from current one .
How could i do that.
My data is as below
year 1800
China - 20000
USA - 80000
France - 15000
year 1801
China - 25000
USA -90000
France - 35000
now for this data my code is replacing France and china data it is not moving up and down. what should i add in my code to do that?
main.js
-------------
/*
* main.js
* Mastering Data Visualization with D3.js
* 2.5 - Activity: Adding SVGs to the screen
*/
var margin = { left:80, right:20, top:50, bottom:100 };
var width = 600 - margin.left - margin.right,
height = 400 - margin.top - margin.bottom;
var g = d3.select("#chart-area")
.append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.append("g")
.attr("transform", "translate(" + margin.left + ", " + margin.top + ")");
var time = 0;
/*var xLabel = g.append("text")
.attr("y", height + 200)
.attr("x", width / 2)
.attr("font-size", "20px")
.attr("text-anchor", "middle")
.text("GDP Per Capita ($)");
var yLabel = g.append("text")
.attr("transform", "rotate(-90)")
.attr("y", -40)
.attr("x", -170)
.attr("font-size", "20px")
.attr("text-anchor", "middle")
.text("Life Expectancy (Years)")*/
var timeLabel = g.append("text")
.attr("y", height +100)
.attr("x", width + 100)
.attr("font-size", "40px")
.attr("opacity", "0.4")
.attr("text-anchor", "middle")
.text("1800");
var data = d3.json("buildings.json").then(function(data){
// console.log(data);
// Clean data
data.forEach(function(d) {
d.Year = +d.Year;
d.Budget=+d.Budget;
});
const formattedData = data.map(function(year){
return year["countries"].filter(function(country){
var dataExists = (country.budget);
return dataExists
})
});
// Run the code every 0.1 second
d3.interval(function(){
// At the end of our data, loop back
time = (time < 5) ? time+1 : 0
update(formattedData[time]);
}, 10000);
// First run of the visualization
update(formattedData[0]);
})
function update(data) {
// Standard transition time for the visualization
var t = d3.transition()
.duration(10000);
// JOIN new data with old elements.
var rects = g.selectAll("rect").data(data, function(d){
return d;
});
// EXIT old elements not present in new data.
//rects.exit()
//.attr("class", "exit")
//.remove();
// ENTER new elements present in new data.
rects.enter()
.append("rect")
.attr("class", "enter")
//.attr("fill", function(d) { return continentColor(d.continent); })
// .merge(rects)
.attr("x", 0)
.attr("y", function(d,i){
return i*20;
}).transition(t)
.attr("width", function(d,i){
return d.budget/100;
console.log(d.budget);
})
.attr("height", 18)
.attr("fill",function(d,i){
if(d.country=="Italy")
{
return "green";
}else if(d.country=="Australia"){
return "blue";
}
});
// Update the time label
timeLabel.text(+(time + 1800))
}
Your problem is just the key function, which is fundamental for achieving object constancy.
Right now you have this:
var rects = g.selectAll("rect").data(data, function(d){
return d;
});
Which returns d, that is, the whole datum. Given your data structure, the datum is an object.
However, the API is clear about the value returned by the key function:
If a key function is not specified, then the first datum in data is assigned to the first selected element, the second datum to the second selected element, and so on. A key function may be specified to control which datum is assigned to which element, replacing the default join-by-index, by computing a string identifier for each datum and element. (emphasis mine)
Therefore, change the key function to:
var rects = g.selectAll("rect").data(data, function(d){
return d.country;
//string---^
});
I am new to Javascript and i am trying to draw 4 radar charts. Each chart has different title. I create TitleOptions var and call it below. but it shows everything on every chart. Can I filter the title by using ChartID? I attached my code below. and could anyone help me with this?
<script>
var w = 200;
var h = 200;
var colorscale = d3.scale.category10();
//Legend, titles
var LegendOptions = ['Try Count','Succcess Count', 'Success Rate'];
var TitleOptions=['Try/Success Count Per Skill', 'Try/Success Rate Per Skill', 'Point Get Per Skill', 'Point Lose Per Skill']
////////////////////////////////////////////
/////////// Initiate legend ////////////////
////////////////////////////////////////////
var svg = d3.select('#body')
// .selectAll('svg')
.append('svg')
.attr("width", w+300)
.attr("height", h)
//Create the title for the legend
var text = svg.append('g').append("text")
.attr("class", "title")
.attr('transform', 'translate(90,0)')
.attr("x", 30)
.attr("y", 10)
.attr("font-size", "12px")
.attr("fill", "#404040")
// .text("What % of owners use a specific service in a week");
.text(TitleOptions);
//Initiate Legend
var legend = svg.append("g")
.attr("class", "legend")
.attr("height", 100)
.attr("width", 200)
.attr('transform', 'translate(90,20)')
;
//Create colour squares
legend.selectAll('rect')
.data(LegendOptions)
.enter()
.append("rect")
.attr("x", w - 65)
.attr("y", function(d, i){ return i * 20;})
.attr("width", 10)
.attr("height", 10)
.style("fill", function(d, i){ return colorscale(i);})
;
//Create text next to squares
legend.selectAll('text')
.data(LegendOptions)
.enter()
.append("text")
.attr("x", w - 52)
.attr("y", function(d, i){ return i * 20 + 9;})
.attr("font-size", "11px")
.attr("fill", "#737373")
.text(function(d) { return d; })
;
//Options for the Radar chart, other than default
var mycfg = {
w: w,
h: h,
maxValue: 0.6,
levels: 6,
ExtraWidthX: 300
}
//Load the data and Call function to draw the Radar chart
// dynamic data creation
d3.json("<c:url value='/chart/radarChartData.do?ChartID=${ChartID}&PlayerKey=${PlayerKey}'/>", function(error, data){
RadarChart.draw("#chart", JSONconverter(data.list), mycfg);
});
</script>
Encapsulate the drawing part into a function and call it four times. Something like:
function draw(title) {
const svg = ..
..
.text(title);
}
draw('title1');
draw('title2');
// or better:
['title1', 'title2'].forEach(draw);
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/
I've got a working Stacked Area Chart using NVD3.js: working jsfiddle here
var volumeData = [{"key":"Hit","values":[[1.3781628E12,12],[1.3782492E12,9],[1.3783356E12,9],[1.378422E12,4],[1.3785084E12,2],[1.3785948E12,3],[1.3786812E12,6],[1.3787676E12,5],[1.378854E12,1],[1.3789404E12,5],[1.3790268E12,1],[1.3791132E12,3],[1.3791996E12,0],[1.379286E12,2],[1.3793724E12,0]]},{"key":"Miss","values":[[1.3781628E12,3],[1.3782492E12,3],[1.3783356E12,1],[1.378422E12,12],[1.3785084E12,4],[1.3785948E12,7],[1.3786812E12,10],[1.3787676E12,13],[1.378854E12,14],[1.3789404E12,8],[1.3790268E12,5],[1.3791132E12,2],[1.3791996E12,3],[1.379286E12,11],[1.3793724E12,6]]}];
(function(data){
var colors = d3.scale.category20();
keyColor = function(d, i) {return colors(d.key)};
var chart;
nv.addGraph(function() {
chart = nv.models.stackedAreaChart()
.x(function(d) { return d[0] })
.y(function(d) { return d[1] })
.color(keyColor);
chart.xAxis.tickFormat(function(d) { return d3.time.format('%x')(new Date(d)) });
chart.yAxis.tickFormat(d3.format('d'));
d3.select('#graph svg')
.datum(data)
.transition().duration(0)
.call(chart);
//nv.utils.windowResize(chart.update);
return chart;
});
})(volumeData);
What I'd like to do is add an "average" line for each series over the visible x-range. I'll have no trouble calculating the average for each series, I just don't know how to get the line to show up on the graph.
Is it possible with nvd3.js, or would I have to drop down to d3 to do it?
Two other options:
1. Quick but a little hacky, create another line in your data for each average line by setting the same y value for each x value. You'll get a straight line although each x tick will have a tooltip with the same average value.
2. Draw a fixed line across the chart from the point where the average is on the yAxis:
//margin from chart declaration
var margin = { top: 30, right: 60, bottom: 60, left: 100 };
//calculate the yScale
var yScale = chart.yAxis.scale();
//call generic function...since you'll want this on potentially multiple types of charts
drawFixedLineAndText(chartID, 960, margin, <your average value goes here>, yScale, <your average label text goes here>);
function drawFixedLineAndText(chartName, width, margin, yValue, yValueScale, text) {
var svg = d3.select("#" + chartName + " svg");
svg.append("line")
.style("stroke", "#FF7F0E")
.style("stroke-width", "2.5px")
.attr("x1", margin.left)
.attr("y1", yValueScale(yValue) + margin.top)
.attr("x2", width - margin.right)
.attr("y2", yValueScale(yValue) + margin.top);
//add text to fixed line
d3.select("#" + chartName + " svg")
.append("text")
.attr("x", width - margin.right / 2)
.attr("y", yValueScale(yValue) + margin.top)
.attr("text-anchor", "middle")
.text(text);
//end fixed line
}
You can use nvD3 multichart option to achieve it.
https://github.com/novus/nvd3/blob/master/examples/multiChart.html
It can be done in D3. Calculate average for the line while processing area data:
var line = d3.svg.line().interpolate("basis").x(function(d) {
return x(d.x_axis);
}).y(function(d) {
return y(d.y_axis);
});
var stacked_data = [];
var line_data = [];
data.forEach(function(d) {
var total = 0;
for (var i = 0; i < AREAS.length; i++) {
var nd = {};
nd.date = X_DATA_PARSE(d.date);
nd.key = AREAS[i];
nd.value = +d[AREAS[i]];
stacked_data.push(nd);
total = total + nd.value;
}
var ld = {};
ld.x_axis = X_DATA_PARSE(d.date);
ld.y_axis = total / AREAS.length;
line_data.push(ld);
});
Then draw the line like normal line chart:
svg.append("path")
.datum(line_data)
.attr("class", "line")
.attr("d", line);
Here's a complete example:
http://vida.io/documents/jXKmv3j3gF9LmtiyW