I have an area graph (supposed to represent a time series). I want to color the graph based on the y value such that for regions where the y > c it is one color and for the area where y<=c it is another color. Is this possible in D3?
This is the code I have that generates a single color graph:
var width = 700,
height = 400;
var vis = d3.select("#chart")
.append("svg")
.attr("width",width)
.attr("height",height);
var mpts = [{"x":0,"val":15}];
var n = 200;
for(var i=0;i<n;i++)
{
if(Math.random()>.5)
{
mpts = mpts.concat({"x":i+1,"val":mpts[i].val*(1.01)});
}
else
{
mpts = mpts.concat({"x":i+1,"val":mpts[i].val*(.99)});
}
}
var x = d3.scale.linear().domain([0,n]).range([10,10+width]);
var y = d3.scale.linear().domain([10,20]).range([height-10,0]);
var area = d3.svg.area()
.interpolate("linear")
.x(function(d) { return x(d.x); })
.y1(function(d) { return y(d.val); })
.y0(function(d) { return y(0); });
vis.append("svg:path")
.attr("class", "area")
.attr("d",area(mpts))
.attr("fill","orange");
</script>
Change the way you add the areas to something like
vis.selectAll("path").data(mpts).enter().append("svg:path")
.attr("class", "area")
.attr("d", area)
.attr("fill", function(d) { return (d.val > c ? "orange" : "yellow"); });
Related
I'm trying to make a d3 scatterplot that offers a searchbox on the webpage; when the user types a word into the search box, d3 filters the dataset based on the word, then creates a path through the filtered points on the scatterplot.
I've managed to get the code to the point that one can place the search term into the code, and get the desired effect (jsfiddle), but of course, I do not want my users to have to open up a text editor to search the dataset.
I like Gerardo Furtado's method of changing the attribute of the node
d3.select("button").on("click", function() {
var txtName = d3.select("#txtName").node().value;
circles.style("fill", function(d) {
return d.doc === txtName ? "red" : "black";
Based on his answer, I have experimented with placing the code that appends the line to the graph within this piece of code. I get no errors, but I also get no line:
d3.select("button").on("click", function() {
var txtName = d3.select("#txtName")
chartGroup.selectAll(".line")
.data(nest)
.enter()
.filter(function(d){return d.key == txtName;})
.append("path")
.attr("class","line")
.attr("d",function(d) {return line(d.values)})
.attr("stroke", function(d) {return colors(d.key)})
.attr("stroke-width","2px")
.style("stroke-dasharray", ("3, 3"))
I've also looked at Amber Thomas's multi-line graph, but I can't make sense of the code blocks that follow the filter function.
Any help appreciated
There are several different solutions here. You can just draw all the paths, with zero opacity:
var lines = chartGroup.selectAll(".line")
.data(nest)
.enter()
.append("path")
.style("opacity", 0)
.attr("class", "line")
.attr("d", function(d) {
return line(d.values)
})
.attr("stroke", function(d) {
return colors(d.key)
})
.attr("stroke-width", "2px")
.style("stroke-dasharray", ("3, 3"));
And then changing the opacity after the click:
d3.select("button").on("click", function() {
var txtName = d3.select("#txtName").node().value;
circles.style("fill", function(d) {
return d.doc === txtName ? "red" : "black";
})
lines.style("opacity", function(d) {
return d.key === txtName ? 1 : 0;
})
})
Here is the resulting demo:
var parseDate = d3.timeParse("%m/%d/%Y");
mycolour = d3.rgb("#f7f7f7");
var doc = `date number creator doc
6/16/2000 3 molly 3 rat
2/25/2002 4 may 2 cat
12/05/2004 3 molly 4 fish
07/06/2006 1 milly 1 dog
09/07/2003 4 may 4 fish
12/10/2001 4 may 3 rat
6/15/2005 2 maggie 3 rat
06/09/2004 1 milly 4 fish
10/05/2005 1 milly 3 rat
10/07/2003 4 may 1 dog
1/19/2009 4 may 2 cat
10/30/2007 1 milly 4 fish
8/13/2009 4 may 2 cat
9/30/2004 3 molly 1 dog
1/17/2006 4 may 3 rat
12/18/2009 3 molly 1 dog
11/02/2007 2 maggie 3 rat
4/17/2007 1 milly 4 fish`;
var data = d3.tsvParse(doc, function(d) {
return {
creator: d.creator,
date: parseDate(d.date),
number: Number(d.number),
doc: d.doc
};
});
var height = 300;
var width = 500;
function sortByDateAscending(a, b) {
return a.date - b.date;
}
data = data.sort(sortByDateAscending);
margin = {
top: 40,
right: 50,
bottom: 0,
left: 50
};
var minDate = new Date(2000, 1, 1);
var maxDate = new Date(2011, 1, 1);
var y = d3.scalePoint()
.domain(['may', 'milly', 'maggie', 'molly'])
.range([height, 0])
.padding(0.2);
var x = d3.scaleTime()
.domain([minDate, maxDate])
.range([0, width]);
var yAxis = d3.axisLeft(y);
var xAxis = d3.axisBottom(x);
var svg = d3.select("body").append("svg").attr("height", height + 100).attr("width", width + 100);
var chartGroup = svg.append("g").attr("transform", "translate(" + margin.left + "," + margin.top + ")")
var line = d3.line()
.x(function(d) {
return x(d.date);
})
.y(function(d) {
return y(d.creator);
});
var redBox = chartGroup.append("rect")
.attr("y", 0)
.attr("width", width)
.attr("height", height)
.attr("fill", mycolour)
.append("g");
var nest = d3.nest()
.key(function(d) {
return d.doc;
})
.entries(data);
var colors = d3.scaleOrdinal()
.domain(function(d) {
return colors(d.key)
})
.range(["#e66101", "#fdb863", "#b2abd2", "#5e3c99"]);
var line = d3.line()
.x(function(d, i) {
return x(d.date);
})
.y(function(d, i) {
return y(d.creator);
});
var lines = chartGroup.selectAll(".line")
.data(nest)
.enter()
.append("path")
.style("opacity", 0)
.attr("class", "line")
.attr("d", function(d) {
return line(d.values)
})
.attr("stroke", function(d) {
return colors(d.key)
})
.attr("stroke-width", "2px")
.style("stroke-dasharray", ("3, 3"));
var circles = chartGroup.selectAll("circle")
.data(data)
.enter().append("circle")
.attr("cx", function(d) {
return x(d.date);
})
.attr("cy", function(d) {
return y(d.creator);
})
.attr("r", 4)
.style("fill", "black");
d3.select("button").on("click", function() {
var txtName = d3.select("#txtName").node().value;
circles.style("fill", function(d) {
return d.doc === txtName ? "red" : "black";
})
lines.style("opacity", function(d) {
return d.key === txtName ? 1 : 0;
})
})
chartGroup.append("g").attr("class", "x axis").attr("transform", "translate(0," + height + ")").call(d3.axisBottom(x).ticks(14));
chartGroup.append("g").attr("class", "y axis").call(d3.axisLeft(y).ticks(5));
path {
fill: none;
}
/* circle {
fill: #FF00FF;
stroke:navy;
stroke-width:2px; */
}
g.tick text y {
font-size: 30px;
font: Garamond;
}
g.tick text x {
font-size: 10px;
font: Garamond;
}
g.tick line {
display: none;
}
<script src="https://d3js.org/d3.v4.min.js"></script>
<textarea id="txtName" name="txt-Name" placeholder="Search for something.."></textarea>
<button>Try it</button>
On the other hand, if you really want to put that block of code inside the listener, this is the problem:
When you do...
chartGroup.selectAll(".line")
...you are selecting an existing element, so your enter selection is empty. Instead of that, select some new class, like:
chartGroup.selectAll(".lineFiltered")
Here is the updated JSFiddle: https://jsfiddle.net/qmps19ox/1/
The problem with that approach, however, is that the lines will pile up, unless you create a proper enter/update/exit selections.
I have created this curve line chart and I filled by purple color only. Then I decided to add color range and I used color scaling method is Category10 but it is not working. I am new to D3.js and it has been my headache for the past week.
In order to make it look more presentable, I need to add colors and I did tried to add this way by using .style attribute but
function buildLine(data){
var xScale=d3.scale.linear()
.domain([0,d3.max(data,function(d){return d.spend;})])
.range([0, w]);
var yScale=d3.scale.linear()
.domain([0,d3.max(data,function(d){return d.alpha;})])
.range([h/2, 0]);
var yAxisGen=d3.svg.axis().scale(yScale).orient("left");
var xAxisGen=d3.svg.axis().scale(xScale).orient("bottom");
var svg=d3.select("body").append("svg").attr({width:w,height:h});
var yAxis= svg.append("g")
.call(yAxisGen)
.attr("class","axis")
.attr("transform", "translate(50, " +10 +")");
var xAxisTranslate = h/2 + 10;
var xAxis= svg.append("g")
.call(xAxisGen)
.attr("class","axis")
.attr("transform", "translate(50, " + xAxisTranslate +")");
Adding color function here
var color=d3.scale.category10
var lineFun = d3.svg.line()
.x(function (d) { return xScale(d.x); })
.y(function (d) { return yScale(d.y); })
.interpolate("basis");
Here I tried to add it dynamically. '.style does not work. WHy?'
var viz = svg.selectAll("path")
.data(data)
.enter()
.append("path")
.attr({
d: function(d) {
return lineFun(d.arr)
},
"stroke": "purple",
"stroke-width": 2,
"fill": "none",
"class":"line"
})
.style("stroke",function(){
return d.color=color(d.arr);
})
.attr("transform", "translate(48, " + 10 +")")
;
}
d3.json("sample-data.json",function(error,data){
//check the file loaded properly
if (error) { //is there an error?
console.log(error); //if so, log it to the console
} else { //If not we're golden!
//Now show me the money!
ds=data; //put the data in the global var
}
data.forEach(function(jsonData){
var lineData = d3.range(0, jsonData.spend, 100)
.map(x => [x, (jsonData.alpha * (1 - Math.pow(2.71828, (-jsonData.beta * x))))] );
/* console.log("this is the data:",lineData);*/
//i think line date returns an array with each item in it also an array of 2 items
var arr = [];
for (var i = 0; i < lineData.length; i++) {
arr.push({
x: lineData[i][0],
y: lineData[i][1]
});
}
jsonData.arr = arr;
console.log(jsonData);
});
buildLine(data);
});
These are the problems:
You have to call the scale function:
var color=d3.scale.category10()
//parentheses here ----------^
You cannot use the argument d if you don't set the anonymous function parameter:
.style("stroke",function(d){
//parameter here-----^
return d.color=color(d.arr);
})
scale.category10() works on a "first come, first served" basis. You just need the index:
.style("stroke",function(d,i){
return d.color=color(i);
});
Here is a demo showing how to use that scale:
var color = d3.scale.category10();
var divs = d3.select("body").selectAll(null)
.data(d3.range(10))
.enter()
.append("div")
.style("background-color", function(d, i) {
return color(i)
})
div {
width: 20px;
height: 20px;
display: inline-block;
margin: 2px;
}
<script src="https://d3js.org/d3.v3.min.js"></script>
I am working on a d3.js Dashboard (so only one dashboard and many layouts). I wanted to display a scatterplot animated, with the dots moving regarding some filter I am applying.
But in order, I have to display something first which is not happening.
Here are my program, which is a scatterplot.js file in an MVC format.
function scatterplot(DOMElement){
var scatObj = {}; // main object
scatObj.setTooltipText = function(f){
tooltipText = f;
//updateInteractions();
return scatObj;
}
scatObj.setMouseclickCallback = function(f){
mouseclickCallback = f;
//updateInteractions();
return scatObj;
}
// function loading data and rendering it in scatter chart
// parameters:
// - dataset in following format:
// [{"key": year, "value": money}, {...}, ...]
// returns:
// - scatObj for chaining
scatObj.loadAndRender = function(data){
dataset = data;
render();
return scatObj;
}
// ---- PRIVATE VARIABLES
// sizing vars
var dataset = []; // Initialize empty array
var numDataPoints = dataset.length;
for(var i=0; i<numDataPoints; i++) {
var newNumber1 = d.value.x; // New random integer
var newNumber2 = d.value.y; // New random integer
dataset.push([newNumber1, newNumber2]); // Add new number to array
}
// Setup settings for graphic
var canvas_width = 500;
var canvas_height = 300;
var padding = 30; // for chart edges
// Create scale functions
var xScale = d3.scaleLinear() // xScale is width of graphic
.domain([0, d3.max(dataset, function(d) {
return d[0]; // input domain
})])
.range([padding, canvas_width - padding * 2]); // output range
var yScale = d3.scaleLinear() // yScale is height of graphic
.domain([0, d3.max(dataset, function(d) {
return d[1]; // input domain
})])
.range([canvas_height - padding, padding]); // remember y starts on top going down so we flip
// scales
// Define X axis
var xAxis = d3.axisBottom()
.scale(xScale)
// Define Y axis
var yAxis = d3.axisLeft()
.scale(yScale)
var svg = d3.select("DOMElement") // This is where we put our vis
.append("svg")
.attr("width", canvas_width)
.attr("height", canvas_height);
var tooltip = d3.select(DOMElement).append("div")
.classed("tooltip", true);
// interaction settings
var tooltipText = function(d, i){return "tooltip over element "+i;}
var mouseoverCallback = function(d, i){ };
var mouseoutCallback = function(d, i){ };
var mouseclickCallback = function(d, i){ console.log(d,i)};
var keySelected = null;
// ---- PRIVATE FUNCTIONS
function render(){
GUP_scat();
}
function GUP_scat(){
// GUP
// Create Circles
svg.selectAll("circle")
.data(dataset)
.enter()
.append("circle") // Add circle svg
.attr("cx", function(d) {
return xScale(d[0]); // Circle's X
})
.attr("cy", function(d) { // Circle's Y
return yScale(d[1]);
})
.attr("r", 2); // radius
// Add to X axis
svg.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + (canvas_height - padding) +")")
.call(xAxis);
// Add to Y axis
svg.append("g")
.attr("class", "y axis")
.attr("transform", "translate(" + padding +",0)")
.call(yAxis);
// On click, update with new data
d3.select(DOMElement)
.on("click", function() {
keySelected = (keySelected == d.key) ? null : d.key;
var numValues = dataset.length; // Get original dataset's length
dataset = []; // Initialize empty array
for(var i=0; i<numValues; i++) {
var newNumber1 = d.value.x; // Random int for x
var newNumber2 = d.value.y; // Random int for y
dataset.push([newNumber1, newNumber2]); // Add new numbers to array
}
// Update scale domains
xScale.domain([0, d3.max(dataset, function(d) {
return d[0]; })]);
yScale.domain([0, d3.max(dataset, function(d) {
return d[1]; })]);
// Update circles
svg.selectAll("circle")
.data(dataset) // Update with new data
.transition() // Transition from old to new
.duration(1000) // Length of animation
.each("start", function() { // Start animation
d3.select(this) // 'this' means the current element
.attr("fill", "red") // Change color
.attr("r", 5); // Change size
})
.delay(function(d, i) {
return i / dataset.length * 500; // Dynamic delay (i.e. each item delays a little longer)
})
//.ease("linear") // Transition easing - default 'variable' (i.e. has acceleration), also: 'circle', 'elastic', 'bounce', 'linear'
.attr("cx", function(d) {
return xScale(d[0]); // Circle's X
})
.attr("cy", function(d) {
return yScale(d[1]); // Circle's Y
})
.each("end", function() { // End animation
d3.select(this) // 'this' means the current element
.transition()
.duration(500)
.attr("fill", "black") // Change color
.attr("r", 2); // Change radius
});
// Update X Axis
svg.select(".x.axis")
.transition()
.duration(1000)
.call(xAxis);
// Update Y Axis
svg.select(".y.axis")
.transition()
.duration(100)
.call(yAxis);
mouseclickCallback(d, i);
});}
return scatObj; // returning the main object
}
I am calling it like this in my main.js file :
dash.append('div').attr('id', 'scat1').classed("scatterplot", true)
.append("h3").text("Per Scateub");
var scat1 = scatterplot("div#scat1")
.setTooltipText(function(d, i){
return "<b>"+d.data.key +" : "+d3.format(",.2f")(d.value)+"</b> /10 ";
})
.loadAndRender(dataManager.getFteOverall());
scat1.setMouseclickCallback(function(d, i){
dataManager.setNameFilter(d.key);
redraw();
})
and here is my nest :
dmObj.getFteOverall= function(){
var nestedData = d3.nest()
.key(function(d){ return d.naAsses;})
.rollup(function (v) { return {
x: d3.sum(v,function(d){
return (d.four*4,d.three*3,d.two*2,d.one)/100 }),
y: d3.sum(v, function(d){
return (d.fte)
}
)
};
})
.sortKeys(d3.ascending)
.entries(filteredData());
return nestedData;
}
I know it can seem a bit a stupid question, but I have been struggling with it, and I have already been stuck for a while.
I hope I am clear and you will be able to help me (just in case I am working with d3 V4).
Thanks to advance guys.
I have an array of equally spaced values which I am using to draw concentric circles. I want to use an emanating effect, in essence, remove the outermost circle once its value exceeds the maximum value and add a new one at the center to compensate. I am unsure about manipulation on data set to remove and add new circle.
<script src="https://d3js.org/d3.v4.min.js"></script>
<script>
var svg = d3.select("body").append("svg");
var w = window.innerWidth;
var h = window.innerHeight;
var separation = Math.min(50, w/12);
var n=Math.floor((w/2)/separation);
var ellipse=new Array(n);
for(var i=1;i<=n;i++){
ellipse[i-1]=(i*separation);
}
svg.attr("width", w).attr("height", h);
var g = svg.append("g");
var e=g.selectAll("ellipse")
.data(ellipse)
.enter()
.append("ellipse")
.attr("cx", w/2)
.attr("cy", h/2)
.attr("rx",0)
.attr("ry",0)
.transition()
.duration(5000)
.attr("rx", function(d,i){return ellipse[i];})
.attr("ry", function(d,i){return ellipse[i]/5;});
loop();
function loop(){
e.transition()
.attr("rx", function(d,i){
return ellipse[i]+separation;
})
.attr("ry", function(d,i){
return (ellipse[i]+separation)/5;
})
.on("end",loop());
}
</script>
You could approach it with a remove().exit() and enter().append() selection for each ring - but essentially you always have the same number of rings on the screen. Why not just recycle the same elements? When the size hits a threshold, reset it to zero, or some other starting value?
Something along the lines of:
var scale = d3.scaleLinear()
.range(["orange","steelblue","purple"])
.domain([0,60]);
var data = [0,10,20,30,40,50,60];
var width = 200;
var height = 200;
var svg = d3.select("body")
.append("svg")
.attr("width",width)
.attr("height",height);
var circles = svg.selectAll("circle")
.data(data)
.enter()
.append("circle")
.attr("r",function(d) { return d; })
.attr("transform","translate(80,80)")
.attr("fill","none")
.style("stroke-width","4")
.style("stroke",function(d) { return scale(d) });
function transition() {
// Update data, max d is 60:
data = data.map(function(d) { return d == 60 ? 0 : d + 10});
var i = 0;
// Grow circles
circles
.data(data)
.filter(function(d) { return d > 0 })
.transition()
.ease(d3.easeLinear)
.attr("r", function(d) { return d; })
.style("stroke", function(d) { return scale(d) })
.style("opacity",function(d) { return d == 60 ? 0 : 1 })
.duration(1000)
.on("end",function(){if(++i==circles.size()-1) { transition(); } });
// Reset circles where r == 0
circles
.filter(function(d) { return d == 0 })
.attr("r", 0)
.style("opacity",1)
.style("stroke",function(d) { return scale(d); });
}
transition();
<script src="https://d3js.org/d3.v4.min.js"></script>
Note that .on("end",... triggers on each element's transition end - this is why I count to see if all elements are done transitioning before running the transition function again.
I read "Interactive Data Visualization for the web" by Scott Murray, but this book use d3 version3. I've tried to fix it, but some problems happen, and my code is below. I got errors about "y: Expected length, "NaN".", and maybe my stack function doesn't work. However, I don't know how to solve it. I need someone to help me.
// declare variable
var svgWidth = 500,
svgHeight = 300,
svgData = [],
maxValue = 16;
svgData = getData(svgData);
// set stack color
var color = d3.scaleOrdinal(d3.schemeCategory10);
// create stack layout
var stack = d3.stack();
stack(svgData);
// define x,y scale
var xScale = d3.scaleBand()
.domain(d3.range(svgData[0].length))
.rangeRound([0, svgWidth])
.paddingInner(0.05),
yScale = d3.scaleLinear()
.domain([0, d3.max(svgData, function(d){
return d3.max(d, function(d){
d.y0 + d.y;
});
})])
.range([0, svgHeight])
.clamp(true);
// create svg
var svg = d3.select("body")
.append("svg")
.attr("width", svgWidth)
.attr("height", svgHeight);
// add group and fill color for each row of data
var group = svg.selectAll("g")
.data(svgData)
.enter()
.append("g")
.style("fill", function(d, i){
return color(i);
});
// add a rect for each data value
var rect = group.selectAll("rect")
.data(function(d){
return d;
})
.enter()
.append("rect")
.attr("x", function(d, i){
return xScale(i);
})
.attr("y", function(d){
return yScale(d.y0);
})
.attr("width", xScale.bandwidth())
.attr("height", function(d){
return yScale(d.y);
});
// get data
function getData(data){
var temp =0,
tempArr = [];
data = [];
for(var i=0; i<3; i++){
tempArr = [];
for(var j=0; j<5; j++){
temp = Math.round(Math.random() *maxValue);
tempArr.push( { x: j, y: temp });
}
data.push(tempArr);
}
return data;
}