I'm attempting to set up a simple bar chart using D3 version 4.x.x However, I believe I am doing everything correctly, but can not seem to get the rects to display. I've attached a codepen to see this.
Thank you in advance for any noob issue that is causing this as I am new to D3.
http://codepen.io/PizzaPokerGuy/pen/XpoJxG?editors=0111
enter code here//Width of svg, will be used again down the road
const width = 1000;
//Height of svg, will be used again down the road
const height = 800;
//Padding so things have room to be displayed within svg
const padding = 60;
//Create our SVG container
var svg = d3.select("body")
.append('svg')
.attr("width", width)
.attr("height", height);
//JSON Enter data
var data = d3.json('https://raw.githubusercontent.com/FreeCodeCamp/ProjectReferenceData/mast er/GDP-data.json',
(error, data) => {
var chartData = data.data;
//Stores barWidth variable, math.ciel rounds up, used to set an equal width for each rect
var barWidth = Math.ceil((width - padding) / chartData.length);
//Define xScale
const xScale = d3.scaleLinear()
.domain([0, d3.max(chartData, (d) => d[0])])
.range([padding, width - padding]);
//Define yScale
const yScale = d3.scaleLinear()
.domain([0, d3.max(chartData, (d) => d[1])])
.range([height - padding, padding]);
//Selects SVG elements and selects all rect elements within it
svg.selectAll("rect")
//Adds data to use later
.data(chartData)
//Allows us to add items to the dom if data is larger than ammoutn of rect elements selected
.enter()
//Adds rect element
.append("rect")
//Adds x attribute to x based off of d(chartData), need to create a date as a string is not enough
.attr("x", (d) => xScale(new Date(d[0])))
//Sets y attribute of rectangle
.attr("y", (d) => yScale(d[1]))
//Sets height, we minus the value from height to invert the bars
.attr("height", (d) => height - yScale(d[1]))
//sets width of rect elements
.attr("width", barWidth)
//fill in color of rects
.attr("fill", "black");
});
You are using dates for the X axis, so you'll be better to use a timescale rather than scaleLinear
const xScale = d3.scaleTime()
.domain(d3.extent(chartData, function(d) { return new Date(d[0]); }))
.range([padding, width - padding]);
Codepen: http://codepen.io/anon/pen/egbGaJ?editors=0111
You x values are strings that represent dates but you've made no attempt to treat them as such. Your current scale code is expecting them to be numbers. So you need to decide to create them as strings or dates. For example, coercing them to dates would look like this;
// a time parser
var tF = d3.timeParse("%Y-%m-%d");
// convert to dates
chartData.forEach(function(d){
d[0] = tF(d[0])
});
...
//Define xScale as a time scale
const xScale = d3.scaleTime()
.domain([0, d3.max(chartData, (d) => d[0])])
...
Updated codepen.
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
am plotting histogram based on Data ( which is changing dynamically ) , but the height of some bars are not fitting the svg zone ( the exceed the svg area )
this is piece of code which i have doubt on :
private drawBars(data: any[]): void {
let f = Math.min.apply(Math, this.fixedData.map(function (o) {
return o.xAxis;
}));
/** Create the X-axis band scale */
const x = d3.scaleBand()
.range([f, 240])
.domain(data.map(d => d.xAxis))
.padding(0);
/** Create the Y-axis band scale */
const y = d3.scaleLinear()
.domain([0, this.axisMax])
.range([this.height, 0])
.nice();
/** Create and fill the bars */
this.svg.selectAll("*").remove()
this.svg.selectAll("bars")
.data(data, d => d)
.enter()
.append("rect")
.attr("x", d => x(d.xAxis))
.attr("y", d => y(d.yAxis))
.attr("width", x.bandwidth())
.attr("height", (d) => this.height - y(d.yAxis))
.attr("fill", (d) => this.colorPicker(d))
}
and here is a proof of that weird behaviour :
any ideas and i would be thankful !
Your problem is caused by this.axisMax being set way too low, here:
const y = d3.scaleLinear()
.domain([0, this.axisMax])
.range([this.height, 0])
.nice();
You need to recalculate it based on the available data.
The domain of a scale holds the range of possible values it accepts, but, at least for scaleLinear, scaleTime, and some others, being outside that range doesn't throw an error or even return NaN. For these types of scales, the domain is instead used to identify a linear transformation between the input values and output pixels.
For example
Suppose you have a linear scale with domain [0, 10] and range [0, 100]. Then, x = 0 gives pixel value 0, and x = 10 gives 100. However, x = 20 gives pixel value 200, which is more than the given range.
I just tried out d3js for some days and I want to beautify the x and y scales of my graph to be something like this
But this is what I got so far.
I have tried changing from scaleBand() to scaleLinear() and fix the normally bandwidth() method to a constant value, the graph just would not show.
This is the code
mounted () {
this.generateChart()
},
methods: {
generateChart () {
// set the dimensions and margins of the graph
const margin = { top: 20, right: 20, bottom: 30, left: 30 }
const width = 1850 - margin.left - margin.right
const height = 200 - margin.top - margin.bottom
// make the area for the graph to stay
const svg = d3.select('#heatmap')
.append('svg') // svg area can include headers and color scales
.attr('width', width + margin.left + margin.right) // set width
.attr('height', height + margin.top + margin.bottom) // set height
.append('g') // new g tag area for graph only
.attr('transform', `translate(${margin.left}, ${margin.bottom})`)
// stick g tag to the bottom
// range function generate graph scales
// TODO: make a range using date and time
const xLabel = d3.range(259)
const yLabel = d3.range(23, -1, -1)
// create x, y scales and axes
const x = d3.scaleBand()
.domain(xLabel)
.range([0, width])
.padding(0.05)
svg.append('g')
.attr('transform', `translate(0, ${height})`)
.call(d3.axisBottom(x))
const y = d3.scaleBand()
.domain(yLabel)
.range([height, 0])
.padding(0.05)
svg.append('g').call(d3.axisLeft(y))
d3.json('../predictions.json').then(function (data) {
svg.selectAll()
.data(data.heatmaps.kw.Sand_Heads)
.enter()
.append('rect')
.attr('x', function (d) {
return x(d[1]) // return cell's position
})
.attr('y', function (d) {
return y(d[0])
})
.attr('cx', 1)
.attr('cy', 1)
.attr('width', x.bandwidth()) // return cell's width
.attr('height', y.bandwidth()) // return cell's height
.style('fill', function (d) {
return rgbaToHex(0, 128, 255, 100 * d[2])
})
.on('mouseover', function () { // box stroke when hover
d3.select(this)
.style('stroke', 'black')
.style('opacity', 1)
})
.on('mouseout', function () { // fade block stroke when mouse leave the cell
d3.select(this)
.style('stroke', 'none')
.style('opacity', 0.8)
})
})
}
Note: I have to make it work with date selection in the future too.
This is the structure of the data I'm working on.
{
"days": ["2019-04-11", "2019-04-12", ..., "2019-12-25"],
"heatmaps": {
"kw": {
"Tilly_Point": [[5, 112, 0.0012], [6, 112, 0.0016], ...],
"Mouat_Point": [...]
},
"hw": {
...
}
}
}
Explanation:
the first element of subarray in Tilly_Point is the time of the whale found. ranging from 0 to 23 (midnight to next midnight) and 5 means 05:00 A.M. to 06:00 A.M.
the second element is the nth day of the operation. It's 112 meaning it's the 112th day of the operation. which is 1 August 2019
the last element is the real data being plotted on the graph. the higher -> darker colour towards the real color with 1 opacity
By looking at the desired design we can understand what you mean by "beautify" is reducing the number of ticks. And you are absolutely correct: in very few and specific situations we need to show all of them; most of the times, the design is cleaner and the user benefits from a more tidy dataviz if we choose what ticks to display.
That's clear if we look at this basic example I wrote, simulating your axes:
const svg = d3.select("svg");
const yScale = d3.scaleBand()
.domain(d3.range(25))
.range([10, 80])
.paddingInner(1);
const xScale = d3.scaleBand()
.domain(d3.range(261))
.range([25, 490])
.paddingInner(1);
d3.axisLeft(yScale)(svg.append("g").attr("transform", "translate(25,0)"));
d3.axisBottom(xScale)(svg.append("g").attr("transform", "translate(0,80)"));
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<svg width="500" height="100"></svg>
There are different approaches for reducing the number of ticks here: you can explicitly chose the ticks to show by value or, as I'll do in this answer, you can simply choose how many of them to show. Here, I'll do this using the remainder operator (%) filtering the scale's domain and passing it to tickValues (since you have a band scale we cannot use ticks), for instance showing every 6th value for the y axis:
.tickValues(yScale.domain().filter((_, i) => !(i % 6)))
Here is the result:
const svg = d3.select("svg");
const yScale = d3.scaleBand()
.domain(d3.range(25))
.range([10, 80])
.paddingInner(1);
const xScale = d3.scaleBand()
.domain(d3.range(261))
.range([25, 490])
.paddingInner(1);
d3.axisLeft(yScale).tickValues(yScale.domain().filter((_, i) => !(i % 6)))(svg.append("g").attr("transform", "translate(25,0)"));
d3.axisBottom(xScale).tickValues(xScale.domain().filter((_, i) => !(i % 20)))(svg.append("g").attr("transform", "translate(0,80)"));
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<svg width="500" height="300"></svg>
I have a d3 project where I want to include all my dates but only on certain intervals. Right now it displays everything which is too cluttered. I only want to display the labels on the x axis every 7 years. so for example 1947, 1954, 1961, 1968, etc. Pease help and thank you in advance.
Here is my code:
loadData = ()=> {
req = new XMLHttpRequest();
req.open("GET", "https://raw.githubusercontent.com/freeCodeCamp/ProjectReferenceData/master/GDP-data.json" , true);
req.send();
req.onload= ()=>{
json = JSON.parse(req.responseText);
//dynmaic height
/*var margin = {top: 20, right: 200, bottom: 0, left: 20},
width = 300,
height = datajson.length * 20 + margin.top + margin.bottom;*/
//create measurements
const margin = 60
const width = 1000 - margin;
const height = 600 - margin;
const maxYScale = d3.max(json.data, (d) => d[1]);
//date formatter
const formatDate = d3.timeParse("%Y-%m-%d"); //convert from string to date format
const parseDate = d3.timeFormat("%Y"); //format date to cstring
//create svg
const svg = d3.select("svg");
const chart = svg.append("g")
.attr("transform", `translate(${margin}, ${margin})`);
//y-axis: split charts into 2 equal parts using scaling function
const yScale = d3.scaleLinear()
.range([height, 0]) //length
.domain([0, maxYScale]); //content
//create x-axis
const yAxis = d3.axisLeft(yScale);
//append y-axis
chart.append("g")
.call(yAxis);
//create x-scale
const xScale = d3.scaleBand()
.range([0, width]) //length
//.domain(json.data.filter((date, key) => { return (key % 20 === 0)}).map((d)=> parseDate(formatDate(d[0]))))
.domain(json.data.map((d)=> parseDate(formatDate(d[0]))))
.padding(0.2);
//create x-axis
const xAxis = d3.axisBottom(xScale);
//append x-axis
chart.append("g")
.attr(`transform`, `translate(0, ${height})`)
.call(xAxis);
//make bars
chart.selectAll("rect")
.data(json.data)
.enter()
.append("rect")
.attr("x", (d) => xScale(parseDate(formatDate(d[0]))))
.attr("y", (d) => yScale(d[1]))
.attr("height", (d) => height - yScale(d[1]))
.attr("width", xScale.bandwidth())
}
}
loadData();
Here is my codepen:
codepen
I am just going to answer my own question as I found the solution. In order to set intervals in the x axis I simply used tickValues. Then I used my scale and a filter function to filter the intervals based on the data I had. Below you may find the answer.
const xAxis = d3.axisBottom(xScale)
.tickValues(xScale.domain().filter(function(d) { return (d % 7 === 0)}));
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.