I built a graph in D3, to show spending based on date time and amount of spending, but I can't dynamically set tickValues() on my time.scale to show only ticks for data points.
My scale set up is:
var dateScale = d3.time.scale()
.domain(d3.extent(newData, function(d) { return d.Date; }))
.range([padding, width - padding]);
var amountScale = d3.scale.linear()
.domain([0, d3.max(newData, function(d) { return d.Amount; })])
.range([window.height - padding, padding]);
// Define date Axis
var dateAxis = d3.svg.axis().scale(dateScale)
.tickFormat(d3.time.format('%a %d %m'))
.tickSize(100 - height)
.orient("bottom");
// Draw date Axis
svg.append("g")
.attr({
"class": "axis date-axis",
"transform": "translate(" + [0, height -padding] + ")"
}).call(dateAxis);
// Define amount Axis
var amountAxis = d3.svg.axis().scale(amountScale)
.tickSize(1)
.orient("left");
// Draw amount Axis
svg.append("g")
.attr({
"class": "axis amount-axis",
"transform": "translate(" + padding + ",0)"
}).call(amountAxis);
I've tried to set
dateAxis.tickValues(d3.extent(newData, function(d) { return d.Date; }))
but this only returns min and max value for date axis.
Also tickValues() doesn't accept newData itself - what else I can try here?
I want to achieve this:
to have only ticks highlighted, associated with corresponding data.
I ended up with creating new array of all date points out of my data, to pass it to tickValues() method. My code now is:
var newData = toArray(uniqueBy(data, function(x){return x.Date;}, function(x, y){ x.Amount += y.Amount; return x; }));
// Create array of data date points
var tickValues = newData.map(function(d){return d.Date;});
// Sorting data ascending
newData = newData.sort(sortByDateAscending);
var dateScale = d3.time.scale()
.domain(d3.extent(newData, function(d) { return d.Date; }))
.range([padding, width - padding]);
var amountScale = d3.scale.linear()
.domain([0, d3.max(newData, function(d) { return d.Amount; })])
.range([window.height - padding, padding]);
// Define date Axis
var dateAxis = d3.svg.axis().scale(dateScale)
.tickFormat(d3.time.format('%d %m %Y'))
.tickValues(tickValues)
.orient("bottom");
So now whole graph looks as:
I found this SO (#AmeliaBR) answer explaining tickValues() function, and here is documentation for it too.
Related
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 have this simple d3.js single line graph that is displaying multiple dates across the x-axis:
<script>
// 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");
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("#air_temp_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 + ")");
// Get the data
d3.csv("data.csv", function(error, data) {
data.forEach(function(d) {
d.date = parseDate(d.date);
d.close = +d.close;
});
var tickValues = data.map(function(d) { return d.date; });
xAxis
.tickValues(tickValues)
.tickFormat(d3.time.format('%H:%M'));
// 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);
});
data.csv:
date,close
1-May-12,58.13
30-Apr-12,53.98
27-Apr-12,67.00
I'd like to change the data so that the date is the same for all, but the times and temperature readings are different values.
I changed the time.format line to:
var parseDate = d3.time.format("%d-%b-%y %H").parse;
to include the times with the readings, but the graph breaks when I do this.
I'd like to use this data with the same date but different times and temperature readings:
1-May-12 06:00,58.13
1-May-12 06:30,53.98
1-May-12 07:00,67.00
How do I modify the x-axis code to work with the values above?
You can provide specific tick values to be displayed on a d3 chart.
Firstly, correct your date parser:
var parseDate = d3.time.format("%e-%b-%y %H:%M").parse; // %e instead of %d
First, you need to get the list of tick values you want to display. After you've loaded the csv and processed it, extract the tick values and assign them to your xAxis:
d3.csv("data.csv", function(error, data) {
data.forEach(function(d) {
d.date = parseDate(d.date);
d.close = +d.close;
});
var tickValues = data.map(function(d) { return d.date; });
xAxis
.tickValues(tickValues)
.tickFormat(d3.time.format('%H:%M'));
Do not forget to remove the ticks on your current xAxis definition.
Edit: After Cyril correctly solved the problem I noticed that simply putting the functions that generate my axes underneath the functions used to generate the labels solves the problem.
I've almost finished reading the O'Reilly book's tutorials on D3.js and made the scatter graph on the penultimate page, but when adding the following code to generate my X axis more than half of my labels disappear:
// Define X Axis
var xAxis = d3.svg.axis()
.scale(xScale)
.orient('bottom');
// Generate our axis
svg.append('g')
.call(xAxis);
The odd thing is that of the labels that don't disappear the 3 that stay are the bottom 3 pairs from my dataset ([85,21], [220,88], [750,150]):
var myData = [
[5, 20],
...,
...,
[85, 21],
[220, 88],
[750,150]
];
Here is an image of what's happening, prior to adding the axis at the top each of these points had red text labels:
Below is the rest of the code that generates my scatter graph, it follows the methods explained in the book almost exactly and I can't pinpoint where the error is coming from.
// =================
// = SCALED SCATTER GRAPH
// =================
var p = 30; // Padding
var w = 500 + p; // Width
var h = 500 + p; // Height
// SVG Canvas and point selector
var svg = d3.select('body')
.append('svg')
.attr('width',w)
.attr('height',h);
// Scales take an input value from the input domain and return
// a scaled value that corresponds to the output range
// X Scale
var xScale = d3.scale.linear()
.domain([0, d3.max(myData, function(d){
return d[0];
})])
.range([p, w - (p + p)]); // With padding. Doubled so labels aren't cut off
// Y Scale
var yScale = d3.scale.linear()
.domain([0, d3.max(myData, function(d){
return d[1];
})])
.range([h - p, p]); // With padding
// Radial scale
var rScale = d3.scale.linear()
.domain([0, d3.max(myData, function(d){ return d[1];})])
.range([2,5]);
// Define X Axis
var xAxis = d3.svg.axis()
.scale(xScale)
.orient('bottom');
// Generate our axis
svg.append('g')
.call(xAxis);
// Plot scaled points
svg.selectAll('circle')
.data(myData)
.enter()
.append('circle')
.attr('cx', function(d){
return xScale(d[0]);
})
.attr('cy', function(d){
return yScale(d[1]);
})
.attr('r', function(d){
return rScale(d[1]);
});
// Plot all labels
svg.selectAll('text')
.data(myData)
.enter()
.append('text')
.text(function(d){
return d;
})
.attr('x', function(d){
return xScale(d[0]);
})
.attr('y', function(d){
return yScale(d[1]);
})
.style('fill', 'red')
.style('font-size',12);
js-fiddle: https://jsfiddle.net/z30cqeoo/
The problem is here:
svg.selectAll('text')
The x axis and y axis makes text element as ticks, so when the axis are present the above line will return array of ticks, thus it explains why it's not displaying when axis is added.
So the correct way would be to do something like this:
svg.selectAll('.text') //I am selecting those elements with class name text
svg.selectAll('.text')
.data(myData)
.enter()
.append('text')
.text(function(d){
console.log(d)
return d;
})
.attr('x', function(d){
return xScale(d[0]);
})
.attr('y', function(d){
return yScale(d[1]);
})
.attr('class',"text") //adding the class
.style('fill', 'red')
.style('font-size',12);
Full working code here.
I have been experimenting with D3 for some time now, but i have come to a dead end.
On initialization i am creating an svg with the axes set on some default data.
Graph.initiateGraph = function(data){
Graph.time_extent = d3.extent(data, function(d) { return d.date; });
Graph.time_scale = d3.time.scale()
.domain(Graph.time_extent)
.range([Graph.padding, Graph.w]);
Graph.value_extent = d3.extent(data, function(d) { return parseInt(d.value); });
Graph.value_scale = d3.scale.linear()
.domain(Graph.value_extent)
.range([Graph.h, Graph.padding]);
Graph.svg = d3.select("#graph")
.append("svg:svg")
.attr("width", Graph.w + Graph.padding)
.attr("height", Graph.h + Graph.padding)
.call(d3.behavior.zoom().x(Graph.time_scale).y(Graph.value_scale).on("zoom", Graph.zoom));
Graph.chart = Graph.svg.append("g")
.attr("class","chart")
.attr("pointer-events", "all")
.attr("clip-path", "url(#clip)");
Graph.line = d3.svg.line()
.x(function(d){ return Graph.time_scale(d.date); })
.y(function(d){ return Graph.value_scale(d.value); })
.interpolate("linear");
Graph.time_axis = d3.svg.axis()
.scale(Graph.time_scale)
.orient("bottom").ticks(5);
Graph.value_axis = d3.svg.axis()
.scale(Graph.value_scale)
.orient("left").ticks(5);
Graph.svg.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + Graph.h + ")")
.call(Graph.time_axis);
Graph.svg.append("g")
.attr("class","y axis")
.attr("transform","translate(" + Graph.padding + ",0)")
.call(Graph.value_axis);
}
Then, i have a pair of input boxes that implement jquery calendar script to provide 2 dates (start and end). I then update my svg chart with these dates.
$("#date_filter_button").click(function(){
var start_date = $("#analysis [name='date_start']").datepicker('getDate');
var end_date = $("#analysis [name='date_end']").datepicker('getDate');
Graph.time_extent = [start_date.getTime(),end_date.getTime()];
Graph.time_scale.domain(Graph.time_extent);
Graph.svg.select(".x.axis").transition().call(Graph.time_axis);
}
Unit here everything works like a charm. The x axis domain changes to the dates provided. So then i draw the chart and it works beautifully.
The problem comes when i try to pan/zoom the chart. On the first pan/zoom, the time axis changes to the first default values i used on the initialization.
Graph.zoom = function(){
var trans = d3.event.translate[0],
scale = d3.event.scale;
Graph.svg.select(".x.axis").call(Graph.time_axis);
Graph.svg.select(".y.axis").call(Graph.value_axis);
}
It's like when the zoom function comes in and calls the Graph.time-axis, it uses the initial values of the Graph.time_extent and/or Graph.time_scale values...
I have a line chart and the full array of data is attached to the line. I want to change from using the value column to the pct (percent) column in the data. Is there a way of doing this in place, ie. using the values already in the DOM without passing it a new set of data?
as far as I've got - http://bl.ocks.org/3099307
var width = 700, // width of svg
height = 400, // height of svg
padding = 100; // space around the chart, not including labels
var data=[{"date":new Date(2012,0,1), "value": 3, 'pct': 55},
{"date":new Date(2012,0,3), "value": 2, "pct": 30 },
{"date":new Date(2012,0,12), "value": 33, "pct": 10},
{"date":new Date(2012,0,21), "value": 13, "pct": 29},
{"date":new Date(2012,0,30), "value": 23, "pct": 22}];
var x_domain = d3.extent(data, function(d) {
return d.date; }),
y_domain = d3.extent(data, function(d) { return d.value; });
// define the y scale (vertical)
var yScale = d3.scale.linear()
.domain(y_domain)
.range([height - padding, padding]); // map these top and bottom of the chart
var xScale = d3.time.scale()
.domain(x_domain)
.range([padding, width - padding]); // map these sides of the chart, in this case 100 and 600
// define the y axis
var yAxis = d3.svg.axis()
.orient("left")
.scale(yScale);
// define the x axis
var xAxis = d3.svg.axis()
.orient("bottom")
.scale(xScale);
// create the svg
var div = d3.select("body");
div.select("svg").remove();
var vis = div.append("svg")
.attr("width", width)
.attr("height", height)
.attr("transform", "translate(" + padding + "," + padding + ")");
// draw y axis with labels and move in from the size by the amount of padding
vis.append("g")
.attr("class", "axis yaxis")
.attr("transform", "translate("+padding+",0)")
.call(yAxis);
// draw x axis with labels and move to the bottom of the chart area
vis.append("g")
.attr("class", "axis")
.attr("transform", "translate(0," + (height - padding) + ")")
.call(xAxis);
// DRAW LINES
var line = d3.svg.line()
.x(function(d) {
return xScale(d.date); })
.y(function(d) {
return yScale(d.value); })
.interpolate("basis");
vis.selectAll(".lines")
.data([data])
.enter()
.append("svg:path")
.attr("d", line)
.attr("class", "lines");
function rescale() {
// change the y axis to show percentage
yScale.domain([0,100]) // redraw as percentage outstanding
vis.select(".yaxis")
.transition().duration(1500).ease("sin-in-out") // https://github.com/mbostock/d3/wiki/Transitions#wiki-d3_ease
.call(yAxis);
What happens here?
// now redraw the line to use pct
line.y(function(d) {
return yScale(d.pct); });
vis.selectAll("lines")
.transition()
.duration(500)
.ease("linear");
}
Your data is already joined, so you just need to update your selection:
var yPctScale = d3.scale.linear()
.domain([0, 100])
.range([height - padding, padding]);
var pct_line = d3.svg.line()
.x(function(d) {
return xScale(d.date); })
.y(function(d) {
return yPctScale(d.pct); })
.interpolate("basis");
vis.selectAll(".lines")
.transition().duration(1500).ease("sin-in-out")
.attr("d", pct_line);