How to set y axis (yDomain) value to max in D3 Js - javascript

In My D3 chart, How to set yDomain to the max value. Any suggestions ?
If we see on nov 10 my line and area is going out of the box.
My Code sandbox here
I am calculating the two domains separately for yDomainMagnitude and yDomainStartupMagnitude, but now how to consolidate or take the union of both and assign to yDomain.
var yDomainMagnitude = d3.extent(data, function(d) {
return d.magnitude;
});
var yDomainStartupMagnitude = d3.extent(data, function(d) {
return d.startupMagnitude;
});
var yDomain = yDomainStartupMagnitude; // here I have to have union of both and assign.
var xScale = d3
.scaleTime()
.range([0, width])
.domain(xDomain);
var yScale = d3
.scaleLinear()
.range([height, 0])
.domain(yDomain);

There are several ways to calculate the yDomain in place. Yours does obviously work, it can be significantly simplified, though:
var yDomain = [
d3.min(data, d => Math.min(d.magnitude, d.startupMagnitude)),
d3.max(data, d => Math.max(d.magnitude, d.startupMagnitude))
];
This approach checks whatever value magnitude or startupMagnitude is smaller / greater using Math.min() / Math.max(), respectively, for any data point. It then utilizes d3.min() and d3.max() to calculate the global extent over those values.

As you have a relational condition inside your function, it'll always take the larger value. You could instead calculate the two domains separately for startupMagnitude and magnitude, and then union the extents (take the minimum and maximum of the minimum and maximum extent values, respectively).
const magnitude = d3.extent(data, d => d.magnitude);
const startup = d3.extent(data, d => d.startupMagnitude);
const yDomain = [
d3.min([magnitude[0], startup[0]]),
d3.max([magnitude[1], startup[1]])
];
Or, a single-assignment version, exploiting that you just union the extents:
const yDomain = d3.extent([
...d3.extent(data, d => d.magnitude),
...d3.extent(data, d => d.startupMagnitude)
]);
Yet another is to union the data first, then compute the extent:
const yDomain = d3.extent([
...data.map(d => d.magnitude),
...data.map(d => d.startupMagnitude)
]);
These don't mix the D3 way of calculating min/max/extent with the different semantics of Math.min/Math.max, eg. handling of strings, nulls, and results if the array is empty. In this regard, the last version is the most faithful to the D3 way, as it uses a single d3.extent.

This is how I am calculating the two domains separately for startupMagnitude and magnitude, and then comparing the max and min and creating the yDomain.
I am not sure this is correct or any other d3 method is there,
var yDomainMagnitude = d3.extent(data, function(d) {
return d.magnitude;
});
var yDomainStartupMagnitude = d3.extent(data, function(d) {
return d.startupMagnitude;
});
var max =
d3.max(d3.values(yDomainMagnitude)) >
d3.max(d3.values(yDomainStartupMagnitude))
? d3.max(d3.values(yDomainMagnitude))
: d3.max(d3.values(yDomainStartupMagnitude));
var min =
d3.min(d3.values(yDomainMagnitude)) <
d3.min(d3.values(yDomainStartupMagnitude))
? d3.min(d3.values(yDomainMagnitude))
: d3.min(d3.values(yDomainStartupMagnitude));
var yDomain = [min, max];

Related

D3: Passing extra arguments to attr functions inside a selection.join()

I've some code inside a selection.join() pattern:
const nodeWidth = (node) => node.getBBox().width;
const toolTip = selection
.selectAll('g')
.data(data)
.join(
(enter) => {
const g = enter
.append('g')
g.append('text')
.attr('x', 17.5)
.attr('y', 10)
.text((d) => d.text);
let offset = 0;
g.attr('transform', function (d) {
let x = offset;
offset += nodeWidth(this) + 10;
return `translate(${x}, 0)`;
});
selection.attr('transform', function (d) {
return `translate(${
(0 - nodeWidth(this)) / 2
},${129.6484} )`;
});
},
(update) => {
update
.select('text')
.text((d) => d.text);
let offset = 0;
update.attr('transform', function (d) {
let x = offset;
offset += nodeWidth(this) + 10;
return `translate(${x}, 0)`;
});
selection.attr('transform', function (d) {
return `translate(${
(0 - nodeWidth(this)) / 2
},${129.6484} )`;
});
}
);
as you can see, in the enter and update section I need to call a couple of functions to calculate several nodes transformations. In particular, the code stores in the accumulation var offset the length of the previous text element. This properly spaces text elements (ie, text0 <- 10 px -> text1 <- 10 px -> ...).
As you can see, the "transform functions" in the enter and update section are identical. I'm trying to define them just in one place and call them where I need. E.g.,
(update) => {
update.attr('transform', foo);
selection.attr('transform', bar);
}
However, I cannot refactor the code this way because it looks like I cannot pass in neither the offset value nor this to the function passed to attr().
Is there a way to do it?
EDIT:
As per Gerardo Furtado's hint (if I got it right), you can define foo as follows:
const foo = function(d, i, n, offset) {
let x = offset;
offset += nodeWidth(n[i]) + 10;
return `translate(${x}, 0)`;
}
then in the selection.join¡ you have to call foo this way:
(update) => {
let offset = 0;
update.attr('transform', (d, i, n) => foo(d, i, n, offset));
}
However, refactoring this way, offset is ever equal to 0. A possibile solution here: https://stackoverflow.com/a/21978425/4820341
Have a look at Function.prototype.bind().
const doSomething = (d) => {
return `translate(${
(0 - nodeWidth(this)) / 2
},${129.6484} )`;
}
Calling the function inside (enter) and (update)
selection.attr('transform', doSomething.bind(d));
This way the function gets executed in the current scope.
I guess this is what you are looking for. Please be aware that I could not test my code!

Is it possible to make offset/shift in d3.js scales?

I want to make an "overflow" scale that will reset value and continue count from zero if it overflows limit
For example, I want to draw a chart but change the original 0 point place. The first column represents a normal scale, and in the second column I shifted the scale
5 2
4 1
3 0
2 5
1 4
0 3
I think I can alter input data and convert first half of values to negative so the domain will look like this [-2: 3]. But I would prefer not to alter input data values and just have a parameter in scale that can do this for me.
UPD:
I've tried to use more than 2 domain/range values - didn't give me the results that I want, so I wrote a custom interpolation function.
I doubt that only I need something like this and probably d3.js provide something similar. If you know a function like this in standard d3.js distribution please point me to it.
const svg = d3.select("svg").attr('height', 200);
myinterpolate = function(a, b) {
var a = a;
var b = b;
var shift = 100;
var max_value = Math.max(a, b)
var min_value = Math.min(a, b)
return function(t) {
let result = a * (1 - t) + b * t + shift;
if (result < min_value) {
result = result + max_value
} else if (result > max_value) {
result = result % max_value
}
return result
}
;
}
const myScale = d3.scaleLinear().domain([0, 10]).range([200, 0]).interpolate(myinterpolate);
const axis = d3.axisLeft(myScale).tickValues(d3.range(10)).tickFormat(d=>~~d)(svg.append("g").attr("transform", "translate(50,0)"))
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<svg><svg>
You said "I would prefer not to alter input data values and just have a parameter in scale that can do this for me", but that's a very specific and highly unusual use case, so there is none.
What you could easily do is creating a piecewise scale with more than 2 domain/range values:
const svg = d3.select("svg");
const myScale = d3.scaleLinear()
.domain([0, 3, 4, 6])
.range([75, 6, 144, 98]);
const axis = d3.axisLeft(myScale).tickValues(d3.range(7)).tickFormat(d => ~~d)(svg.append("g").attr("transform", "translate(50,0)"))
path {
stroke: none;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<svg></svg>
This simply answers your question as it is. Mind you that it raises several other questions: where are you drawing the values between 3 and 4? What's the meaning of the area between 6 and 0?
A custom interpolation function worked for me.
My code snippet uses a hardcoded value for the shift, but it can be rewritten to use shift value as a parameter.
const svg = d3.select("svg").attr('height', 200);
myinterpolate = function(a, b) {
var a = a;
var b = b;
var shift = 100;
var max_value = Math.max(a, b)
var min_value = Math.min(a, b)
return function(t) {
let result = a * (1 - t) + b * t + shift;
if (result < min_value) {
result = result + max_value
} else if (result > max_value) {
result = result % max_value
}
return result
}
;
}
const myScale = d3.scaleLinear().domain([0, 10]).range([200, 0]).interpolate(myinterpolate);
const axis = d3.axisLeft(myScale).tickValues(d3.range(10)).tickFormat(d=>~~d)(svg.append("g").attr("transform", "translate(50,0)"))
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<svg><svg>

How to add missing values to two associated arrays? (JavaScript)

I have two arrays:
category.data.xAxis // containing: ["0006", "0007", "0009", "0011", "0301"]
category.data.yAxis // containing: [6.31412, 42.4245, 533.2234, 2345.5413, 3215.24]
How do I take a max length, say DATAPOINT_LENGTH_STANDARD = 540 and fill in every missing number-based string on the xAxis array? so that:
The resulting xAxis array will read from "0000" to "0540" (or whatever the standard length is)
The associated yAxis indexes will remain connected to the original xAxis datapoints (i.e. "0006" to 6.31412)
Every newly created xAxis datapoint has an associated yAxis value of 0 (so the newly created "0000" xAxis entry will contain 0 in the yAxis at index 0 for both)
The xAxis value strings can be assumed to already be sorted from lowest to highest.
EDIT
The clarity of my question was an effort to help the community with finding a more straight-forward answer, but I suppose the exactness gave the appearance of a homework question. In response to the comments, my original attempt, which does not properly manipulate the x-axis and maintain proper indexing:
let tempArray = categoryObject.data.xAxis;
let min = Math.min.apply(null, tempArray);
let max = Math.max.apply(null, tempArray);
while (min <= max) {
if (tempArray.indexOf(min.toString()) === -1) {
tempArray.push(min.toString());
categoryObject.data.yAxis.push(0);
}
min++;
}
console.log(tempArray);
console.log(categoryObject.data.yAxis);
let xAxis = ["0006", "0007", "0009", "0011", "0301"]
let yAxis = [6.31412, 42.4245, 533.2234, 2345.5413, 3215.24]
const DATAPOINT_LENGTH_STANDARD = 540
// This assumes at least one data point
let paddedLength = xAxis[0].length
// Creates a new array whose first index is 0, last index is
// DATAPOINT_LENGTH_STANDARD, filled with 0s.
let yAxis_new = new Array(DATAPOINT_LENGTH_STANDARD + 1).fill(0)
// Copy each known data point into the new array at the given index.
// The + before x parses the zero-padded string into an actual number.
xAxis.forEach((x, i) => yAxis_new[+x] = yAxis[i])
// Replace the given array with the new array.
yAxis = yAxis_new
// Store the padded version of the index at each index.
for (let i = 0; i <= DATAPOINT_LENGTH_STANDARD; ++i) {
xAxis[i] = ('' + i).padStart(paddedLength, '0')
}
console.log(xAxis)
console.log(yAxis)

How to place a date in a tooltip when only part of axis ticks with dates are shown? Using D3.js

I have a bar chart with the dates along the x-axis. I define the x-axis this way:
var xAxis = d3.svg.axis()
.scale(xScale)
.orient("bottom")
.ticks(d3.time.months, 1)
.tickSize(0)
.tickFormat(RU.timeFormat("%b %Y"));
After that I pick the dates from the axis texts and put them in tooltips. This way:
.on("mouseover", function(d, i) {
var tickDate = d3.select(d3.selectAll(".axis .tick text")[0][i]).data()[0];
var formatDate = RU.timeFormat("%B %Y");
var tooltipDate = formatDate(tickDate);
And everything is ok with this approach until I'm changing the frequency of the ticks with the dates in this row:
.ticks(d3.time.months, 2)
After that the part of tooltips disappears. How can I fix this mistake?
My JSFiddle: https://jsfiddle.net/anton9ov/cLb1nxwj/
The problem is in mouseover where you try to bind the data with the dates:
var tickDate = d3.select(d3.selectAll(".axis .tick text")[0][i]).data()[0];
and then try to get the correct date
// here d3.selectAll(".axis .tick text")[0][i] fails and returns undefined
// because i is out of range since there are more data than months
var tickDate = d3.select(d3.selectAll(".axis .tick text")[0][i]).data()[0];
var formatDate = RU.timeFormat("%B %Y");
// so formatDate fails when trying to format an empty value
var tooltipDate = formatDate(tickDate);
You could try something like this: https://jsfiddle.net/b03prLm8/5/
Where:
// you get your dates with length 10
var dates = d3.selectAll(".axis .tick text")[0];
// Since it is two values per date - with one exception see below
// check if the current value's modulo with two (the ticks freq.) is zero
// if not, use ceil to get the correct bin/ date the value belongs
// e.g. if i is 9, then 9/2=4.5 becomes 4, so 9 belongs to the 4th date
// and so on...
var index = (i % 2 == 0)
? i/2
: Math.ceil(i/ 2);
if(index > 0) index -= 1; // because the first bar is not within the dates range
// then go on as usual
var tickDate = d3.select(dates[index]).data()[0];
var formatDate = RU.timeFormat("%B %Y");
var tooltipDate = formatDate(tickDate);
Hope this helps. Good luck!

d3 chart - get x axis time of max value

I have a array of values [date, value].
I can find the max value with:
max_value = d3.max(data_array, function(d) { return d[1]; } );
How do I get the d[0] value of d[1] maximum so I can mark the position of maximum value on chart?
Use javascript array filter function.
var max_value = d3.max(data_array, function(d) { return d[1]; } );
var requiredArr = data_array.filter(function(d){
return d[1]==max_value;
});
var dateWithMaxValue = requiredArr[0];
If you don't care about performance too much, here's a one-liner:
// d is [x, y] where x is what you're looking for
var d = data_array.sort(function (a, b) { return b[1] - a[1]; })[0];
Here's a simple demo.

Categories