-
Notifications
You must be signed in to change notification settings - Fork 121
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: auto interval approximation #1294
Conversation
|
||
for (let i = 0; i < approximatedOptions.length; i++) { | ||
const { unit, min = 1, max = 10 } = approximatedOptions[i]; | ||
const unitInterval = moment.duration(1, unit).asMilliseconds(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
can we just add the millisecond resolution in the approximatedOptions
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not sure what you are suggesting here. Do you want me to remove the milliseconds
option altogether?
const valuesLength = xValues.length; | ||
if (valuesLength <= 0) { | ||
return 0; | ||
} | ||
if (valuesLength === 1) { | ||
return 1; | ||
return null; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Instead of returning null, can we just call guessMinInterval(seriesXComputedDomains, fallbackScale ?? type);
here?
return null; | |
return guessMinInterval(seriesXComputedDomains, fallbackScale ?? type); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I thought about that but then I would need to add two more arguments to this function. Which would you prefer?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
fwiw I tend to trade argument list brevity for return type simplicity so I wouldn't mind having two extra args, although there may be some better solution. Infinity
would make it number
consistently—see the minDelta
suggestion below—, and then it needs a Number.isFinite
check, or some other way of handling the infinity
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Putting aside the question around null
the function is basically a reduction:
export function findMinInterval(xValues: number[]): number | null {
const sortedValues = xValues.slice().sort(compareByValueAsc);
return xValues.length === 0
? 0
: xValues.length === 1
? null
: sortedValues.reduce(
(p, n, i, a) => Math.min(p, (i < a.length - 1 ? a[i + 1] : Infinity) - n),
sortedValues[1] - sortedValues[0],
);
}
Somehow it feels asymmetrical that for a zero length array, we return zero, and for a 1-length array, it's null, then from 2, it's a number again
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh my point about this being a reduction: probably it belongs in some basic array processing util, not x_domain.ts
, so
export function findMinInterval(xValues: number[]): number | null {
const sortedValues = xValues.slice().sort(compareByValueAsc);
return xValues.length === 0
? 0
: xValues.length === 1
? null
: minDelta(sortedValues)
}
Or minDelta
does the sorting too:
const minDelta = (values: number, predicate: Sorter): number =>
values
.slice()
.sort(predicate)
.reduce((p, n, i, a) => Math.min(p, (i < a.length - 1 ? a[i + 1] : Infinity) - n), Infinity);
It naturally yields Infinity
if there element count is less than 2, so maybe that could be used instead of null
@@ -127,18 +133,77 @@ function getMinInterval(computedMinInterval: number, size: number, customMinInte | |||
return customMinInterval; | |||
} | |||
|
|||
const approximatedOptions: { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would there be a more telling name for anything in the code change with approx* in it, as what is going on here doesn't look like approximation, more like, inferred min interval? Yeah any min interval is inferred that's not expressly given, so if it's ambiguous, it could be more explicit, like singleton min interval or some such, easy to grep.
The preexisting minInterval
itself is already a bit of a misnomer, esp. for the case of a singleton bar, when there's no interval by definition. I see such alternatives in dataviz land as
- pitch, temporal pitch
- cadence
- bin width, temporal bin width
So it might be binWidthFromSinglePoint
or binCountForSinglePoint
even
{ unit: 'second', max: 60 }, | ||
{ unit: 'millisecond', max: 1000 }, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It means that if the specified time domain is such that there'd be 60 seconds in the domain, then it'll accept minute
? So the entire chart width becomes one giant bar.
What drives the numbers?
Asking it as we already have problems with enormous bar widths that no dataviz designer would manually consider. I realize it's an edge case, but a minute-bar shown on a 61-second domain will be almost full Cartesian area width, or if there's some nicing of the time axis(?) 1/3rd to half of the Cartesian width.
Also, shouldn't the whole thing depend on the screenspace Cartesian width ie. pixels, or estimated bar width pixels or bar pitch pixels? It feels like showing a minute-bar in a 61-second domain is great if the chart is very narrow, but not so great if it spans a dashboard horizontally
Logger.warn( | ||
`The minInterval, given only a single data point, has been approximated as ${ | ||
scaleType === ScaleType.Time ? moment.duration(value).humanize() : value | ||
}. This is not ideal as this data point likely does not coincide with the proper interval. Please set the minInterval in the xDomain.`, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: the linter complains of line length but it's a template string, what do we want to generally do in cases like this?
const valuesLength = xValues.length; | ||
if (valuesLength <= 0) { | ||
return 0; | ||
} | ||
if (valuesLength === 1) { | ||
return 1; | ||
return null; | ||
} | ||
const sortedValues = xValues.slice().sort(compareByValueAsc); | ||
let i; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Might as well put the let
inside the for
* @internal | ||
*/ | ||
export function findMinInterval(xValues: number[]): number { | ||
export function findMinInterval(xValues: number[]): number | null { | ||
const valuesLength = xValues.length; | ||
if (valuesLength <= 0) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Preexisting: [].length can't be less than 0 (I'm OK with a <=
convention though if we want to save on bytes)
const valuesLength = xValues.length; | ||
if (valuesLength <= 0) { | ||
return 0; | ||
} | ||
if (valuesLength === 1) { | ||
return 1; | ||
return null; | ||
} | ||
const sortedValues = xValues.slice().sort(compareByValueAsc); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since we sort, there's no need for the Math.abs
in const interval = Math.abs(next - current);
min?: number; | ||
max?: number; | ||
}[] = [ | ||
{ unit: 'year' }, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
{ unit: 'year' , max: Infinity}
const unitInterval = moment.duration(1, unit).asMilliseconds(); | ||
const unitDiff = Math.floor(value / unitInterval); | ||
|
||
if ((unitDiff > min && unitDiff <= max) || (unit === 'year' && unitDiff > max)) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
... then no need for the extra || (unit === 'year' && unitDiff > max)
check
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Minor: it's also fine, and maybe more intuitive here to permit the min value too, and using a math-like ordering min <= unitDiff <= max
min <= unitDiff && unitDiff <= max
Closing in favor of different approach, that being to require |
Summary
The PR adds auto approximation of the
minInterval
when not directly inferable. This includes both time and non-time based data.Details
This PR adds a simplified approximation of the
minInterval
for single point datasets when amin
andmax
xDomain
is provided without aminInterval
. Currently, we default the interval in such cases to1
. This is problematic as this often prohibits rendering the bar given its infinitesimal width.The solution in this case is to determine the best unit interval that would reasonably display the single data point. This is done by looping through different time units of decreasing scale to determine the best time unit for the provided domain extents. Some examples include:
A development warning is surfaced in the console to promote adding the actual
minInterval
based on the data.Issues
fixes #840, fixes #1184
Checklist