I've built out a heatmap with D3 in React and the basic result works fine: the rects are rendering and so are the conditional color values, based on my data.
Here is roughly what it looks like:
const dayRects = bounds.selectAll("rect").data(processed)
dayRects
.join("rect")
.attr("x", d => xScale(d.exampleData))
.attr("width", ordinalScale.bandwidth)
.attr("y", d => yScale(d.exampleData))
.attr("height", ordinalScale.bandwidth)
.attr("rx", 3)
.style("fill", d => colorScale(d.moreData))
What I'm struggling to do and cannot find guidance for online, is to conditionally add elements based on data. For example, I'm trying to add an additional rect for items in my array of data that meet certain conditions. This additional rect would be slightly larger than the above rects, have a transparent fill, and a different colored stroke - resulting in, for example, something like this:
When I attempt to do this with something like below the above code
const dayStarted = bounds
.select("rect")
.data(processed.filter(i => i.value === desiredValue))
.join("rect")
.attr("x", d => xScale(d.exampleData))
.attr("width", ordinalScale.bandwidth)
.attr("y", d => yScale(d.exampleData))
.attr("rx", 20)
... it just manipulates the first rect element in the first set of rects (dayRects).
You should bind your data to a g element, position that and then add the rects as sibling in the g.
Here's a quick example:
<!DOCTYPE html>
<html>
<head>
<script src="https://d3js.org/d3.v6.min.js"></script>
</head>
<body>
<svg width="125" height="125"></svg>
<script>
var processed = [
{ x: Math.random() * 100, y: Math.random() * 100, value: 1 },
{ x: Math.random() * 100, y: Math.random() * 100, value: 0 },
];
var bounds = d3.select('svg');
const g = bounds
.selectAll('g')
.data(processed)
.join('g')
.attr('transform', (d) => {
// use your scales to position g
return "translate(" + d.x + "," + d.y + ")";
});
g.append('rect') // first rect
.attr('width', 20)
.attr('height', 20)
.attr('rx', 3)
.style('fill', 'orange');
g.filter( d => d.value == 1) //apply filter on g for ones that get 2nd rect
.append('rect')
.attr('width', 20)
.attr('height', 20)
.attr('rx', 3)
.style('fill', 'none')
.style('stroke', 'steelblue')
.style('stroke-width', '3px');
</script>
</body>
</html>
Related
I'm using react and d3, trying to create a simple bar chart that updates the chart when data is refreshed. The chart is updating when the data changes, but it seems to be layering on top of the old chart. I think the issue is with the d3 exit().remove() function.
As I understand it d3's exit method should return an array of items to be removed, however when I console log it I see an array of "undefined"s. I'm super grateful for any help!
Here is the codesandbox:
https://codesandbox.io/s/gifted-field-n66hw?file=/src/Barchart.js
Here is the code snippet:
import React, { useEffect, useRef } from "react";
import * as d3 from "d3";
const BarChart = props => {
const { randomData, width, height, padding } = props;
const ref = useRef(null);
function colorGradient(v) {
return "rgb(0, " + v * 5 + ", 0";
}
//insert & remove elements using D3
useEffect(() => {
if (randomData.length > 0 && ref.current) {
const group = d3.select(ref.current);
// the data operator binds data items with DOM elements
// the resulting selection contains the enter and exit subselections
const update = group
.append("g")
.selectAll("rect")
.data(randomData);
let bars = update
.enter() // create new dom elements for added data items
.append("rect")
.merge(update)
.attr("x", (d, i) => i * (width / randomData.length))
.attr("y", d => height - d * 5)
.attr("width", width / randomData.length - padding)
.attr("height", d => d * 5)
.attr("fill", d => colorGradient(d));
let labels = update
.enter()
.append("text")
.text(d => d)
.attr("text-anchor", "middle")
.attr(
"x",
(d, i) =>
i * (width / randomData.length) +
(width / randomData.length - padding) / 2
)
.attr("y", d => height - d * 5 + 12)
.style("font-family", "sans-serif")
.style("font-size", 12)
.style("fill", "#ffffff");
update.exit().remove();
}
}, [randomData, height, padding, width]);
return (
<svg width={width} height={height}>
<g ref={ref} />
</svg>
);
};
export default BarChart;
Everytime you update the chart you run this:
const update = group
.append("g") // create a new g
.selectAll("rect") // select all the rectangles in that g (which are none)
.data(randomData);
update is now an empty selection, there are no rects to select in the newly created g. So when we use update.enter(), a DOM element is created for every item in the data array. Using enter will create an element for every item in the data array that doesn't have a corresponding element already.
update.exit() will be empty, because there are no elements selected in update, so nothing will be removed. Previously created bars are not touched, you aren't selecting them.
If we change your code just to remove the .append("g"), it gets us closer to working (eg). The bars were colored white, so they were not visible, I've changed the fill color so the update selection is visible
If we remove .append("g") we have some other problems on update now:
you are not exiting text (as you are not selecting text with .selectAll(), only rect elements), and
you're merging text and rectangles into one selection, which is a bit problematic if you want to position and color them differently on update.
The second problem could be explained a bit more:
update.enter().append("text") // returns a selection of newly created text elements
.merge(update) // merges the selection of newly created text with existing rectangles
.attr("fill", .... // affects both text and rects.
These two issues can be resolved by using the enter/update/exit cycle correctly.
One thing to note is that D3's enter update exit pattern isn't designed to enter elements more than once with the same statement, you're entering text and rects with the same enter statement, see here.
Therefore, one option is to use two selections, one for text and one for rects:
const updateRect = group
.selectAll("rect")
.data(randomData);
let bars = updateRect
.enter() // create new dom elements for added data items
.append("rect")
.merge(updateRect)
.attr("x", (d, i) => i * (width / randomData.length))
.attr("y", d => height - d * 5)
.attr("width", width / randomData.length - padding)
.attr("height", d => d * 5)
.attr("fill", d => colorGradient(d));
const updateText = group
.selectAll("text")
.data(randomData);
let labels = updateText
.enter()
.append("text")
.merge(updateText)
.text(d => d)
.attr("text-anchor", "middle")
.attr(
"x",
(d, i) =>
i * (width / randomData.length) +
(width / randomData.length - padding) / 2
)
.attr("y", d => height - d * 5 + 12)
.style("font-family", "sans-serif")
.style("font-size", 12)
.style("fill", "#fff");
updateRect.exit().remove();
updateText.exit().remove();
Here in sandbox form.
The other option is to use a parent g to hold both rect and text, this could be done many ways, but if you don't need a transition between values or the number of bars, would probably be most simple like:
const update = group
.selectAll("g")
.data(randomData);
// add a g for every extra datum
const enter = update.enter().append("g")
// give them a rect and text element:
enter.append("rect");
enter.append("text");
// merge update and enter:
const bars = update.merge(enter);
// modify the rects
bars.select("rect")
.attr("x", (d, i) => i * (width / randomData.length))
.attr("y", d => height - d * 5)
.attr("width", width / randomData.length - padding)
.attr("height", d => d * 5)
.attr("fill", d => { return colorGradient(d)});
// modify the texts:
bars.select("text")
.text(d => d)
.attr("text-anchor", "middle")
.attr(
"x",
(d, i) =>
i * (width / randomData.length) +
(width / randomData.length - padding) / 2
)
.attr("y", d => height - d * 5 + 12)
.style("font-family", "sans-serif")
.style("font-size", 12)
.style("fill", "#ffffff");
Here's that in sandox form.
A bit further explanation: selection.select() selects the first matching element for each element in the selection - so we can select the sole rectangle in each parent g (that we add when entering the parent) with bars.select("rect") above. D3 passes the parent datum to the child when appending in the above. Note: If we had nested data (multiple bars or texts per data array item) we'd need to have nested enter/exit/update cycles.
I am creating grouped bar chart using D3 V5 in react.I am able to display y axis but not ticks and text.but in case of x-axis it's completely invisible. i have added d3.min.js to index.html file, but nothing works. any help is appreciated
here I am attaching my code
DrawChart = (data) => {
var w = 450, h = 300, p = 100;
var x0 = d3.scaleBand().range([0, w]).padding(0.4);
var x1 = d3.scaleBand();
var y = d3.scaleLinear().range([h, 0]);
var color = d3.scaleOrdinal().range(["#a85db3", "#95f578"]);
const svg = d3.select("div#predicative")
.append("svg").attr("width", w).attr("height", h)
.attr("padding", p).style("margin-left", 30)
.style("margin-top", 20).style("margin-bottom", 10);
var ageNames = d3.keys(data[0]).filter(function (key) { return key !== "dept"; });
data.forEach(function (d) {
d.ages = ageNames.map(function (name) { return { name: name, value: +d[name] }; });
});
x0.domain(data.map(function (d) { return d.dept; }));
x1.domain(ageNames).rangeRound([0, x0.bandwidth()]);
y.domain([0, (d3.max(data, function (d) { return d3.max(d.ages, function (d) { return d.value; }); })) + 10]);
svg.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + h + ")")
.call(d3.axisBottom(x0)
.tickSize(-w, 0, 0)
.tickFormat(''));
svg.append("g")
.attr("class", "y axis")
.call(d3.axisLeft(y))
.append("text")
.attr("transform", "rotate(-90)")
.attr("y", 6)
.attr("dy", ".71em")
.style("text-anchor", "end")
.text("Time");
svg:not(:root){
overflow: visible;
}
Im creating a line chart graph using d3 js. I need a solution to change the y scale values when I resize the window instead of scroll bar.
I have added the code below which adds scroll bar when I resize the screen size. I want design dynamic y scale values when we resize for different screen sizes.
`
<!DOCTYPE html>
<meta charset="utf-8">
<style> /* set the CSS */
body { font: 12px Arial;}
path {
stroke: steelblue;
stroke-width: 2;
fill: none;
}
.axis path,
.axis line {
fill: none;
stroke: grey;
stroke-width: 1;
shape-rendering: crispEdges;
}
</style>
<body>
<!-- load the d3.js library -->
<script src="http://d3js.org/d3.v3.min.js"></script>
<script>
// Set the dimensions of the canvas / graph
var margin = {top: 30, right: 20, bottom: 30, left: 50},
width = 600 - margin.left - margin.right,
height = 270 - margin.top - margin.bottom;
// Parse the date / time
var parseDate = d3.time.format("%d-%b-%y").parse;
// Set the ranges
var x = d3.time.scale().range([0, width]);
var y = d3.scale.linear().range([height, 0]);
// Define the axes
var xAxis = d3.svg.axis().scale(x)
.orient("bottom").ticks(5);
var yAxis = d3.svg.axis().scale(y)
.orient("left").ticks(5);
// Define the line
var valueline = d3.svg.line()
.x(function(d) { return x(d.date); })
.y(function(d) { return y(d.close); });
// Adds the svg canvas
var svg = d3.select("body")
.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 + ")");
// Get the data
d3.csv("data.csv", function(error, data) {
data.forEach(function(d) {
d.date = parseDate(d.date);
d.close = +d.close;
});
// Scale the range of the data
x.domain(d3.extent(data, function(d) { return d.date; }));
y.domain([0, d3.max(data, function(d) { return d.close; })]);
// Add the valueline path.
svg.append("path")
.attr("class", "line")
.attr("d", valueline(data));
// Add the X Axis
svg.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + height + ")")
.call(xAxis);
// Add the Y Axis
svg.append("g")
.attr("class", "y axis")
.call(yAxis);
});
</script>
</body>
`
A method I like to use is to wrap the code in a function (lets call it main()), and re-run when the screen size changes.
At the beginning of this new main() function, remove the old (and now redundantly sized) svg.
d3.select("#<id of svg>").remove();
Then, create the new y scale using
var new_width = document.getElementById("<Div ID>").clientWidth;
var new_height = document.getElementById("<Div ID>").clientHeight;
and apply these to the new svg as you create it. D3 should allow you to run the .remove() line before the initial svg has been created. Make sure to then add an id when you create the svg (using attr("id", "<id of svg>")).
After that, you can call the main() function on resizing with
d3.select(window).on( "resize", main() );
The way in which you want to actually size your Div will now rely on your CSS, so you can use something like {height:50vh} or whatever you like.
Hope this helps.
P.S. By the way, why are you using D3 version 3? We're up to v5 :)
drag n drop utility: need to develop a tree structures with nodes and connectors. Nodes and connectors are to be manually drawn using the tool bar(manually created). On the nodes and connectors need to generate events.Using angular js. Please provide sample code.
Once clicked on the nodes the node gets created in one division and the connector can be used graphically to connect between nodes.
I have achieved this with d3 library which seems to be very useful.
Below is answer:
<div id="drawArea" class="division" ></div>
<script type="text/javascript">
// Create a svg canvas
var svg = d3.select("#drawArea")
.append("svg")
.attr("width", 700)
.attr("height", 500);
//Drag nodes
var drag = d3.behavior.drag()
.on("dragstart", function() {
d3.event.sourceEvent.stopPropagation()
})
.on("drag", dragmove);
//First circle
var g1 = svg.append("g")
.attr("transform", "translate(" + 150 + "," + 100 + ")")
.attr("class", "first")
.call(drag)
.append("circle").attr({
r: 20,
})
.style("fill", "#FFFF00")
//Second circle
var g2 = svg.append("g")
.attr("transform", "translate(" + 250 + "," + 300 + ")")
.attr("class", "second")
.call(drag)
.append("circle").attr({
r: 20,
})
.style("fill", "#00FF00")
svg.on('dblclick', function() {
var coords = d3.mouse(this);
console.log(coords);
drawCircle(coords[0], coords[1]);
});
function drawCircle(x, y) {
var g2 = svg.append("g")
.attr("transform", "translate(" + x + "," + y + ")")
.attr("class", "third")
.call(drag)
.append("circle").attr({
r: 20,
})
.style("fill", "#00F");
}
//Drag handler
function dragmove(d) {
var x = d3.event.x;
var y = d3.event.y;
d3.select(this).attr("transform", "translate(" + x + "," + y + ")");
if(d3.select(this).attr("class") == "first") {
// line.attr("x1", x);
// line.attr("y1", y);
d3.select(this).attr("cx", x);
d3.select(this).attr("cy", y);
} else {
d3.select(this).attr("cx", x);
d3.select(this).attr("cy", y);
//line.attr("x2", x);
//line.attr("y2", y);
}
}
</script>
I am a newbee in javascript, i followed an example to create a pie chart. I would like it to update with changes in select list. While the pie works, the legend list simply grows.
I tried exit().remove() and selectAll("g").remove() and I saw in debug that "g" elements have been removed but the variable legend keeps growing in each update instead of removing the old data. Can anyone figure out why the code works with the pie while the legend won't work?
Many thanks!
Here is my code
var app=angular.module("app",[]);
// controller gets average json data and reform by host, produces executions
app.controller("pieCtrl",function($scope,$http){
$http.get("average.json").success(function(data){
//executions- key:hostName, value:[listExecutions]
var executions={};
data.forEach(function(d){
if(!(d.hostName in executions)){
executions[d.hostName]={
hostName:d.hostName,
listExecutions:[]
}
}
executions[d.hostName].listExecutions.push(d);
});
//listHostnames for <select> [["host1"],["host2"]]
$scope.ListHostnames=[];
Object.keys(executions).forEach(function(hostName) {
$scope.ListHostnames.push(executions[hostName]);
});
//default value, bound to <select>
$scope.selectedHostname = $scope.ListHostnames[2];
}).error(function(err){
throw err;
});
});
app.directive("pie",function() {
function link(scope,element,attr) {
var wpie = 300;
var hpie = 300;
var outerRadius = wpie / 2;
var innerRadius = outerRadius*0.5;
var color = d3.scale.category10();
var legendRectSize = 18;
var legendSpacing = 4;
scope.$watch('data', update);
var pie = d3.layout.pie()
.value(function(d) {
return d.executionNum;
})
.sort(null);//prevent values from being sorted;
var arc = d3.svg.arc()
.outerRadius(outerRadius)
.innerRadius(innerRadius);
//svg must be outside update(), otherwise multiple charts will be created instead of updating one chart
var svg = d3.select("#chart").append("svg")
.attr("width", wpie*2)
.attr("height", hpie*1.5)
.append("g")
.attr("transform", "translate(" + wpie / 2 + "," + hpie / 2 + ")");
//tooltip
var tooltip = d3.select('pie')
.append('div')
.attr('class', 'tooltip');
tooltip.append('div')
.attr('class', 'date');
tooltip.append('div')
.attr('class', 'num');
function update(){
//clear what is left
svg.selectAll('path').remove();
//input data
if (!scope.$parent.selectedHostname){ return }
var data = scope.$parent.selectedHostname.listExecutions;
//bind data
var arcs = svg.selectAll("path")
.data(pie(data));
//append path for shapes
arcs.enter().append("path")
.attr("d", arc)
.attr("fill", function(d) {
return color((d.data.day));
})
.each(function(d){this._current=d;});// animation
//mouseover tooltip to show contents
arcs.on('mouseover', function(d) {
var num=(d.enabled)? d.data.executionNum :0;
tooltip.select('.date').html('Date: '+d.data.day+' / '+d.data.month);
tooltip.select('.num').html('Number of executions: '+num);
tooltip.style('display', 'block');
});
arcs.on('mouseout', function() {
tooltip.style('display', 'none');
});
//add labels
arcs.enter().append("text")
.attr("transform", function(d) {
return "translate(" + arc.centroid(d) + ")";
})
.attr("dy", ".35em")
.style("text-anchor", "middle")
.text(function(d) {
return d.data.executionNum;
})
.attr("fill","black");
svg.selectAll(".legend").remove();
svg.selectAll("g").remove();
svg.selectAll("g.legend").remove();
var legend = svg.selectAll('.legend')
.data(color.domain());
legend.exit().remove();
legend.enter()
.append('g')
.attr('class', 'legend')
.attr('transform', function(d, i) {
var height = legendRectSize + legendSpacing;
var offset = height * color.domain().length / 2;
var horz = 10 * legendRectSize;
var vert = i * height - offset;
return 'translate(' + horz + ',' + vert + ')';
});
legend.append('circle')
.attr('cx', legendRectSize)
.attr('cy', legendRectSize)
.attr('r',8)
.style('fill', color)
.style('stroke', color)
.attr('class', 'legend');
legend.append('text')
.attr('x', legendRectSize + 4*legendSpacing)
.attr('y', legendRectSize +4)
.text(function(d) { return d; })
.attr('class', 'legend');
legend.exit().remove();
}
}
return{
link: link,
restrict: 'E',
scope: { data : '='}
};
})
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<!DOCTYPE html>
<html>
<head lang="en">
<meta charset="UTF-8">
<title>dynamic pie</title>
</head>
<body ng-app="app" ng-controller="pieCtrl">
<h1> Dynamic pie chart </h1>
<div id="list">
<select ng-model="selectedHostname" ng-options="hostName as hostName.hostName for hostName in ListHostnames"></select>
</div>
<div id="chart">
<pie data="selectedHostname"></pie>
</div>
</body>
</html>
I'm using an angular controller to query the data and d3 code is in the pie directive. update function is called to refresh the chart. original data is a json file.
[{"durationSec":515,"month":2,"day":15.0,"executionNum":8,"hostName":"monitoring02"},{"durationSec":515,"month":2,"day":17.0,"executionNum":3,"hostName":"monitoring02"},{"durationSec":521,"month":2,"day":16.0,"executionNum":5,"hostName":"monitoring02"},{"durationSec":515,"month":2,"day":18.0,"executionNum":6,"hostName":"monitoring02"},{"durationSec":739,"month":2,"day":18.0,"executionNum":7,"hostName":"apple-pc"},{"durationSec":1140,"month":2,"day":19.0,"executionNum":7,"hostName":"apple-pc"},{"durationSec":1117,"month":2,"day":20.0,"executionNum":6,"hostName":"apple-pc"},{"durationSec":1125,"month":2,"day":21.0,"executionNum":3,"hostName":"monitoring01"},{"durationSec":1169,"month":2,"day":22.0,"executionNum":5,"hostName":"monitoring01"}]