I have a function called show_product_price to draw line based on input data (containing date and price).
When I did a test by manually generating 2 dataset, then call the function, everything works fine and 2 lines are drawn.
like this:
show_product_price(data1, "origin");
show_product_price(data2, "competitor");
But when I try to call the function using 2 ajax calls. Problem happens. The function does receive 2 different datasets, but the 2 lines drawn overlap each other. Basically the 2 lines use the same dataset, even though during the 2 ajax call, the function receive 2 daatset.
I am not sure why this is happening.
The ajax looks like:
//inside one function, ajax call to get one dataset, I checked show_product_price() receive it successfuly
$.ajax({
url: 'xxx',
success: function(data) {
show_product_price(data, "origin");
}
});
//inside another function, get another dataset, also received successfuly
$.ajax({
url: 'xxx',
success: function(data) {
show_product_price(data, "competitor");
}
});
The function is:
function show_product_price(input, who) {
var data = JSON.parse(JSON.stringify(input));
// I did an alert() here, i amde sure the data passed in is different
//#visualisation is a svg
var vis = d3.select("#visualisation"),
WIDTH = 1000,
HEIGHT = 500,
MARGINS = {
top: 50,
right: 50,
bottom: 50,
left: 50
},
//set up scale here
xScale = d3.time.scale()
.range([MARGINS.left, WIDTH - MARGINS.right])
.domain([new Date(data[0].date),
d3.time.day.offset(new Date(data[data.length - 1].date), 1)]),
yScale = d3.scale.linear().range([HEIGHT - MARGINS.top, MARGINS.bottom])
.domain([0, d3.max(data, function(d) {
return d.price;
}) * 1.5]);
if (who == "origin") {
// Append axis and text, code works, omit here
}
var parseDate = d3.time.format("%Y-%m-%d").parse;
var lineGen = d3.svg.line()
.x(function(d) {
return xScale(parseDate(d.date));
})
.y(function(d) {
return yScale(d.price);
})
.interpolate("basis");
var color = "green";
if (who == "competitor") {
color = "red";
}
// draw each data point as a circle
vis.selectAll("circle." + who)
.data(data)
.enter()
.append("circle")
.attr("class", who)
.attr("cx", function(d) {
return xScale(parseDate(d.date));
})
.attr("cy", function(d) {
return yScale(d.price);
})
.attr("fill", color)
.transition()
.delay(function(d, i) { return i * (3000 / (data.length - 1)); })
.attr("r", 5);
//draw lines connecting the circles
vis.append('svg:path')
.attr('d', lineGen(data))
.attr('stroke', color)
.attr("id", who)
.attr('stroke-width', 2)
.attr('fill', 'none');
}
Related
I adapted a multi-line chart which has a legend and axis and displays correctly on the bl.ocks.org site (http://bl.ocks.org/Matthew-Weber/5645518). The legend reorganizes itself when you select a different type from the drop down field. On my adaptation when the legend reorganizes itself the items start to overlap each other when some types are selected. Also the axes draw on top of each other. The original code uses tipsy but I have not checked it.
// original author's code http://bl.ocks.org/Matthew-Weber/5645518;
//set the margins
var margin = {
top: 50,
right: 160,
bottom: 80,
left: 50
},
width = 900 - margin.left - margin.right,
height = 500 - margin.top - margin.bottom;
//set dek and head to be as wide as SVG
d3.select('#dek')
.style('width', width + 'px');
d3.select('#headline')
.style('width', width + 'px');
//write out your source text here
var sourcetext = "xxx";
// set the type of number here, n is a number with a comma, .2% will get you a percent, .2f will get you 2 decimal points
var NumbType = d3.format(",");
// color array
var bluescale4 = ["red", "blue", "green", "orange", "purple"];
//color function pulls from array of colors stored in color.js
var color = d3.scale.ordinal().range(bluescale4);
//defines a function to be used to append the title to the tooltip.
var maketip = function(d) {
var tip = '<p class="tip3">' + d.name + '<p class="tip1">' + NumbType(d.value) + '</p> <p class="tip3">' + formatDate(d.date) + '</p>';
return tip;
}
//define your year format here, first for the x scale, then if the date is displayed in tooltips
var parseDate = d3.time.format("%Y-%m-%d").parse;
var formatDate = d3.time.format("%b %d, '%y");
//create an SVG
var svg = d3.select("#graphic").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 + ")");
//make a rectangle so there is something to click on
svg.append("svg:rect")
.attr("width", width)
.attr("height", height)
.attr("class", "plot"); //#fff
// force data to update when menu is changed
var menu = d3.select("#menu select")
.on("change", change);
//suck in the data, store it in a value called formatted, run the redraw function
d3.csv("/sites/default/d3_files/d3-provinces/statistics-april-15-2.csv", function(data) {
formatted = data;
redraw();
});
d3.select(window)
.on("keydown", function() {
altKey = d3.event.altKey;
})
.on("keyup", function() {
altKey = false;
});
var altKey;
// set terms of transition that will take place
// when a new type (Death etc.)indicator is chosen
function change() {
d3.transition()
.duration(altKey ? 7500 : 1500)
.each(redraw);
} // end change
// REDRAW all the meat goes in the redraw function
function redraw() {
// create data nests based on type indicator (series)
var nested = d3.nest()
.key(function(d) {
return d.type;
})
.map(formatted)
// get value from menu selection
// the option values are set in HTML and correspond
//to the [type] value we used to nest the data
var series = menu.property("value");
// only retrieve data from the selected series, using the nest we just created
var data = nested[series];
// for object constancy we will need to set "keys", one for each type of data (column name) exclude all others.
color.domain(d3.keys(data[0]).filter(function(key) {
return (key !== "date" && key !== "type");
}));
var linedata = color.domain().map(function(name) {
return {
name: name,
values: data.map(function(d) {
return {
name: name,
date: parseDate(d.date),
value: parseFloat(d[name], 10)
};
})
};
});
//make an empty variable to stash the last values into so we can sort the legend // do we need to sort it?
var lastvalues = [];
//setup the x and y scales
var x = d3.time.scale()
.domain([
d3.min(linedata, function(c) {
return d3.min(c.values, function(v) {
return v.date;
});
}),
d3.max(linedata, function(c) {
return d3.max(c.values, function(v) {
return v.date;
});
})
])
.range([0, width]);
var y = d3.scale.linear()
.domain([
d3.min(linedata, function(c) {
return d3.min(c.values, function(v) {
return v.value;
});
}),
d3.max(linedata, function(c) {
return d3.max(c.values, function(v) {
return v.value;
});
})
])
.range([height, 0]);
//will draw the line
var line = d3.svg.line()
.x(function(d) {
return x(d.date);
})
.y(function(d) {
return y(d.value);
});
//create and draw the x axis - need to clear the existing axis
var xAxis = d3.svg.axis()
.scale(x)
.orient("bottom")
.tickPadding(8)
.ticks(10);
//create and draw the y axis
var yAxis = d3.svg.axis()
.scale(y)
.orient("left")
.tickSize(0 - width)
.tickPadding(8);
svg.append("svg:g")
.attr("class", "x axis");
svg.append("svg:g")
.attr("class", "y axis")
.attr("transform", "translate(" + (0) + ",0)")
.call(yAxis);
//bind the data
var thegraph = svg.selectAll(".thegraph")
.data(linedata)
//append a g tag for each line and set of tooltip circles and give it a unique ID based on the column name of the data
var thegraphEnter = thegraph.enter().append("g")
.attr("class", "thegraph")
.attr('id', function(d) {
return d.name + "-line";
})
.style("stroke-width", 2.5)
.on("mouseover", function(d) {
d3.select(this) //on mouseover of each line, give it a nice thick stroke // works
.style("stroke-width", '6px');
var selectthegraphs = $('.thegraph').not(this); //select all the rest of the lines, except the one you are hovering on and drop their opacity
d3.selectAll(selectthegraphs)
.style("opacity", 0.2);
var getname = document.getElementById(d.name); //use get element cause the ID names have spaces in them - not working
var selectlegend = $('.legend').not(getname); //grab all the legend items that match the line you are on, except the one you are hovering on
d3.selectAll(selectlegend) // drop opacity on other legend names
.style("opacity", .2);
d3.select(getname)
.attr("class", "legend-select"); //change the class on the legend name that corresponds to hovered line to be bolder
}) // end of mouseover
.on("mouseout", function(d) { //undo everything on the mouseout
d3.select(this)
.style("stroke-width", '2.5px');
var selectthegraphs = $('.thegraph').not(this);
d3.selectAll(selectthegraphs)
.style("opacity", 1);
var getname = document.getElementById(d.name);
var getname2 = $('.legend[fakeclass="fakelegend"]')
var selectlegend = $('.legend').not(getname2).not(getname);
d3.selectAll(selectlegend)
.style("opacity", 1);
d3.select(getname)
.attr("class", "legend");
});
//actually append the line to the graph
thegraphEnter.append("path")
.attr("class", "line")
.style("stroke", function(d) {
return color(d.name);
})
.attr("d", function(d) {
return line(d.values[0]);
})
.transition()
.duration(2000)
.attrTween('d', function(d) {
var interpolate = d3.scale.quantile()
.domain([0, 1])
.range(d3.range(1, d.values.length + 1));
return function(t) {
return line(d.values.slice(0, interpolate(t)));
};
});
//then append some 'nearly' invisible circles at each data point
thegraph.selectAll("circle")
.data(function(d) {
return (d.values);
})
.enter()
.append("circle")
.attr("class", "tipcircle")
.attr("cx", function(d, i) {
return x(d.date)
})
.attr("cy", function(d, i) {
return y(d.value)
})
.attr("r", 3) // was 12
.style('opacity', .2)
.attr("title", maketip);
//append the legend
var legend = svg.selectAll('.legend')
.data(linedata);
var legendEnter = legend
.enter()
.append('g')
.attr('class', 'legend')
.attr('id', function(d) {
return d.name;
})
.on('click', function(d) { //onclick function to toggle off the lines
if ($(this).css("opacity") == 1) {
//uses the opacity of the item clicked on to determine whether to turn the line on or off
var elemented = document.getElementById(this.id + "-line"); //grab the line that has the same ID as this point along w/ "-line"
//use get element cause ID has spaces
d3.select(elemented)
.transition()
.duration(1000)
.style("opacity", 0)
.style("display", 'none');
d3.select(this)
.attr('fakeclass', 'fakelegend')
.transition()
.duration(1000)
.style("opacity", .2);
} else {
var elemented = document.getElementById(this.id + "-line");
d3.select(elemented)
.style("display", "block")
.transition()
.duration(1000)
.style("opacity", 1);
d3.select(this)
.attr('fakeclass', 'legend')
.transition()
.duration(1000)
.style("opacity", 1);
}
});
//create a scale to pass the legend items through // this is broken for some types
var legendscale = d3.scale.ordinal()
.domain(lastvalues)
.range([0, 30, 60, 90, 120, 150, 180, 210]);
//actually add the circles to the created legend container
legendEnter.append('circle')
.attr('cx', width + 20) // cx=width+50 made circle overlap text
.attr('cy', function(d) {
var newScale = (legendscale(d.values[d.values.length - 1].value) + 20);
return newScale;
})
.attr('r', 7)
.style('fill', function(d) {
return color(d.name);
});
//add the legend text
legendEnter.append('text')
.attr('x', width + 35) // is this an issue?
.attr('y', function(d) {
return legendscale(d.values[d.values.length - 1].value);
})
.text(function(d) {
return d.name;
});
// set variable for updating visualization
var thegraphUpdate = d3.transition(thegraph);
// change values of path and then the circles to those of the new series
thegraphUpdate.select("path")
.attr("d", function(d, i) {
lastvalues[i] = d.values[d.values.length - 1].value;
lastvalues.sort(function(a, b) {
return b - a
});
legendscale.domain(lastvalues);
return line(d.values);
// }
});
thegraphUpdate.selectAll("circle")
.attr("title", maketip) // displays HTML but not circle
.attr("cy", function(d, i) {
return y(d.value)
})
.attr("cx", function(d, i) {
return x(d.date)
});
// and now for legend items
var legendUpdate = d3.transition(legend);
legendUpdate.select("circle")
.attr('cy', function(d, i) {
return legendscale(d.values[d.values.length - 1].value);
});
legendUpdate.select("text")
.attr('y', function(d) {
return legendscale(d.values[d.values.length - 1].value);
});
d3.transition(svg).select(".x.axis")
.attr("transform", "translate(0," + height + ")")
.call(xAxis);
//make my tooltips work
$('circle').tipsy({
opacity: .9,
gravity: 'n',
html: true
});
//end of the redraw function
}
svg.append("svg:text")
.attr("text-anchor", "start")
.attr("x", 0 - margin.left)
.attr("y", height + margin.bottom - 10)
.text(sourcetext)
.attr("class", "source");
My adapted code (including a lot of console.log messages) is in jsfiddle https://jsfiddle.net/pwarwick43/13fpn567/2/
I am beginning to think the problem might be with the version of d3 or jquery. Anyone got suggestions about this?
I created this chart using D3 V5. Also, I have attached the sample data on the fiddle you can view by clicking here.
I've included the tick function code block which appends new domains for x and y scales and line/data on the path to slide left:
When the tick function executes, the line sort of rebuilds which makes it look like it bounces.
How can it be smooth, without a bounce at all when it rebuilds the line?
var tr = d3
.transition()
.duration(obj.tick.duration)
.ease(d3.easeLinear);
function tick() {
return setInterval(function() {
var newData = [];
var tickFunction = obj.tick.fnTickData;
if (tickFunction !== undefined && typeof tickFunction === "function") {
newData = tickFunction();
for (var i = 0; i < newData.length; i++) {
obj.data.push(newData[i]);
}
}
if (newData.length > 0) {
var newMaxDate, newMinDate, newDomainX;
if (isKeyXDate) {
newMaxDate = new Date(
Math.max.apply(
null,
obj.data.map(function(e) {
return new Date(e[obj.dataKeys.keyX]);
})
)
);
newMinDate = new Date(
Math.min.apply(
null,
obj.data.map(function(e) {
return new Date(e[obj.dataKeys.keyX]);
})
)
);
newDomainX = [newMinDate, newMaxDate];
} else {
newDomainX = [
d3.min(obj.data, function(d) {
return d[obj.dataKeys.keyX];
}),
d3.max(obj.data, function(d) {
return d[obj.dataKeys.keyX];
})
];
}
// update the domains
//x.domain([newMinDate, newMaxDate]);
if (obj.tick.updateXDomain) {
newDomainX = obj.tick.updateXDomain;
}
x.domain(newDomainX);
if (obj.tick.updateYDomain) {
y.domain(obj.tick.updateYDomain);
}
path.attr("transform", null);
// slide the line left
if (obj.area.allowArea) {
areaPath.attr("transform", null);
areaPath
.transition()
.transition(tr)
.attr("d", area);
}
path
.transition()
.transition(tr)
.attr("d", line);
svg
.selectAll(".x")
.transition()
.transition(tr)
.call(x.axis);
svg
.selectAll(".y")
.transition()
.transition(tr)
.call(y.axis);
// pop the old data point off the front
obj.data.shift();
}
}, obj.tick.tickDelay);
}
this.interval = tick();
That bounce is actually the expected result when you transition the d attribute, which is just a string.
There are several solutions here. Without refactoring your code too much, a simple one is using the pathTween function written by Mike Bostock in this bl.ocks: https://bl.ocks.org/mbostock/3916621. Here, I'm changing it a little bit so you can pass the datum, like this:
path.transition()
.transition(tr)
.attrTween("d", function(d) {
var self = this;
var thisd = line(d);
return pathTween(thisd, 1, self)()
})
Here is the forked plunker: https://plnkr.co/edit/aAqpdSb9JozwHsErpqa9?p=preview
As Gerardo notes, transitioning the d attribute of the path won't work very well unless you modfiy the approach. Here's a simple example of the sort of wiggle/bouncing that will arise if simply updating the d attribute of the path:
Pᴏɪɴᴛs ᴛʀᴀɴsɪᴛɪᴏɴɪɴɢ ᴀᴄʀᴏss sᴄʀᴇᴇɴ, ᴡɪᴛʜ ᴘᴀᴛʜ ᴛʀᴀɴsɪᴛɪᴏɴɪɴɢ ғʀᴏᴍ ᴏɴᴇ ᴅᴀᴛᴀ sᴇᴛ ᴛᴏ ᴛʜᴇ ɴᴇxᴛ.
The above behavior is noted by Mike Bostock in a short piece here, and here's a snippet reproducing the above animation:
var n = 10;
var data = d3.range(n).map(function(d) {
return {x: d, y:Math.random() }
})
var x = d3.scaleLinear()
.domain(d3.extent(data, function(d) { return d.x; }))
.range([10,490])
var y = d3.scaleLinear()
.range([290,10]);
var line = d3.line()
.x(function(d) { return x(d.x); })
.y(function(d) { return y(d.y); })
var svg = d3.select("body")
.append("svg")
.attr("width",500)
.attr("height", 400)
.append("g");
var path = svg.append("path")
.datum(data)
.attr("d", line);
var points = svg.selectAll("circle")
.data(data, function(d) { return d.x; })
.enter()
.append("circle")
.attr("cx", function(d) { return x(d.x); })
.attr("cy", function(d) { return y(d.y); })
.attr("r", 5);
function tick() {
var transition = d3.transition()
.duration(1000);
var newPoint = {x:n++, y: Math.random() };
data.shift()
data.push(newPoint);
x.domain(d3.extent(data,function(d) { return d.x; }))
points = svg.selectAll("circle").data(data, function(d) { return d.x; })
points.exit()
.transition(transition)
.attr("cx", function(d) { return x(d.x); })
.attr("cy", function(d) { return y(d.y); })
.remove();
points.enter().append("circle")
.attr("cx", function(d) { return x(d.x)+30; })
.attr("cy", function(d) { return y(d.y); })
.merge(points)
.transition(transition)
.attr("cx", function(d) { return x(d.x); })
.attr("r", 5);
path.datum(data)
.transition(transition)
.attr("d", line)
.on("end", tick);
}
tick();
path {
fill: none;
stroke: black;
stroke-width: 2;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
One solution to this wiggle/bounce is:
add an additional point(s) to the data,
redraw the line with the recently added to data array
find out the next extent of the data
transition the line to the left
update the scale and transition the axis
remove the first data point(s)
This is also proposed in Mike's article that I've linked to. Here would be a basic implementation with your code:
I've avoided a setInterval function by recursively calling the function at the end of the last transition:
function slide() {
// Stop any ongoing transitions:
d3.selectAll().interrupt();
// A transition:
var transition = d3.transition()
.duration(2000)
.ease(d3.easeLinear)
// 1. add an additional point(s) to the data
var newData = obj.tick.fnTickData();
obj.data.push(...newData);
// 2. redraw the line with the recently added to data array
path.datum(obj.data)
areaPath.datum(obj.data)
// Redraw the graph, without the translate, with less data:
path.attr("transform","translate(0,0)")
.attr("d", line)
areaPath.attr("transform","translate(0,0)")
.attr("d", area)
// 3. find out the next extent of the data
// Assuming data is in chronological order:
var min = obj.data[newData.length][obj.dataKeys.keyX];
var max = obj.data[obj.data.length-1][obj.dataKeys.keyX];
// 4. transition the line to the left
path.datum(obj.data)
.transition(transition)
.attr("transform", "translate("+(-x(new Date(min)))+",0)");
areaPath.datum(obj.data)
.transition(transition)
.attr("transform", "translate("+(-x(new Date(min)))+",0)");
// 5. update the scale and transition the axis
x.domain([new Date(min),new Date(max)])
// Update the xAxis:
svg.selectAll('.x')
.transition(transition)
.call(x.axis)
.on("end",slide); // Trigger a new transition at the end.
// 6. remove the first data point(s)
obj.data.splice(0,newData.length)
}
slide();
Here's an updated plunkr.
I am plotting points on a UK map using D3 off a live data stream. When the data points exceed 10,000 the browser becomes sluggish and the animation is no longer smooth. So I modify the dataPoints array to keep only the last 5000 points.
However when I modify the dataPoints the first time using splice() D3 stops rendering any new points. The old points gradually disappear (due to a transition) but there are no new points. I am not sure what I am doing wrong here.
I have simulated the problem by loading data of a CSV as well storing it in memory and plotting them at a rate of 1 point every 100ms. Once the number of dots goes above 10 I splice to retain the last 5 points. I see the same behaviour. Can someone review the code and let me know what I am doing wrong?
Setup and the plotting function:
var width = 960,
height = 1160;
var dataPoints = []
var svg = d3.select("#map").append("svg")
.attr("width", width)
.attr("height", height);
var projection = d3.geo.albers()
.center([0, 55.4])
.rotate([4.4, 0])
.parallels([40, 70])
.scale(5000)
.translate([width / 2, height / 2]);
function renderPoints() {
var points = svg.selectAll("circle")
.data(dataPoints)
points.enter()
.append("circle")
.attr("cx", function (d) {
prj = projection([d.longitude, d.latitude])
return prj[0];
})
.attr("cy", function (d) {
prj = projection([d.longitude, d.latitude])
return prj[1];
})
.attr("r", "4px")
.attr("fill", "blue")
.attr("fill-opacity", ".4")
.transition()
.delay(5000)
.attr("r", "0px")
}
/* JavaScript goes here. */
d3.json("uk.json", function(error, uk) {
if (error) return console.error(error);
console.log(uk);
var subunits = topojson.feature(uk, uk.objects.subunits);
var path = d3.geo.path()
.projection(projection);
svg.selectAll(".subunit")
.data(subunits.features)
.enter().append("path")
.attr("class", function(d) { return "subunit " + d.id })
.attr("d", path);
svg.append("path")
.datum(topojson.mesh(uk, uk.objects.subunits, function(a,b) {return a!== b && a.id !== 'IRL';}))
.attr("d", path)
.attr("class", "subunit-boundary")
svg.append("path")
.datum(topojson.mesh(uk, uk.objects.subunits, function(a,b) {return a=== b && a.id === 'IRL';}))
.attr("d", path)
.attr("class", "subunit-boundary IRL")
svg.selectAll(".place-label")
.attr("x", function(d) { return d.geometry.coordinates[0] > -1 ? 6 : -6; })
.style("text-anchor", function(d) { return d.geometry.coordinates[0] > -1 ? "start": "end"; });
svg.selectAll(".subunit-label")
.data(topojson.feature(uk, uk.objects.subunits).features)
.enter().append("text")
.attr("class", function(d) { return "subunit-label " + d.id })
.attr("transform", function(d) { return "translate(" + path.centroid(d) + ")"; })
.attr("dy", ".35em")
.text(function(d) { return d.properties.name; })
// function applyProjection(d) {
// console.log(d);
// prj = projection(d)
// console.log(prj);
// return prj;
// }
lon = -4.6
lat = 55.45
dataPoints.push([lon,lat])
renderPoints()
});
Function to cleanup old points
var cleanupDataPoints = function() {
num_of_elements = dataPoints.length
console.log("Pre:" + num_of_elements)
if(num_of_elements > 10) {
dataPoints = dataPoints.splice(-5, 5)
}
console.log("Post:" + dataPoints.length)
}
Loading data from CSV and plotting at a throttled rate
var bufferedData = null
var ptr = 0
var renderNext = function() {
d = bufferedData[ptr]
console.log(d)
dataPoints.push(d)
ptr++;
renderPoints()
cleanupDataPoints()
if(ptr < bufferedData.length)
setTimeout(renderNext, 100)
}
d3.csv('test.csv', function (error, data) {
bufferedData = data
console.log(data)
setTimeout(renderNext, 100)
})
In the lines
points = svg.selectAll("circle")
.data(dataPoints)
points.enter() (...)
d3 maps each element in dataPoints (indexed from 0 to 5000) to the circle elements (of which there should be 5000 eventually). So from its point of view, there is no enter'ing data: there are enough circles to hold all your points.
To make sure that the same data point is mapped to the same html element after it changed index in its array, you need to use an id field of some sort attached to each of your data point, and tell d3 to use this id to map the data to elements, instead of their index.
points = svg.selectAll("circle")
.data(dataPoints, function(d){return d.id})
If the coordinates are a good identifier for your point, you can directly use:
points = svg.selectAll("circle")
.data(dataPoints, function(d){return d.longitude+" "+d.latitude})
See https://github.com/mbostock/d3/wiki/Selections#data for more details.
I've got an array of objects called graphData (size varies). Each element contains all the information required to create a d3 graph, and I am able to successfully draw the graphs if I access graphData elements by hardcoding (i.e. graphdata[0], graphdata[1] etc).
The problem comes when I attempt to use a for loop to generate one graph for each of the elements. Looked around stackoverflow and the web, but the solutions are all about generating a fixed number of multiple graphs, not generating multiple graphs dynamically.
Below is my working code for generating one graph. What is the recommended way to generate x number of graphs automatically?
var graphData = data.graph;
var RADIUS = 15;
var edgeData = graphData[0].edges;
var nodeData = graphData[0].nodes;
var stageNum = graphData[0].stage;
var xScale = d3.scale.linear()
.domain([d3.min(edgeData, function (d) {
return d.start[0];
}),
d3.max(edgeData, function (d) {
return d.start[0];
})])
.range([50, w - 100]);
var yScale = d3.scale.linear()
.domain([d3.min(edgeData, function (d) {
return d.start[1];
}),
d3.max(edgeData, function (d) {
return d.start[1];
})])
.range([50, h - 100]);
var rScale = d3.scale.linear()
.domain([0, d3.max(edgeData, function (d) {
return d.start[1];
})])
.range([14, 17]);
// already have divs with classes stage1, stage2... created.
var svg = d3.select(".stage" + stageNum).append("svg")
.attr({"width": w, "height": h})
.style("border", "1px solid black");
var elemEdge = svg.selectAll("line")
.data(edgeData)
.enter();
var edges = elemEdge.append("line")
.attr("x1", function (d) {
return xScale(d.start[0]);
})
.attr("y1", function (d) {
return yScale(d.start[1]);
})
.attr("x2", function (d) {
return xScale(d.end[0]);
})
.attr("y2", function (d) {
return yScale(d.end[1]);
})
.attr("stroke-width", 2)
.attr("stroke", "black");
var elemNode = svg.selectAll("circle")
.data(nodeData)
.enter();
var nodes = elemNode.append("circle")
.attr("cx", function (d) {
return xScale(parseInt(d.x));
})
.attr("cy", function (d) {
return yScale(parseInt(d.y));
})
.attr({"r": rScale(RADIUS)})
.style("fill", "yellow")
.style("stroke", "black");
Mike Bostock recommends implementing charts as reusable closures with methods. This would be an ideal implementation in your case as you want to have
multiple graphs with different data
potential reloading with new data (hopefully this is what you mean by dynamic?)
In broad strokes, what you want to do is wrap your code above into a function in very much the same way Mike describes in the post above, and then have data be an attribute of your closure. So here is some badly hacked code:
// your implementation here
var chart = function(){...}
var graphData = d3.json('my/graphdata.json', function(error, data){
// now you have your data
});
// let's say you have a div called graphs
var myGraphs = d3.select('.graphs')
.data(graphData)
.enter()
.append('g')
//now you have g elements for each of your datums in the graphData array
//we use the saved selection above and call the chart function on each of the elements in the selection
myGraphs.call(chart);
//note that internally in your `chart` closure, you have to take in a selection
//object and process it(data is already bound to each of your selections from above):
function chart(selection) {
selection.each(function(data) {
//...
Here is some more good reading on the topic.
Well you can try the following approach.
var graphData = data.graph;
//forEach will return each element for the callback, you can then make use of the e1 to draw the graph.
graphData.forEach(function(e1){
//graph code goes here.
});
providing this as your source array
//it's just a single circle in 3, 4
var stuff = [3, 4];
var source = [ [stuff, stuff], [stuff] ];
a bit of Array stuff
Array.prototype.max = function() {
return Math.max.apply(null, this);
};
Array.prototype.min = function() {
return Math.min.apply(null, this);
};
setup:
var dim = [];
source.forEach(function(elem){
elem.forEach(function(circle){
dim.push(circle.min());
dim.push(circle.max());
});
});
var min = dim.min();
var max = dim.max();
var x = d3.scale.linear()
.domain([min, max])
.scale([yourscale]);
var y = d3.scale.linear()
.domain([min, max])
.scale([yourscale]);
d3.select('body').selectAll('div')
.data(source) //first step: a div with an svg foreach array in your array
.enter()
.append('div')
.append('svg')
.selectAll('circle') //second step: a circle in the svg for each item in your array
.data(function(d){
return d; //returns one of the [stuff] arrays
}).enter()
.append('circle')
.attr('r', 5)
.attr('cx', function(d){
return x(d[0]);
})
.attr('cy', function(d){
return y(d[1]);
})
.style('fill','blue');
I'm trying to plot circles from data in my csv file, but the circles are not appearing on the svg canvas. I believe the problem stems from how I load in the data (it gets loaded as an array of objects), but I'm not quite sure how to figure out what to do next.
Based off this tutorial: https://www.dashingd3js.com/svg-text-element
D3.js code:
var circleData = d3.csv("files/data.csv", function (error, data) {
data.forEach(function (d) {
d['KCComment'] = +d['KCComment'];
d['pscoreResult'] = +d['pscoreResult'];
d['r'] = +d['r'];
});
console.log(data);
});
var svg = d3.select("body").append("svg")
.attr("width", 480)
.attr("height", 480);
var circles = svg.selectAll("circle")
.data(circleData)
.enter()
.append("circle");
var circleAttributes = circles
.attr("cx", function (d) { return d.KCComment; })
.attr("cy", function (d) { return d.pscoreResult; })
.attr("r", function (d) { return d.r; })
.style("fill", "green");
var text = svg.selectAll("text")
.data(circleData)
.enter()
.append("text");
var textLabels = text
.attr("x", function(d) { return d.KCComment; })
.attr("y", function(d) { return d.pscoreResult; })
.text(function (d) { return "( " + d.KCComment + ", " + d.pscoreResult + " )"; })
.attr("font-family", "sans-serif")
.attr("font-size", "20px")
.attr("fill", "red");
What the CSV looks like:
fmname, fmtype, KCComment, pscoreResult, r
test1, type1, 7.1, 8, 39
test2, type2, 1.2, 3, 12
You should have the circle-drawing code within the d3.csv function's callback, so it's only processed when the data is available.
d3.csv("data.csv", function (error, circleData) {
circleData.forEach(function (d) {
d['KCComment'] = +d['KCComment'];
d['pscoreResult'] = +d['pscoreResult'];
d['r'] = +d['r'];
});
console.log(circleData);
// Do the SVG drawing stuff
...
// Finished
});
Also note that instead of setting var circleData = d3.csv(... you should just define it in the callback function.
Here's a plunker with the working code: http://embed.plnkr.co/fzBX0o/preview
You'll be able to see a number of further issues now: both circles are overlapping and only one quarter is visible. That's because your KCComment and pscoreResult values used to define the circles' cx and cy are too small. Try multiplying them up so that the circles move right and down and are a bit more visible! Same is true of the text locations, but I'll leave those problems for you to solve