How to modify D3js graph depending on change of value? - javascript

I have a graph which I build using d3js and I want to draw red line or other kind of indicator on the graph to indicate when the graphs blue line's value hits 0(in this case its 6th of june). The example of a graph http://jsfiddle.net/wRDXt/81/
My starting point is:
if(dataset.value = 0){
//draw line
}
But I have no clue where to go from here. Any help would be greatly appreciated.

There are a few different ways to do this. One way would be to filter the dataset to create a new array that only had the entries with a value of 0.
var zeroData = dataset.filter(function(d){
if (d.value == 0) return d;
});
This new array could be used to draw lines at each zero point with a separate svg.selectAll(".redlines") using the new array as the data.
// plot lines
svg.selectAll(".redline")
.data(zeroData)
.enter()
.append("line")
.attr("class", "redline")
.attr("x1", function(d) { return xScale(d.date); })
.attr("x2", function(d) { return xScale(d.date); })
.attr("y1", 0)
.attr("y2", 250)
.style("stroke", "#ff0000");
http://jsfiddle.net/wRDXt/85/
Another way is to start by appending a "g" element for each data point instead of a circle then adding a circle and a line for each point. Styling can be used to make only the value == 0 points visible.
// plot data points as g elements
var datapts = svg.selectAll(".data-point")
.data(dataset);
var dataptsEnter = datapts
.enter()
.append("g")
.attr("class", "data-point")
.attr("transform", function(d) {
return "translate(" + xScale(d.date) + "," + yScale(d.value) + ")";
});
dataptsEnter
.append("circle")
.attr("class", "data-point-circle")
.attr("cx", 0)
.attr("cy", 0)
.attr("r", 5);
dataptsEnter
.append("line")
.attr("x1", 0)
.attr("y1", 0)
.attr("x2", 0)
.attr("y2", -50)
.style("stroke", "#ff0000")
.style("opacity", function(d) {
if (d.value == 0) return 1;
return 0;
});
http://jsfiddle.net/wRDXt/82/
In the second approach, there is a red line for each datapoint but opacity is used to make only the zero value visible.
The first approach is the one I'd use.

Related

Add styling to a column of a D3 heatmap programmatically

I've got a basic heatmap from here.
Now I want to be able to highlight a whole column of the plot, like drawing a rectangle around all values (Could also be something simpler):
I'm also using react to keep track of which column should be highlighted.
Therefore I need to be able to change this highlighting programmatically without any mouse actions.
Does anyone know how to style a whole column without using mouse events? is that even possible?
You can append a highlight element when you're creating your heatmap, and update the position/opacity of the highlight element when your variable changes.
Note that you will also need to store the d3 scale function in a variable.
const svg = "however you're selecting the svg here"
svg.append('rect')
.attr("id", "highlight")
.attr("width", xScale.bandwidth())
.attr("height", 'height of the heatmap')
.attr("x", 0)
.attr("y", 0)
.attr("opacity", 0)
.attr("stroke", '#ffffff')
.attr("stroke-width", 2)
When the variable is changed programatically, use d3 to select that element and update its position
function changePosition(column) {
const svg = "however you're selecting the svg here"
const xScale = "that variable where you put the xScale function when you appended your heatmap"
svg.select("#highlight")
.attr("x", xScale(column))
.attr("opacity", 1)
}
I solved this by using mouseover. You can get coordinates and size of a single rect by using:
var mouseover = function(event, d) {
x_coordinate = this.x.animVal.value
y_coordinate = his.y.animVal.value
height_of_rect = this.height.animVal.value
width_of_rect = this.width.animVal.value
}
To draw a line over a heatmap you need to apppend that line on its parent component. I used svg:
const drawLine = function(x, rectWidth) {
svg.append('line')
.style("stroke", "black")
.style("stroke-width", 2)
.attr("x1", x)
.attr("y1", 0)
.attr("x2", x)
.attr("y2", height);
svg.append('line')
.style("stroke", "black")
.style("stroke-width", 2)
.attr("x1", x+rectWidth)
.attr("y1", 0)
.attr("x2", x+rectWidth)
.attr("y2", height);
svg.append('line')
.style("stroke", "black")
.style("stroke-width", 2)
.attr("x1", x)
.attr("y1", 0)
.attr("x2", x+rectWidth)
.attr("y2", 0);
svg.append('line')
.style("stroke", "black")
.style("stroke-width", 2)
.attr("x1", x)
.attr("y1", height)
.attr("x2", x+rectWidth)
.attr("y2", height);
}
drawLine function will vertically highlight a column of a rect and it is placed inside of mouseover. Here x is x coordinate, rectWidth is width of one rect component and height is heat map height. To remove highlight put in mouseleave function:
d3.selectAll("line").remove()

Grid-lines are being recreated and overlaid on each other

I'm playing with some d3 code - to create the y axis I do the following :
function renderYAxis(svg) {
var yAxis = d3.svg.axis()
.orient("left")
.scale(_y.range([quadrantHeight(), 0]))
.tickFormat(d3.format("s"));
axisData = _currentData.filter(function(row) {
if ((row['filter1'] === _filter1)) {
return true;
}
}).filter(function(row) {
if ((row['filter2'] === _filter2)) {
return true;
}
}).map(function(d) {
return {
y: +d["Y"]
};
});
var minY2 = d3.min(axisData, function(d) { return d.y });
if (minY2 > 0) {
minY2 = 0;
};
_y.domain([minY2, d3.max(axisData, function(d) { return d.y })])
if (!_axesYG) {
_axesYG = svg
.append("g")
.attr("class", "y axis");
}
_axesYG
.attr("transform", function() {
return "translate(" + xStart() + "," + yEnd() + ")";
})
.transition()
.duration(1000)
.call(yAxis);
//>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> maybe following needs changing somehow? >>>>>>>>>>>>>>
d3.selectAll("g.y g.tick")
.append("line")
.classed("grid-line", true)
.attr("x1", 0)
.attr("y1", 0)
.attr("x2", quadrantWidth())
.attr("y2", 0);
}
The chart has a transition but after transitioning several times some of the grid lines are reproducing and being laid on top of each other - so producing some thicker lines. I've marked above where I think the problem may be, I'm unsure how to change this code - is there a standard approach?
A full working example of the behavior is saved here: http://plnkr.co/edit/JD52TfAddZSpNR3oaMRv?p=preview
If you hit the button several times you will see it is the common grid lines that are shared before and after the transition that are being recreated and overlaid. These two:
Any help much appreciated.
An easy solution is just setting the tick width with a negative value:
.innerTickSize(-quadrantWidth());
That way, you don't have to worry about appending, removing or updating the lines, and you won't have duplicated elements: the axis generator takes care of all that for you.
Here is the updated plunker: http://plnkr.co/edit/BoP4hEkILlwJzRuCJFBD?p=preview
EDIT: you mentioned in your answer that you're having problems with Nick Zhu's approach. That's because your selection is not correct. It should be something like this:
var lines = d3.selectAll("g.y g.tick")
lines.selectAll(".grid-line")
.remove();
lines.append("line")
.classed("grid-line", true)
.attr("x1", 0)
.attr("y1", 0)
.attr("x2", quadrantWidth())
.attr("y2", 0);
Here is the respective plunker: http://plnkr.co/edit/189hJBepdVVreLghBgc0?p=preview
Here is a simple fix (hack), since the original code structure is hard to change to follow General Update Pattern correctly:
// remove old ones
d3.selectAll(".grid-line.y-axis")
.remove();
// draw new ones
// add a new class y-axis to avoid deleting the x axis above
d3.selectAll("g.y g.tick")
.append("line")
.classed("grid-line y-axis", true)
.attr("x1", 0)
.attr("y1", 0)
.attr("x2", quadrantWidth())
.attr("y2", 0);
http://plnkr.co/edit/wdQmllRrrILtXsarXqLY?p=preview
The more correct approach is to follow the General Update Pattern: https://bl.ocks.org/mbostock/3808234
just for completeness I thought I'd add the following, which I found in Nick Qi Zhu's book. I think it follows the general update pattern as well as grid-lines can. Although even adding this I still get a reproduction of the grid-lines!
function renderYGridlines() {
var lines = d3.selectAll("g.y g.tick")
.select("grid-line y-axis")
.remove();
lines = d3.selectAll("g.y g.tick")
.append("line")
.classed("grid-line", true)
lines.attr("x1", 0)
.attr("y1", 0)
.attr("x2", quadrantWidth())
.attr("y2", 0);
}

D3JS Scatter plots refresh speed

I was wondering if there is a way to change the Scatter plots refresh speed?
As you can see in this link the scatter plots gets updated but the time gap between the appearance and disappearance is unreasonable, it look like they are flashing dots.... I tried moving the circle.remove() function right above the circle.transition but it makes no difference.
Below is the relevant code of the refresh function. Thanks!
function updateData() {
// Get the data again
data = d3.json("2301data.php", function(error, data) {
data.forEach(function(d) {
d.dtg = parseDate(d.dtg);
d.temperature = +d.temperature;
// d.hum = +d.hum; // Addon 9 part 3
});
// Scale the range of the data again
x.domain(d3.extent(data, function(d) { return d.dtg; }));
y.domain([0, 60]);
var svg = d3.select("#chart1").select("svg").select("g");
svg.select(".x.axis") // change the x axis
.transition()
.duration(750)
.call(xAxis);
svg.select(".y.axis") // change the y axis
.transition()
.duration(750)
.call(yAxis);
svg.select(".line") // change the line
.transition()
.duration(750)
.attr("d", valueline(data));
var circle = svg.selectAll("circle").data(data);
circle.remove() //remove old dots
// enter new circles
circle.enter()
.append("circle")
.filter(function(d) { return d.temperature > 35 })
.style("fill", "red")
.attr("r", 3.5)
.attr("cx", function(d) { return x(d.dtg); })
.attr("cy", function(d) { return y(d.temperature); })
// Tooltip stuff after this
.on("mouseover", function(d) {
div.transition()
.duration(500)
.style("opacity", 0);
div.transition()
.duration(200)
.style("opacity", .9);
div .html(
d.temperature + "C" + "<br>" +
formatTime(d.dtg))
.style("left", (d3.event.pageX + 8) + "px")
.style("top", (d3.event.pageY - 18) + "px");})
.on("mouseout", function(d) {
div.transition()
.duration(500)
.style("opacity", 0);
});
circle.transition().attr("cx", function(d) { return x(d.dtg); });
// exit
circle.exit();
});
}
Looking at your example as it runs, you appear to have loads more circles in the dom than are visible. This is because you add circles for all the data, but then only give positions to those that meet the filter criteria you set.
There was a related question the other day about data filtering versus d3 filtering - Filtering data to conditionally render elements . Use data filtering if you don't want to add something full stop, use d3.filter if you want to isolate some elements for special treatment (transitions, different styling etc).
At the moment you're filtering the d3 selection once all the circles are added, but in your case I'd suggest filtering the data before it gets to that stage is best (and as suggested by others in that other question). This may make it run faster (but you're also at the mercy of db updates by the look of your example?)
data = data.filter (function(d) { return d.temperature > 35; }); // do filtering here
var circle = svg.selectAll("circle").data(data);
circle.exit().remove() //remove old dots
// enter new circles
circle.enter()
.append("circle")
.style("fill", "red")
.attr("r", 3.5)
.attr("cx", function(d) { return x(d.dtg); })
.attr("cy", function(d) { return y(d.temperature); })
...
PS. It's a bit confusing what you're trying to do with the circle.remove() and circle.exit(). circle.remove() will remove all existing circles (even ones that exist and have new data), circle.exit() at the end will then have no effect. I'd just have circle.exit().remove() to replace the two calls you make.
Also, without a key function - https://bost.ocks.org/mike/constancy/ - on your .data() call, you may find dots move around a bit. If your data points have ids, use them.
var circle = svg.selectAll("circle").data(data, function(d) { return d.id; /* or d.dtg+" "+d.temperature; if no id property */});
Thanks to mgraham the problem was solved.! Below is the revised code in case someone else needs it.
function updateData() {
// Get the data again
data = d3.json("data.php", function(error, data) {
data.forEach(function(d) {
d.dtg = parseDate(d.dtg);
d.temperature = +d.temperature;
});
// Scale the range of the data again
x.domain(d3.extent(data, function(d) { return d.dtg; }));
y.domain([0, 60]); // Addon 9 part 4
var svg = d3.select("#chart1").select("svg").select("g");
svg.select(".x.axis") // change the x axis
.transition()
.duration(750)
.call(xAxis);
svg.select(".y.axis") // change the y axis
.transition()
.duration(750)
.call(yAxis);
svg.select(".line") // change the line
.transition()
.duration(750)
.attr("d", valueline(data));
data = data.filter (function(d) { return d.temperature > 35; });
var circle = svg.selectAll("circle").data(data, function(d) { return d.dtg+" "+d.temperature;});
circle.exit().remove() //remove old dots
// enter new circles
circle.enter()
.append("circle")
.style("fill", "red")
.attr("r", 3.5)
.attr("cx", function(d) { return x(d.dtg); })
.attr("cy", function(d) { return y(d.temperature); })
// Tooltip stuff after this
.on("mouseover", function(d) {
div.transition()
.duration(500)
.style("opacity", 0);
div.transition()
.duration(200)
.style("opacity", .9);
div .html(
d.temperature + "C" + "<br>" +
formatTime(d.dtg))
.style("left", (d3.event.pageX + 8) + "px")
.style("top", (d3.event.pageY - 18) + "px");})
.on("mouseout", function(d) {
div.transition()
.duration(500)
.style("opacity", 0);
});
circle.transition().attr("cx", function(d) { return x(d.dtg); });
});
}
</script>

Points on a map with D3.js

I have a geoJSON of zip code centroid points that I am plotting on a D3.js map. I can get them to display but I am unable to adjust the size of the points. I was assuming that .attr("r", 1) would be doing that but I must be missing something.
d3.json("ZipPoints.json", function (zipPoints) {
svg.selectAll("g")
.data(zipPoints.features)
.enter()
.append("path")
.attr("d", path)
.attr("r", 1)
.style("fill", "red");
});
EDIT:
d3.json("ZipPoints.json", function (zipPoints) {
points.selectAll("circle")
.data(zipPoints.features)
.enter()
.append("circle")
.attr("r", 1.5)
.attr("transform", function(d, i) {
return "translate(" + projection(zipPoints.features[i].geometry.coordinates) + ")";
})
.style("fill", "red")
.classed("point", true);
});
You could try the following.
var pins = svg.append("g");
d3.json("ZipPoints.json", function(zipPoints) {
pins.selectAll("circle")
.data(zipPoints.features)
.enter()
.append("circle")
.attr("r", 5)
.style("fill", "red")
.classed("pin", true);
});
You may need transformation on these points to render them correctly (I guess).
In that case you could use the following bit of code. (The transformation function I used was required to plot data that had lat, long information on a map built using a specific projection).
.attr("transform", function(d) {
/*whatever transformation that needs to be done*/
return "translate(" + projection([ d.lon, d.lat ]) + ")";
})

d3js: adding same type of elements with different data

//add circles with price data
svgContainer.selectAll("circle")
.data(priceData)
.enter()
.append("svg:circle")
.attr("r", 6)
.style("fill", "none")
.style("stroke", "none")
.attr("cx", function(d, i) {
return x(convertDate(dates[i]));
})
.attr("cy", function(d) { return y1(d); })
//add circles with difficulty data
svgContainer.selectAll("circle")
.data(difficultyData)
.enter()
.append("svg:circle")
.attr("r", 6)
.style("fill", "none")
.style("stroke", "none")
.attr("cx", function(d, i) {
return x(convertDate(dates[i]));
})
.attr("cy", function(d) { return y2(d); })
In the first half, circles with price data are added along the relevant line in the graph chart. Now I want to do the same with the second half to add circles with different data to a different line. However, the first circles' data are overwritten by the second circles' data, and the second circles never get drawn.
I think I have a gut feeling of what's going on here, but can someone explain what exactly is being done and how to solve the problem?
possible reference:
"The key function also determines the enter and exit selections: the
new data for which there is no corresponding key in the old data
become the enter selection, and the old data for which there is no
corresponding key in the new data become the exit selection. The
remaining data become the default update selection."
First, understand what selectAll(), data(), enter() do from this great post.
The problem is that since circle element already exists by the time we get to the second half, the newly provided data simply overwrites the circles instead of creating new circles. To prevent this from happening, you need to specify a key function in data() function of the second half. Then, the first batch of circles do not get overwritten.
//add circles with price data
svgContainer.selectAll("circle")
.data(priceData)
.enter()
.append("svg:circle")
.attr("r", 6)
.style("fill", "none")
.style("stroke", "none")
.attr("cx", function(d, i) {
return x(convertDate(dates[i]));
})
.attr("cy", function(d) { return y1(d); })
//add circles with difficulty data
svgContainer.selectAll("circle")
.data(difficultyData, function(d) { return d; }) // SPECIFY KEY FUNCTION
.enter()
.append("svg:circle")
.attr("r", 6)
.style("fill", "none")
.style("stroke", "none")
.attr("cx", function(d, i) {
return x(convertDate(dates[i]));
})
.attr("cy", function(d) { return y2(d); })
you can append the circles in two different groups, something like:
//add circles with price data
svgContainer.append("g")
.attr("id", "pricecircles")
.selectAll("circle")
.data(priceData)
.enter()
.append("svg:circle")
.attr("r", 6)
.style("fill", "none")
.style("stroke", "none")
.attr("cx", function(d, i) {
return x(convertDate(dates[i]));
})
.attr("cy", function(d) { return y1(d); })
//add circles with difficulty data
svgContainer.append("g")
.attr("id", "datacircles")
.selectAll("circle")
.data(difficultyData)
.enter()
.append("svg:circle")
.attr("r", 6)
.style("fill", "none")
.style("stroke", "none")
.attr("cx", function(d, i) {
return x(convertDate(dates[i]));
})
.attr("cy", function(d) { return y2(d); })
if the circles are in different groups they won't be overwritten
I had the same question as OP. And, I figured out a solution similar to tomtomtom above. In short: Use SVG group element to do what you want to do with different data but the same type of element. More explanation about why SVG group element is so very useful in D3.js and a good example can be found here:
https://www.dashingd3js.com/svg-group-element-and-d3js
My reply here includes a jsfiddle of an example involving 2 different datasets both visualized simultaneously on the same SVG but with different attributes. As seen below, I created two different group elements (circleGroup1 & circleGroup2) that would each deal with different datasets:
var ratData1 = [200, 300, 400, 600];
var ratData2 = [32, 57, 112, 293];
var svg1 = d3.select('body')
.append('svg')
.attr('width', 500)
.attr('height', 400);
var circleGroup1 = svg1.append("g");
var circleGroup2 = svg1.append("g");
circleGroup1.selectAll("circle")
.data(ratData1)
.enter().append("circle")
.attr("cy", 60)
.attr("cx", function(d, i) { return i * 100 + 30; })
.attr("r", function(d) { return Math.sqrt(d); });
circleGroup2.selectAll("circle")
.data(ratData2)
.enter()
.append("circle")
.attr("r", function(d, i){
return i*20 + 5;
})
.attr("cy", 100)
.attr("cx", function(d,i){ return i*100 +30;})
.style('fill', 'red')
.style('fill-opacity', '0.3');
What is happening is that you are:
In the FIRST HALF:
getting all circle elements in the svg container. This returns nothing because it is the first time you're calling it so there are no circle elements yet.
then you're joining to data (by index, the default when you do not specify a key function). This puts everything in the priceData dataset in the "enter" section.
Then you draw your circles and everything is happy.
then, In the SECOND SECTION:
You are again selecting generically ALL circle elements, of which there are (priceData.length) elements already existing in the SVG.
You are joining a totally different data set to these elements, again by index because you did not specify a key function. This is putting (priceData.length) elements into the "update section" of the data join, and either:
if priceData.length > difficultyData.length, it is putting (priceData.length - difficulty.length) elements into the "exit section"
if priceData.length < difficultyData.length, it is putting (difficulty.length - priceData.length) elements into the "enter section"
Either way, all of the existing elements from the first "priceData" half are reused and have their __data__ overwritten with the new difficultyData using an index-to-index mapping.
Solution?
I don't think a key function is what you are looking for here. A key function is way to choose a unique field in the data on which to join data instead of index, because index does not care if the data or elements are reordered, etc. I would use this when i want to make sure a single data set is correctly mapped back to itself when i do a selectAll(..).data(..).
The solution I would use for your problem is to group the circles with a style class so that you are creating two totally separate sets of circles for your different data sets. See my change below.
another option is to nest the two groups of circles each in their own "svg:g" element, and set a class or id on that element. Then use that element in your selectAll.. but generally, you need to group them in some way so you can select them by those groupings.
//add circles with price data
svgContainer.selectAll("circle.price")
.data(priceData)
.enter()
.append("svg:circle")
.attr("class", "price")
.attr("r", 6)
.style("fill", "none")
.style("stroke", "none")
.attr("cx", function(d, i) {
return x(convertDate(dates[i]));
})
.attr("cy", function(d) { return y2(d); })
//add circles with difficulty data
svgContainer.selectAll("circle.difficulty")
.data(difficultyData)
.enter()
.append("svg:circle")
.attr("class", "difficulty")
.attr("r", 6)
.style("fill", "none")
.style("stroke", "none")
.attr("cx", function(d, i) {
return x(convertDate(dates[i]));
})
.attr("cy", function(d) { return y2(d); })
Using this method, you will always be working with the correct circle elements for the separate datasets. After that, if you have a better unique value in the data than simply using the index, you can also add a custom key function to the two .data(..) calls.

Categories