Logarithmic time scale - d3.js

How to make a logarithmic datetime scale in D3?
a simple time scale is like this:
d3.time.scale()
.domain([new Date(2014, 0, 1), new Date()])
.range([0, 500])
and a simple log scale is like:
d3.scale.log()
.domain([new Date(2014, 0, 1), new Date()])
.rangeRound([0, 500])
.base(10)
Tried to chain their syntax in a various ways with no effect.
Chart will position users by last login date. Range will be about one year. If we space data linearly, most users will collide during last days/hours. With logarithm we can zoom last hours.
Solution could be by interactive zoom or several charts. But goal here is to make single static chart with nonlinear overview of year.
One alternative could be to convert datetime to "days from now", a number. It would work for data. But then I wouldn't know how to label axis ticks like "01-01-2014"...

Something like the below seems to fool d3js into thinking it has a real scale object. It should make a good starting point:
var xt = d3.scaleUtc()
.domain([start, now])
.range([1, width])
var xp = d3.scalePow()
.exponent(2)
.domain([1, width])
.range([0, width])
// Fool d3js into thinking that it is looking at a scale object.
function x_copy() {
var x = function(t) { return xp(xt(t)) }
x.domain = xt.domain
x.range = xp.range
x.copy = x_copy
x.tickFormat = xt.tickFormat
x.ticks = xt.ticks
return x
}
x = x_copy()
var xAxis = d3.axisBottom(x)

Create two scales and use one after the other. First use the time scale and than the log or pow scale.
var parseDate = d3.time.format("%Y-%m-%d").parse;
var x = d3.time.scale()
.range([0,width]);
var xLog = d3.scale.pow().exponent(4)
.domain([1,width])
.range([0,width]);
than I'm using .forEach to get the linear points:
x.domain(d3.extent(data, function(d) { return parseDate(d.start); }));
data.forEach(function(d) {
d.start = x(parseDate(d.start));
});
when I'm drawing the objects I add the log scale:
.attr('cx', function (d) { return xLog(d.start)})

Related

How to use d3.scaleLinear to find the radius of a map in d3

I have a chart as shown below
where the radius of the pie chart on the chart is am trying to do it as shown below
<PieChartMarkers
totalSites={totalSites}
key={i}
keymarket={name}
pieData={pieData}
x={projection(coordinates)[0]}
y={projection(coordinates)[1]}
**radius={this.getMarketRadius(totalCase)}**
mouseOverHandler={this.showTooltip}
mouseOutHandler={this.hideTooltip}
/>
getMarketRadius = (totalCase) => {
let radius = null;
// let data=d3.scaleLinear([10,130]).range([0,960]);
let callback= d3.scaleLinear()
.range([0, totalCase])
.domain([10, 130])
radius = totalCase / 650 + 10;
console.log(radius)
return radius;
};
currently i am getting the radius radius = totalCase / 650 + 10; which is working fine but suggestion is to use d3.scaleLinear to get the radius of the on chart when trying to use i am getting the value as 1313316 using the below code snippet
//totalCase is the variable value coming from API
let callback= d3.scaleLinear()
.range([0, totalCase])
.domain([10, 130]
please help me understand how to get the radius using d3.scaleLinear to draw the pie chart on the map
First of all, you should not use a linear scale to get these radii: we encode the data as the area of the circles, not as their radii. That said, you should use d3.scaleSqrt. Also, set the 0 range for the 0 domain, because there should be no circle if there are no cases.
So, your function would be something like this:
getMarketRadius = totalCase => {
return d3.scaleLinear().range([0, totalCase]).domain([0, 130])(totalCase);
};
A better option is just setting the scale outside the function, and using it directly for setting the radius.
myScale = d3.scaleLinear()
.range([0, totalCase])
.domain([0, 130]);
radius={this.myScale(totalCase)}

d3.v4: How to set ticks every Math.PI/2

In the d3.v4 documentation the following is stated:
To generate ticks every fifteen minutes with a time scale, say:
axis.tickArguments([d3.timeMinute.every(15)]);
Is there a similar approach that can be used with values other than time? I am plotting sine and cosine curves, so I'd like the ticks to begin at -2*Math.PI, end at 2*Math.PI, and between these values I'd like a tick to occur every Math.PI/2. I could, of course, explicitly compute the tick values and supply them to the tickValue method; however, if there is a simpler way to accomplish this, as in the time-related example quoted above, I'd prefer to use that.
Setting the end ticks and specifying the precise space of the ticks in a linear scale is a pain in the neck. The reason is that D3 axis generator was created in such a way that the ticks are automatically generated and spaced. So, what is handy for someone who doesn't care too much for customisation can be a nuisance for those that want a precise customisation.
My solution here is a hack: create two scales, one linear scale that you'll use to plot your data, and a second scale, that you'll use only to make the axis and whose values you can set at your will. Here, I choose a scalePoint() for the ordinal scale.
Something like this:
var realScale = d3.scaleLinear()
.range([10,width-10])
.domain([-2*Math.PI, 2*Math.PI]);
var axisScale = d3.scalePoint()
.range([10,width-10])
.domain(["-2 \u03c0", "-1.5 \u03c0", "-\u03c0", "-0.5 \u03c0", "0",
"0.5 \u03c0", "\u03c0", "1.5 \u03c0", "2 \u03c0"]);
Don't mind the \u03c0, that's just π (pi) in Unicode.
Check this demo, hover over the circles to see their positions:
var width = 500,
height = 150;
var data = [-2, -1, 0, 0.5, 1.5];
var realScale = d3.scaleLinear()
.range([10, width - 10])
.domain([-2 * Math.PI, 2 * Math.PI]);
var axisScale = d3.scalePoint()
.range([10, width - 10])
.domain(["-2 \u03c0", "-1.5 \u03c0", "-\u03c0", "-0.5 \u03c0", "0", "0.5 \u03c0", "\u03c0", "1.5 \u03c0", "2 \u03c0"]);
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height);
var circles = svg.selectAll("circle").data(data)
.enter()
.append("circle")
.attr("r", 8)
.attr("fill", "teal")
.attr("cy", 50)
.attr("cx", function(d) {
return realScale(d * Math.PI)
})
.append("title")
.text(function(d) {
return "this circle is at " + d + " \u03c0"
});
var axis = d3.axisBottom(axisScale);
var gX = svg.append("g")
.attr("transform", "translate(0,100)")
.call(axis);
<script src="https://d3js.org/d3.v4.min.js"></script>
I was able to implement an x axis in units of PI/2, under program control (not manually laid out), by targetting the D3 tickValues and tickFormat methods. The call to tickValues sets the ticks at intervals of PI/2. The call to tickFormat generates appropriate tick labels. You can view the complete code on GitHub:
https://github.com/quantbo/sine_cosine
My solution is to customise tickValues and tickFormat. Only 1 scale is needed, and delegate d3.ticks function to give me the new tickValues that are proportional to Math.PI.
const piChar = String.fromCharCode(960);
const tickFormat = val => {
const piVal = val / Math.PI;
return piVal + piChar;
};
const convertSIToTrig = siDomain => {
const trigMin = siDomain[0] / Math.PI;
const trigMax = siDomain[1] / Math.PI;
return d3.ticks(trigMin, trigMax, 10).map(v => v * Math.PI);
};
const xScale = d3.scaleLinear().domain([-Math.PI * 2, Math.PI * 2]).range([0, 600]);
const xAxis = d3.axisBottom(xScale)
.tickValues(convertSIToTrig(xScale.domain()))
.tickFormat(tickFormat);
This way if your xScale's domain were changed via zoom/pan, the new tickValues are nicely generated with smaller/bigger interval

D3 Chart - Using tickValues for Time Gives "translate(NaN,0)"

In my d3 line chart, I only want ticks for the plotted data. This proves to be a issue with time stamps though as I get:
d3.js:7651 Error: <g> attribute transform: Expected number, "translate(NaN,0)"..
I thought to convert the strings to numbers in the tickValues array but I can not since it's got a colon. Any ideas?
// Hard coded data
scope.data = [
{date: '12:00', glucoseLevel: 400},
{date: '15:00', glucoseLevel: 200},
{date: '18:00', glucoseLevel: 300},
{date: '23:00', glucoseLevel: 400}
];
var parseDate = d3.timeParse('%I:%M');
scope.data.forEach(function(d) {
d.date = parseDate(d.date);
d.glucoseLevel = +d.glucoseLevel;
});
var x = d3.scaleTime()
.range([0, width]);
var xAxis = d3.axisBottom(x)
.tickValues(['12:00', '15:00', '18:00', '23:00']);
// Add the X Axis
svg.append('g')
.attr('class', 'x axis')
.attr('transform', 'translate(0,' + height + ')')
.call(xAxis);
You are specifying X values as times, so you must also specify the X-axis tick values as times.
As you already have the X values in the correct format, you can just write
var xAxis = d3.axisBottom(x)
.tickValues(scope.data.map(function (d) { return d.date; }));
.tickValues() isn't for setting the tick labels, it's for setting where on the axis the ticks appear. If you want the tick labels formatted in some way, specify a formatter using tickFormat, for example:
var xAxis = d3.axisBottom(x)
.tickValues(scope.data.map(function (d) { return d.date; }))
.tickFormat(d3.timeFormat("%H:%M"));
I've used the format string %H:%M instead of %I:%M as %I is hours in the range 01-12 whereas %H uses the 24-hour clock. For consistency I'd recommend changing your time parsing function to d3.timeParse('%H:%M'), although parsing a time with the hours greater than 12 using %I seems to work.
Finally, you'll also need to set the domain of your scale object x, for example:
var x = d3.scaleTime()
.domain([parseDate('12:00'), parseDate('23:00')])
.range([0, width]);
The two values passed to domain are the minimum and maximum X values to use for the axis. I've used the minimum and maximum values of your data, but I could have chosen a different time range (e.g. 00:00 to 24:00) as long as it contained all of your data points.

How can I get a time axis onto an object, using D3?

I'm very new to d3 and trying to learn by building a visualization.
My goal right now is to make a circle and color the circle based on some temporal data. I've made the circle, and want to add a timescale to it. The circle I have created fine using d3.arc() on an svg element. I have also created a time scale (seen below). My question is, how can I "attach" this time scale to the circle? I want to be able to say that at xyz point in time, my data holds this value, so now color the circle based on a color scale.
Or...am I going about this wrong?
var time = d3.scale.ordinal()
.domain(d3.extent(data, function(d) {
return d.date;
}))
I think you may need to use a quantitative scale instead of ordinal.
https://github.com/mbostock/d3/wiki/Ordinal-Scales says -
Ordinal scales have a discrete domain, such as a set of names or categories
and in your code, you use the "extent" of the date property, which only gives you 2 values - the earliest and most recent date in your data. That is a discrete domain, but a very limited one, and wouldn't represent your data very well. The scale will only output at most 2 values.
var now = Date.now();
var then = now - 1000;
var colors = d3.scale.ordinal()
.domain([then, now])
.range(['#ff0000','#0000ff']);
colors(then); // red
colors(now); // blue
colors(now - 500); // red... expecting violet
change 'ordinal' to 'linear' and leave the rest as is.
var now = Date.now();
var then = now - 1000;
var colors = d3.scale.linear()
.domain([then, now])
.range(['#ff0000','#0000ff']);
colors(then); // red
colors(now); // blue
colors(now - 500); // violet
The tricky part (at least for me) was remembering that the output of d3.scale.linear() (the 'colors' variable above) is a function. It can be called just like any other function.
var fakeData = d3.range(then, now, 10);
var svg = d3.select('body')
.append('svg')
.attr({ height: 500, width: 500 });
var circle = svg.append('circle')
.attr({ r: 100, cx: 250, cy: 250 });
function changeTime(time){
circle.attr('fill', colors(time));
}

d3 mapping numbers with scale

If I set scales so points are in scale on the svg with:
d3.scale.linear().domain(xDomain).range([0, width]);
How can I get the unscaled number of the mouse x position?
Eg.
xPositions = [1,7,10]
7 is # x = ~300 on an svg of width 500
How would I map 300 to 7 based on arbitrary data?
You can use the scale.invert function for the inverse mapping:
var xScale = d3.scale.linear().domain(xDomain).range([0, width]);
var xDomainPos = [1, 7, 10];
var xRangePos = xDomainPos.map(function (d) { return xScale(d); });
var xNewDomainPos = xRangePos.map(function (d) { return xScale.invert(d); });

Resources