I'm implementing a 2d chart using canvas. I want to reuse d3's logic for generating the chart's axes. d3 does quite a lot of good work in generating axes and I'd like to take advantage of it.
(Note: For backward-compatibility reasons I'm stuck using d3v3 for the time being.)
Consider the following d3 code:
let scale = d3.time.scale()
.range([0, width])
.domain([lo, hi]);
let axis = d3.svg.axis()
.scale(scale)
.ticks(num_ticks)
.tickSize(10)
.orient("bottom");
I can render this into a chart div with:
svg.selectAll('.x-axis').call(axis);
I want to be able to programmatically get the tick data out of axis, including the formatted labels, by writing a function that behaves as follows:
ticks = get_axis_ticks(axis)
ticks should hold each tick position (as a Date in this particular case) and the corresponding d3-generated label.
[[Date(...), "Wed 19"],
[Date(...), "Fri 21"],
[Date(...), "Apr 23"],
[Date(...), "Tue 25"],
...]
I could then use this data to paint an axis on my canvas.
I've dug into d3v3 source (in particular here: https://github.com/d3/d3/blob/v3.5.17/src/svg/axis.js) but I find it very difficult to tease apart the logic from the SVG manipulation.
Help would be much appreciated.
One idea I have is to use the scale function you have created to generate the ticks you desire and push them into an array.
As a very simple example, if you would like 10 ticks, each incrementing by a unit of 1, you could do something like this: https://jsfiddle.net/Q5Jag/3148/
//define dummy values
var lo = 1;
var hi = 10;
var width = 512
var scale = d3.time.scale()
.range([0, width])
.domain([lo, hi]);
//define your function
var get_x_axis = function() {
let axisArr = [];
for(var i = 0; i < 10; i++ ) {
//calculate your value
axisArr.push(scale(i))
}
return axisArr
}
//call it
let axisTicks = get_x_axis()
//log it
console.log(axisTicks)
I'm not sure if this is what you're looking for, but if you need further help just ask.
I was able to get this working. I found the time formatting strategy in the d3 docs: https://github.com/d3/d3-3.x-api-reference/blob/master/Time-Formatting.md#format_multi I believe this is the strategy that d3 itself uses by default when users do not provide a custom format.
I learned that simply calling scale.ticks(N) will return Nish ticks suitable for rendering on an axis. These tick values are chosen on natural boundaries. E.g., if you're working with a time axis, these ticks will be aligned on minute, hour, day boundaries.
Here's a solution:
let time_format = d3.time.format.multi([
[".%L", d => d.getMilliseconds()],
[":%S", d => d.getSeconds()],
["%I:%M", d => d.getMinutes()],
["%I %p", d => d.getHours()],
["%a %d", d => d.getDay() && d.getDate() !== 1],
["%b %d", d => d.getDate() !== 1],
["%B", d => d.getMonth()],
["%Y", () => true]
]);
let get_ticks = (width, lo, hi, num_ticks) => {
let scale = d3.time.scale().range([0, width]).domain([lo, hi]);
return scale.ticks(num_ticks).map(t => [t, time_format(t)]);
};
Related
I would like to create a buffer between my smallest/largest data points and the axes, so that upon initial load and before zoom you can see the entire min/max dot and it won't be cut off. My code Pen is at : https://codepen.io/lahesty/pen/XYoyxV (lines 119-141)
I believe that has something to do with scaling? Here's what I have currently. Perhaps I can adjust either the range or domain to give it a little more space before the user zooms. Here is what I have:
// scale, set ranges
var x = d3.scaleLinear()
.range([0, width-100])
.domain(d3.extent(data[0], function(d) { return d.inspected_at; }));
var y = d3.scaleLinear()
.range( [height, 0])
.domain(d3.extent(data[0], function(d) { return d.flow_data; }));
//create x and y axis
var xAxis = d3.axisBottom()
.scale(x)
.ticks((width+2)/(height+2))
.tickSize(-height)
.tickPadding(10)
.tickFormat(d3.timeFormat("%b %d, %H:%M:%S"))
var yAxis = d3.axisLeft()
.scale(y)
.ticks(5)
.tickSize(-(width-100))
.tickPadding(10)
Thanks so much! Let me know if I can provide any more info
Edit: Edit this is different from the possible duplicate answer in that scaling time is a different function of d3 than just a normal number/digit. See answer below (interval.offset)
Since you have a time scale, you cannot simply multiply the domain values by a given number, as the answer linked in the comments correctly does (which is the most common solution for a linear scale).
In a time scale, you can add or subtract time using interval.offset.
For instance, in your case, I'll subtract 10 seconds from the first date and add 10 seconds in the last date:
var x = d3.scaleTime()
.domain([d3.timeSecond.offset(d3.min(data[0], function(d) {
return d.inspected_at;
}), -10),
d3.timeSecond.offset(d3.max(data[0], function(d) {
return d.inspected_at;
}), +10)
])
.range([0, width - 100]);
That should be enough, but you can change that magic number the way you want.
Also, it's worth mentioning that interval.offset is way easier and handier than the JS alternatives, since you can use d3.timeSecond for seconds, d3.timeMinute for minutes, d3.timeDay for days and so on, adding or subtracting the necessary amount of seconds, minutes, days, weeks etc you want.
Here is the updated CodePen: https://codepen.io/anon/pen/NBEyrN?editors=0010
How do I put x-axis values and labels in between two ticks in a chart like the screenshot below?
Each tick represents the start/end date of a period interval (the week of the year in this case), and the dot and bar represent the values for the period.
TL;DR - For a working code sample, please see my D3 block here.
First, the scale for the x-axis must be a time scale:
let xScale = d3.scaleTime()
.domain([domainStartDate, domainEndDate]);
In D3, each axis tick has a line element to draw the tick line, and a text element for the tick label (see API reference). Once we understand this, the solution below would become clearer:
For each x value, we generate two ticks. The first tick represents the mid date of the period, and the second tick represent the end date of the period. Note that the end date of one period is also the start date of the next period. We will hide and display the tick lines and labels in alternate manner in the end.
let tickValues = values
.map((value) => [value.midDate, value.endDate])
.reduce((acc, cur) => {
return acc.concat(cur);
}, []);
Next, we construct an axis generator using the xScale and tickValues above:
let xAxisWeekGenerator = d3.axisBottom(xScale)
.tickValues(tickValues)
.tickSize(10)
.tickPadding(5)
.tickFormat((d, i) => {
// specify the text label to be returned here.
let index = Math.floor(i / 2);
return values[index].Week;
});
Call the axis generator on your svg:
let xAxisWeekUi = svg.append('g')
.attr('id', 'xAxisWeek')
.attr('transform', `translate(0, ${height})`)
.call(xAxisWeekGenerator);
By now, if you view the page in your browser, you will see duplicated x-axis tick lines and labels for each value.
We just need to apply some CSS class to hide and display the tick lines and labels in zig-zag alternate manner:
JavaScript:
xAxisWeekUi.selectAll('.tick')
.attr('class', (d, i) => {
if (i % 2 === 0) {
return 'tick midWeek';
}
return 'tick endWeek';
});
CSS:
/* hide the midWeek tick line. */
#xAxisWeek .tick.midWeek line {
display: none;
}
/* hide the endWeek tick label. */
#xAxisWeek .tick.endWeek text {
display: none;
}
The final output where the x-value label is between tick lines:
I'm using D3 (v3) to build a plot with two axis. One of them it isn't working correctly when I add two or more objects. My piece of code is:
//Set upperLevelyScale
var previousValues = 0;
var upperLevelyScale = d3.scale.ordinal()
.domain(Object.keys(valuesY_numbers).sort())
.rangePoints((function(){
var values = Object.values(valuesY_numbers).map(function(x){
previousValues += (x * itemSize);
return previousValues});
values.unshift(0);
return values;
})());
when domain is ["MyValue1"] and rangePoints is [0, 170], the tick of the axis shows perfectly at the middle of the axis. But if domain is ["MyValue1", "MyValue2"] and rangePoints is [0,170,320] the ticks are not really covering their part but other.
What I'm trying to set up is just: MyValue1 is from 0 to 170, MyValue2 is from 170 to 320, etc. I have tried with '.range' also but still not working.
Thanks in advance.
I have converted a line chart into a cumulative line chart and its y values are not displayed correctly. The range of the y axis should be 80.00 - 140.00 but instead I get -0.08 - 0.20. Has anyone managed to tweak their normalization code below to make it work with all kinds of ranges?
line.values = line.values.map(function(point, pointIndex) {
point.display = {
'y': (lines.y()(point, pointIndex) - v) / (1 + v)
};
return point;
})
Any help will be greatly appreciated.
I know that this question is somewhat old, but I am convinced that the normalization code for the cumulative line chart is not conceptually correct. Furthermore, the NVD3 cumulative line chart implementation is actually an index chart implementation (see Mike Bostock's example). A cumulative line chart would be more like this, I think. The cumulative chart can be easily achieved using the NVD3 line chart and some quick modifications to the underlying data.
If we take Bostock to be correct, and we really do wish to achieve an indexed line chart, then the indexify function in NVD3 should be changed to:
/* Normalize the data according to an index point. */
function indexify(idx, data) {
if (!indexifyYGetter) indexifyYGetter = lines.y();
return data.map(function(line, i) {
if (!line.values) {
return line;
}
var indexValue = line.values[idx];
if (indexValue == null) {
return line;
}
var v = indexifyYGetter(indexValue, idx);
// TODO: implement check below, and disable series if series
// causes a divide by 0 issue
if ((Math.abs(v) < 1e-6) && !noErrorCheck) {
// P.S. You may have to set a higher threshold (~1e-6?).
// I don't know I didn't run any tests...
line.tempDisabled = true;
return line;
}
line.tempDisabled = false;
line.values = line.values.map(function(point, pointIndex) {
point.display = {
'y': (indexifyYGetter(point, pointIndex) - v) / v
};
return point;
});
return line;
})
}
I asked a related question to the authors of NVD3 and plan to submit a pull request. Note that percentage change charts are really only meaningful when all of the underlying data is positive. When you start throwing negative values into the mix, percentage change loses all of its meaning.
What I found works is to insert another point with a y value of 0 at the beginning of the sequence of points.
Given a list of data points in the form [ [x1,y1], [x2,y2], ... [xn,yn]] ],
something like values.upshift([0,0]) works for me (the x value is arbitrary, but i just use 0 or values[0][0]) to insert to the front of the list.
(I'm getting the same thing with that chart. I'm still looking into it, but I hope this helped.)
I am trying to render a dc.js barChart where my y-axis is percentage 0-100% and my x-axis are numbers, however I want to order the x-axis by date.
My data looks like this:
date,trend,percent
01/01/2014 13:00,238,53.6
01/01/2014 13:15,239,64.2
01/01/2014 13:30,219,43.1
01/01/2014 13:45,219.2,43.1
01/01/2014 14:00,237.4,50.6
...
I am adding the data to crossfilter
data.forEach(function (d) { d.date = parseDate(d.date); });
var ndx = crossfilter(data);
var trendDimension = ndx.dimension(function (d) { return d.trend; });
var trendGroup = trendDimension.group().reduce(
function (p, v) {
p.time = v.date.getTime();
p.trend = +v.trend;
p.percent = +v.percent;
return p;
},
...
).order(function (p) { return p.time; }); // ??? order by time rather than trend
When I graph the dimension and group, my x-axis is sorted by trend as my x domain looks like:
var minTrend = trendDimension.bottom(1)[0].trend;
var maxTrend = trendDimension.top(1)[0].trend;
...
chart.x(d3.scale.linear().domain([minTrend, maxTrend]);
...
chart.render();
Everything plots, however the bars a sorted in order of trend and I would like them sorted in order of date/time.
Is this possible?
EDIT
I also tried:
chart.ordering(function (d) { return d.value.time; });
but that does not seem to have an effect on the ordering...
Do you want to graph percent versus trend or percent versus time?
Right now your dimension is on trend, so it will not be possible to order it by date. Crossfilter will create a bin for each trend value (which may have many dates), and the way you have written your reduce function, it will simply replace the date entry for the bin, with the last date it sees.
If you want to order by date and then use trend to affect some other aesthetic (color for example), you should use a date dimension, group by some quantization of the date, not do anything with the date in your reduce, and use date scale/xUnits.