During creating graphs I need to set data. Those data(Array of Objects) I have already in HTML like this:
<svg class="graph-n" data-stuff="{simplified data}"></svg>
Then with Javascript and D3 JS I initialize and setup graphs with the following code:
<script>
var margin = { top: 20, right: 20, bottom: 30, left: 50},
width = 1500 - margin.left - margin.right,
height = 350 - margin.top - margin.bottom;
var x = d3.scaleTime().range([0, width]);
var y = d3.scaleLinear().range([height, 0]);
var valueline = d3.line()
.x(function(d) { return x(new Date(d.t)); })
.y(function(d) { return y(d.y); });
var svg = d3.selectAll(".graph-n")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
x.domain(d3.extent(data, function(d) { return new Date(d.t); }));
y.domain([0, d3.max(data, function(d) { return d.y; })]);
svg.append("path")
.attr("class", "line")
.attr("d", valueline);
svg.append("g")
.attr("transform", "translate(0," + height + ")")
.call(d3.axisBottom(x));
svg.append("g")
.call(d3.axisLeft(y));
</script>
The question is how shall I say, that data are inside each element during Selection in data attribute 'data-stuff' ?
Each SVG has data to plot in his own data attribute.
Or is my approach wrong and I shall use different approach?
Thank you for your responses.
There is no way to just tell d3 explicitly "take data from this attribute". You can however set the data programatically, loading it from the attribute of your choosing. There are several ways on how to achieve it, as demonstrated on these selection examples (they use <ul> and <li> for simplicity, <svg> is usage is analogous):
// the pure D3 way
d3.selectAll("ul.d3-pure") // select the element
.datum(function() { return this.getAttribute("data-list").split(",")}) // set selection's data based on its data attribute
.selectAll("li") // create new selection
.data((d) => d) // set the data from the parent element
.enter().append("li") // create missing elements
.text((content) => content); // set elements' contents
// the DOM way
var domUls = document.querySelectorAll("ul.dom"); // select elements
for(var i = 0; i < domUls.length; i++) { // iterate over those elements
const ul = domUls[i];
const d3_ul = d3.select(ul); // create D3 object from the node
const data = ul.getAttribute("data-list").split(",");
d3_ul.selectAll("li").data(data) // create new selection and assign its data
.enter().append("li") // create missing elements
.text((content) => content) // set elements' content
}
// the hybrid D3-DOM way
d3.selectAll("ul.d3-hybrid") // select elements
.each(function() { // iterate over each node of the selection
const ul = d3.select(this); // "this" is the "ul" HTML node
const data = ul.attr("data-list").split(",");
ul.selectAll("li").data(data) // create new selection, assign its data
.enter().append("li") // create missing elements
.text((content) => content) // set elements' content
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
<ul class="d3-pure" data-list="1,2,3">
</ul>
<ul class="dom" data-list="a,b,c">
</ul>
<ul class="d3-hybrid" data-list="I,II,III">
</ul>
Modern browsers accepts node().dataset
Using D3_selection.node() and pure Javascript's DOM-node dataset property, as commented by #altocumulus before.
It is an old Javascript standard for HTML elements (since Chorme 8 and Firefox 6) but new for SVG (since Chorme 55 and Firefox 51).
The values of dataset's key-values are pure strings, but a good practice is to adopt JSON string format for non-string datatypes, to parse it by JSON.parse().
Using it
Code snippet to get and set key-value datasets at HTML and SVG.
console.log("-- GET values --")
var x = d3.select("#html_example").node().dataset;
console.log("s:", x.s );
for (var i of JSON.parse(x.list)) console.log("list_i:",i)
var y = d3.select("#svg_example g").node().dataset;
console.log("s:", y.s );
for (var i of JSON.parse(y.list)) console.log("list_i:",i)
console.log("-- SET values --");
y.s="BYE!"; y.list="null";
console.log( d3.select("#svg_example").node().innerHTML )
<script src="https://d3js.org/d3.v5.min.js"></script>
<p id="html_example" data-list="[1,2,3]" data-s="Hello123">Hello dataset!</p>
<svg id="svg_example">
<g data-list="[4,5,6]" data-s="Hello456 SVG"></g>
</svg>
Related
I'm trying to update a bargraph created using d3.js to display values from a regularly updated array. Currently, I have a function d3Data that is called upon page load(using jQuery) and as a function invoked whenever buttons are clicked on the page. This d3 data updates the array and then calls another function d3New that is supposed to rerender the bar graph.
The bar graph is able to render along with the bar rectangles if hard coded data in the array is used. However, since I initialize the starting array as empty I am unable to see the rectangles as it seems my bar graph doesn't display rectangles based on updated values in this array.
Here is my logic for displaying the rectangles within the bar graph:
var rects = svg.selectAll("rect")
.data(data)
rects.enter().append("rect")
rects.exit().remove()
rects.attr("x", function(d, i) { return (i * 2.0 + 1.3) * barWidth; })
.attr("y", function(d,i) {
return Math.min(yScale(0), yScale(d))
})
.attr("height", function(d) {
// the height of the rectangle is the difference between the scale value and yScale(0);
return Math.abs(yScale(0) - yScale(d));
})
.attr("width", barWidth)
.style("fill", "grey")
.style("fill", function(d,i) { return color[i];})
I understand the enter() function intially joins the data to the rectangle elements and the exit function is used in order to remove any previous rectangle element values upon rectangle rerender. But, no rectangles are rendered to the screen and not sure why? Here is what it looks like:
Any help would be great
edit:
Here is some more of the two functions:
function d3Data() {
var dataArray = [];
for (var key in gradeFrequency) {
dataArray.push(gradeFrequency[key]);
}
d3New(dataArray);
}
function d3New(data) {
var height = 500;
var width = 500;
var margin = {left: 100, right: 10, top: 100, bottom: 20};
var color = ["#C6C7FF", "#8E8EFC", "#5455FF", "#8E8EFC", "#C6C7FF"];
var svg = d3.select("body").append("svg")
.attr('height', height)
.attr('width', width)
.append("g")
.attr("transform", "translate("+ [margin.left + "," + margin.top] + ")");
var barWidth = 30;
var chartHeight = height-margin.top-margin.left;
var xScale= d3.scaleBand()
.domain(["A", "B", "C", "D", "F"])
.range([100, 450])
.padding([0.8])
// Draw the axis
svg.append("g")
.attr("transform", "translate(-100,300)")
.call(d3.axisBottom(xScale));
var yScale = d3.scaleLinear()
.domain([0, 1.0])
.range([chartHeight, 0]);
var rects = svg.selectAll("rect")
.data(data)
rects.enter().append("rect").merge(rects)
rects.exit().remove()
I figured out how to fix my problem. Had to add:
d3.selectAll("svg").remove();
to the start of the function in order to remove previous outdated graphs and also add the attributes for "rect" before the .exit().remove(). So instead of:
var rects = svg.selectAll("rect")
.data(data)
rects.enter().append("rect").merge(rects)
rects.exit().remove()
rects.attr(..).attr(..).attr(..)
I did:
rects.enter().append("rect").merge("rect").attr(..).attr(..).attr(..) and so on.
rects.exit().remove()
Since the attributes for the rectangles need to be updated as well they had to go before the .exit() and .remove() calls
I am trying to download a graph that I created with d3.js as an png (Any datatyp would do though), but I am failing gloriously.
I followed various questions on Stackoverflow which address similar issues, but still can't get it to work.
With different solutions I very often run into this error when debugging:
Uncaught TypeError: Failed to execute 'serializeToString' on 'XMLSerializer': parameter 1 is not of type 'Node'.
This is my code for my svg-graph:
var data = build
// set the dimensions and margins of the graph
var margin = {top: 20, right: 20, bottom: 30, left: 100},
width = 960 - margin.left - margin.right,
height = 500 - margin.top - margin.bottom;
// set the ranges
var y = d3.scaleBand()
.range([height, 0])
.padding(0.1);
var x = d3.scaleLinear()
.range([0, width]);
// append the svg object to the body of the page
// append a 'group' element to 'svg'
// moves the 'group' element to the top left margin
var svg = d3.select(".svg-net-area").append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.append("g")
.attr("transform",
"translate(" + margin.left + "," + margin.top + ")");
// format the data
data.forEach(function(d) {
d.buildings__net_leased_area = +d.buildings__net_leased_area;
});
// Scale the range of the data in the domains
x.domain([0, d3.max(data, function(d){ return d.buildings__net_leased_area; })])
y.domain(data.map(function(d) { return d.buildings__name; }));
//y.domain([0, d3.max(data, function(d) { return d.buildings__net_leased_area; })]);
// append the rectangles for the bar chart
svg.selectAll(".bar")
.data(data)
.enter().append("rect")
.attr("class", "bar")
//.attr("x", function(d) { return x(d.buildings__net_leased_area); })
.attr("width", function(d) {return x(d.buildings__net_leased_area); } )
.attr("y", function(d) { return y(d.buildings__name); })
.attr("height", y.bandwidth())
.attr("fill", "#348496");
// add the x Axis
svg.append("g")
.attr("transform", "translate(0," + height + ")")
.call(d3.axisBottom(x));
// add the y Axis
svg.append("g")
.call(d3.axisLeft(y));
And this is what I am trying:
function svgDataURL(svg) {
var svgAsXML = (new XMLSerializer).serializeToString(svg);
return "data:image/svg+xml," + encodeURIComponent(svgAsXML);
}
function download(){
var dataURL = svgDataURL(svg)
var dl = document.createElement("a");
document.body.appendChild(dl);
dl.setAttribute("href", dataURL);
dl.setAttribute("download", "test.svg");
dl.click();
}
I call this function in my django template.
I am trying to create a DataURL for my svg graph. Then I want to pass it to the download function. But I am not entirely sure what I am even doing here.
So if someone could help that would be very nice. Thanks in advance.
In your code, svg is a D3 selection. You cannot pass a D3 selection to serializeToString. Have a look at the error here:
var svg = d3.select("svg");
var svgAsXML = (new XMLSerializer).serializeToString(svg);
console.log(svgAsXML)
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<svg>
<circle r="20" cx="100" cy="50"></circle>
</svg>
As you can see, serializeToString requires a node:
The Node to use as the root of the DOM tree or subtree for which to construct an XML representation.
Therefore, instead of passing a D3 selection, you have to pass the node. The easiest way to do that is using the D3 method node() on the selection:
var svg = d3.select("svg");
var svgAsXML = (new XMLSerializer).serializeToString(svg.node());
console.log(svgAsXML)
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<svg>
<circle r="20" cx="100" cy="50"></circle>
</svg>
So, in your code, pass the node to the svgDataUrl function:
var dataURL = svgDataURL(svg.node())
Alternatively, pass the D3 selection to svgDataUrl, but use the node inside serializeToString, as I did above.
I am making a line graph for a set of data regarding letter vs frequency. I have made proper code for x and y axis, but while plotting line I am getting error and not able to plot the line-graph. Can someone help fix the issue?
SNIPPET:
<html>
<head>
<script src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.4.12/angular.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.1.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.3.0/d3.min.js"></script>
</head>
<body ng-app="myApp" ng-controller="myCtrl">
<svg></svg>
<script>
//module declaration
var app = angular.module('myApp',[]);
//Controller declaration
app.controller('myCtrl',function($scope){
$scope.svgWidth = 800;//svg Width
$scope.svgHeight = 500;//svg Height
//Data in proper format
var data = [
{"letter": "A","frequency": "5.01"},
{"letter": "B","frequency": "7.80"},
{"letter": "C","frequency": "15.35"},
{"letter": "D","frequency": "22.70"},
{"letter": "E","frequency": "34.25"},
{"letter": "F","frequency": "10.21"},
{"letter": "G","frequency": "7.68"},
];
//removing prior svg elements ie clean up svg
d3.select('svg').selectAll("*").remove();
//resetting svg height and width in current svg
d3.select("svg").attr("width", $scope.svgWidth).attr("height", $scope.svgHeight);
//Setting up of our svg with proper calculations
var svg = d3.select("svg");
var margin = {top: 20, right: 20, bottom: 30, left: 40};
var width = svg.attr("width") - margin.left - margin.right;
var height = svg.attr("height") - margin.top - margin.bottom;
//Plotting our base area in svg in which chart will be shown
var g = svg.append("g");
//shifting the canvas area from left and top
g.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
//X and Y scaling
var x = d3.scaleLinear().rangeRound([0, width]);
var y = d3.scaleBand().rangeRound([height, 0]).padding(0.4);
//Feeding data points on x and y axis
x.domain([0, d3.max(data, function(d) { return +d.frequency; })]);
y.domain(data.map(function(d) { return d.letter; }));
//Final Plotting
//for x axis
g.append("g")
.attr("transform", "translate(0," + height + ")")
.call(d3.axisBottom(x));
//for y axis
g.append("g")
.call(d3.axisLeft(y))
.append("text")
.attr("transform", "rotate(-90)")
.attr("y", 6)
.attr("dy", "0.71em")
.attr("text-anchor", "end");
//the line function for path
var lineFunction = d3.line()
.x(function(d) {return xScale(d.x); })
.y(function(d) { return yScale(d.y); })
.curve(d3.curveLinear);
//defining the lines
var path = g.append("path");
//plotting lines
path
.attr("d", lineFunction(data))
.attr("stroke", "blue")
.attr("stroke-width", 2)
.attr("fill", "none");
});
</script>
</body>
</html>
ERROR:
NEW ERROR:
Look at the console: you don't have a xScale or a yScale.
So, the line generator should be:
var lineFunction = d3.line()
.x(function(d) {return x(d.frequency); })
.y(function(d) { return y(d.letter); })
.curve(d3.curveLinear);
Besides that, frequency is a string, not a number. So, it's a good idea turning it into a number. Write this right after your data variable:
data.forEach(function(d){
d.frequency = +d.frequency;
});
Note: it's a good practice defining your variable names properly, with descriptive names, like xScale, yAxis, chartLegend or formatNumber... Look at your line generator: you have two different x in a single line. If you don't take care, you'll mix them.
If you want to use xScale and yScale , you need to define these functions. Syntax is given below (ignore values):
Below code is for d3 version 3
me.xscale = d3.scale.linear() // for horizontal distances if using for 2D
.domain([0, 500])
.range([0, 700]);
me.yscale = d3.scale.linear() // for vertical distances if using for 2D
.domain([0, 600])
.range([0, 200]);
These functions are used to define mapping of a values in one range to values in other range.
e.g - Suppose you want draw a graph on your browser screen. And you want assume that width 500px on your browser screen should be counted as 500 on your graph.
You need to define xScale as above . In this case , this function will map every value in domain (0-500) to unique value in range (0-700) and vice versa.
I'm trying to make a scatter plot using a .json file. It will let the user to select which group of data in the json file to be displayed. So I'm trying to use the update pattern.
The following code will make the first drawing, but every time selectGroup() is called(the code is in the html file), nothing got updated. The console.log(selection) did come back with a new array each time, but the enter and exit property of that selection is always empty.
Can anyone help me take a look? Thanks a lot!
var margin = {
top: 30,
right: 40,
bottom: 30,
left: 40
}
var width = 640 - margin.right - margin.left,
height = 360 - margin.top - margin.bottom;
var dataGroup;
var groupNumDefault = "I";
var maxX, maxY;
var svg, xAxis, xScale, yAxis, yScale;
//select and read data by group
function init() {
d3.json("data.json", function (d) {
maxX = d3.max(d, function (d) {
return d.x;
});
maxY = d3.max(d, function (d) {
return d.y;
});
console.log(maxY);
svg = d3.select("svg")
.attr("id", "scatter_plot")
.attr("width", 960)
.attr("height", 500)
.append("g")
.attr("id", "drawing_area")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
//x-axis
xScale = d3.scale.linear().range([0, width]).domain([0, maxX]);
xAxis = d3.svg.axis()
.scale(xScale).orient("bottom").ticks(6);
//y-axis
yScale = d3.scale.linear().range([0, height]).domain([maxY, 0]);
yAxis = d3.svg.axis().scale(yScale).orient("left").ticks(6);
svg.append("g")
.attr("class", "x_axis")
.attr("transform", "translate(0," + (height) + ")")
.call(xAxis);
svg.append("g")
.attr("class", "y_axis")
.call(yAxis);
});
selectGroup(groupNumDefault);
}
//update data
function selectGroup(groupNum) {
d3.json("/data.json", function (d) {
dataGroup = d.filter(function (el) {
return el.group == groupNum;
});
console.log(dataGroup);
drawChart(dataGroup);
});
}
//drawing function
function drawChart(data) {
var selection = d3.select("svg").selectAll("circle")
.data(data);
console.log(selection);
selection.enter()
.append("circle")
.attr("class", "dots")
.attr("cx", function (d) {
console.log("updating!");
return xScale(d.x);
})
.attr("cy", function (d) {
return yScale(d.y);
})
.attr("r", function (d) {
return 10;
})
.attr("fill", "red");
selection.exit().remove();
}
init();
The problem here is on two fronts:
Firstly, your lack of a key function in your data() call means data is matched by index (position in data array) by default, which will mean no enter and exit selections if the old and current datasets sent to data() are of the same size. Instead, most (perhaps all) of the data will be put in the update selection when d3 matches by index (first datum in old dataset = first datum in new dataset, second datum in old dataset = second datum in new dataset etc etc)
var selection = d3.select("svg").selectAll("circle")
.data(data);
See: https://bl.ocks.org/mbostock/3808221
Basically, you need your data call adjusted to something like this (if your data has an .id property or anything else that can uniquely identify each datum)
var selection = d3.select("svg").selectAll("circle")
.data(data, function(d) { return d.id; });
This will generate enter() and exit() (and update) selections based on the data's actual contents rather than just their index.
Secondly, not everything the second time round is guaranteed be in the enter or exit selections. Some data may be just an update of existing data and not in either of those selections (in your case it may be intended to be completely new each time). However, given the situation just described above it's pretty much guaranteed most of your data will be in the update selection, some of it by mistake. To show updates you will need to alter the code like this (I'm assuming d3 v3 here, apparently it's slightly different for v4)
selection.enter()
.append("circle")
.attr("class", "dots")
.attr("r", function (d) {
return 10;
})
.attr("fill", "red");
// this new bit is the update selection (which includes the just added enter selection
// now, the syntax is different in v4)
selection // v3 version
// .merge(selection) // v4 version (remove semi-colon off preceding enter statement)
.attr("cx", function (d) {
console.log("updating!");
return xScale(d.x);
})
.attr("cy", function (d) {
return yScale(d.y);
})
selection.exit().remove();
Those two changes should see your visualisation working, unless of course the problem is something as simple as an empty set of data the second time around which would also explain things :-)
I am trying to build a bar graph that I can switch between the amount of data displayed based on a particular length of time. So far the code that I have is this,
var margin = {
top : 20,
right : 20,
bottom : 30,
left : 50
}, width = 960 - margin.left - margin.right, height = 500
- margin.top - margin.bottom;
var barGraph = function(json_data, type) {
if (type === 'month')
var barData = monthToArray(json_data);
else
var barData = dateToArray(json_data);
var y = d3.scale.linear().domain([ 0, Math.round(Math.max.apply(null,
Object.keys(barData).map(function(e) {
return barData[e]['Count']}))/100)*100 + 100]).range(
[ height, 0 ]);
var x = d3.scale.ordinal().rangeRoundBands([ 0, width ], .1)
.domain(d3.entries(barData).map(function(d) {
return barData[d.key].Date;
}));
var xAxis = d3.svg.axis().scale(x).orient("bottom");
var yAxis = d3.svg.axis().scale(y).orient("left");
var svg = d3.select("#chart").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 + ")");
svg.append("g").attr("class", "x axis").attr("transform",
"translate(0," + height + ")").call(xAxis);
svg.append("g").attr("class", "y axis").call(yAxis).append(
"text").attr("transform", "rotate(-90)").attr("y", 6)
.attr("dy", ".71em").style("text-anchor", "end").text(
"Total Hits");
svg.selectAll(".barComplete").data(d3.entries(barData)).enter()
.append("rect").attr("class", "barComplete").attr("x",
function(d) {
return x(barData[d.key].Date)
}).attr("width", x.rangeBand() / 2).attr("y",
function(d) {
return y(barData[d.key].Count);
}).attr("height", function(d) {
return height - y(barData[d.key].Count);
}).style("fill", "orange");
var bar = svg.selectAll(".barHits").data(d3.entries(barData))
.enter().append("rect").attr("class", "barHits").attr(
"x", function(d) {
return x(barData[d.key].Date) + x.rangeBand() / 2
}).attr("width", x.rangeBand() / 2).attr("y",
function(d) {
return y(barData[d.key].Count);
}).attr("height", function(d) {
return height - y(barData[d.key].Count);
}).style("fill", "red");
};
This does a great job displaying my original data set, I have a button set up to switch between the data sets which are all drawn at the beginning of the page. all of the arrays exist but no matter how hard I try I keep appending a new graph to the div so after the button click I have two graphs, I have tried replacing all of the code in the div, using the enter() exit() remove() functions but when using these I get an error stating that there is no exit() function I got this by following a post that someone else had posted here and another one here but to no avail. Any ideas guys?
What you are most likely doing is drawing a new graph probably with a new div each time the button is clicked, one thing you can try doing is to hold on to the charts container element somewhere, and when the button is clicked, you simply clear it's children and re-draw the graphs.
In practice, I almost never chain .data() and .enter().
// This is the syntax I prefer:
var join = svg.selectAll('rect').data(data)
var enter = join.enter()
/* For reference: */
// Selects all rects currently on the page
// #returns: selection
svg.selectAll('rect')
// Same as above but overwrites data of existing elements
// #returns: update selection
svg.selectAll('rect').data(data)
// Same as above, but now you can add rectangles not on the page
// #returns: enter selection
svg.selectAll('rect').data(data).enter()
Also important:
# selection.enter()
The enter selection merges into the update selection when you append or insert.
Okay, I figured it out So I will first direct all of your attention to here Where I found my answer. The key was in the way I was drawing the svg element, instead of replacing it with the new graph I was just continually drawing a new one and appending it to the #chart div. I found the answer that I directed you guys to and added this line in the beginning of my existing barGraph function.
d3.select("#barChart").select("svg").remove();
and it works like a charm, switches the graphs back and forth just as I imagined it would, now I have a few more tasks for the bargraph itself and I can move on to another project.
Thank you all for all of your help and maybe I can return the favor one day!