-
Notifications
You must be signed in to change notification settings - Fork 11.9k
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
Fix log scale calculations #6903
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,20 +1,20 @@ | ||
'use strict'; | ||
|
||
import defaults from '../core/core.defaults'; | ||
import helpers from '../helpers/index'; | ||
import {_setMinAndMaxByKey} from '../helpers/helpers.math'; | ||
import {isFinite} from '../helpers/helpers.core'; | ||
import {_setMinAndMaxByKey, log10} from '../helpers/helpers.math'; | ||
import Scale from '../core/core.scale'; | ||
import LinearScaleBase from './scale.linearbase'; | ||
import Ticks from '../core/core.ticks'; | ||
|
||
const valueOrDefault = helpers.valueOrDefault; | ||
const log10 = helpers.math.log10; | ||
|
||
function isMajor(tickVal) { | ||
const remain = tickVal / (Math.pow(10, Math.floor(log10(tickVal)))); | ||
return remain === 1; | ||
} | ||
|
||
function finiteOrDefault(value, def) { | ||
return isFinite(value) ? value : def; | ||
} | ||
|
||
/** | ||
* Generate a set of logarithmic ticks | ||
* @param generationOptions the options used to generate the ticks | ||
|
@@ -25,16 +25,9 @@ function generateTicks(generationOptions, dataRange) { | |
const endExp = Math.floor(log10(dataRange.max)); | ||
const endSignificand = Math.ceil(dataRange.max / Math.pow(10, endExp)); | ||
const ticks = []; | ||
let tickVal = valueOrDefault(generationOptions.min, Math.pow(10, Math.floor(log10(dataRange.min)))); | ||
let exp, significand; | ||
|
||
if (tickVal === 0) { | ||
exp = 0; | ||
significand = 0; | ||
} else { | ||
exp = Math.floor(log10(tickVal)); | ||
significand = Math.floor(tickVal / Math.pow(10, exp)); | ||
} | ||
let tickVal = finiteOrDefault(generationOptions.min, Math.pow(10, Math.floor(log10(dataRange.min)))); | ||
let exp = Math.floor(log10(tickVal)); | ||
let significand = Math.floor(tickVal / Math.pow(10, exp)); | ||
let precision = exp < 0 ? Math.pow(10, Math.abs(exp)) : 1; | ||
|
||
do { | ||
|
@@ -50,7 +43,7 @@ function generateTicks(generationOptions, dataRange) { | |
tickVal = Math.round(significand * Math.pow(10, exp) * precision) / precision; | ||
} while (exp < endExp || (exp === endExp && significand < endSignificand)); | ||
|
||
const lastTick = valueOrDefault(generationOptions.max, tickVal); | ||
const lastTick = finiteOrDefault(generationOptions.max, tickVal); | ||
ticks.push({value: lastTick, major: isMajor(tickVal)}); | ||
|
||
return ticks; | ||
|
@@ -69,19 +62,20 @@ const defaultConfig = { | |
class LogarithmicScale extends Scale { | ||
_parse(raw, index) { // eslint-disable-line no-unused-vars | ||
const value = LinearScaleBase.prototype._parse.apply(this, arguments); | ||
return helpers.isFinite(value) && value >= 0 ? value : undefined; | ||
if (value === 0) { | ||
return undefined; | ||
} | ||
return isFinite(value) && value > 0 ? value : NaN; | ||
} | ||
|
||
determineDataLimits() { | ||
const me = this; | ||
const minmax = me._getMinMax(true); | ||
const min = minmax.min; | ||
const max = minmax.max; | ||
const minPositive = minmax.minPositive; | ||
|
||
me.min = helpers.isFinite(min) ? Math.max(0, min) : null; | ||
me.max = helpers.isFinite(max) ? Math.max(0, max) : null; | ||
me.minNotZero = helpers.isFinite(minPositive) ? minPositive : null; | ||
me.min = isFinite(min) ? Math.max(0, min) : null; | ||
me.max = isFinite(max) ? Math.max(0, max) : null; | ||
|
||
me.handleTickRangeOptions(); | ||
} | ||
|
@@ -94,30 +88,19 @@ class LogarithmicScale extends Scale { | |
let max = me.max; | ||
|
||
if (min === max) { | ||
if (min !== 0 && min !== null) { | ||
min = Math.pow(10, Math.floor(log10(min)) - 1); | ||
max = Math.pow(10, Math.floor(log10(max)) + 1); | ||
} else { | ||
if (min <= 0) { // includes null | ||
kurkle marked this conversation as resolved.
Show resolved
Hide resolved
|
||
min = DEFAULT_MIN; | ||
max = DEFAULT_MAX; | ||
} else { | ||
min = Math.pow(10, Math.floor(log10(min)) - 1); | ||
max = Math.pow(10, Math.floor(log10(max)) + 1); | ||
} | ||
} | ||
if (min === null) { | ||
if (min <= 0) { | ||
min = Math.pow(10, Math.floor(log10(max)) - 1); | ||
} | ||
if (max === null) { | ||
max = min !== 0 | ||
? Math.pow(10, Math.floor(log10(min)) + 1) | ||
: DEFAULT_MAX; | ||
} | ||
if (me.minNotZero === null) { | ||
if (min > 0) { | ||
me.minNotZero = min; | ||
} else if (max < 1) { | ||
me.minNotZero = Math.pow(10, Math.floor(log10(max))); | ||
} else { | ||
me.minNotZero = DEFAULT_MIN; | ||
} | ||
if (max <= 0) { | ||
max = Math.pow(10, Math.floor(log10(min)) + 1); | ||
} | ||
me.min = min; | ||
me.max = max; | ||
|
@@ -152,6 +135,10 @@ class LogarithmicScale extends Scale { | |
return ticks; | ||
} | ||
|
||
getLabelForValue(value) { | ||
benmccann marked this conversation as resolved.
Show resolved
Hide resolved
|
||
return value === undefined ? 0 : value; | ||
} | ||
|
||
getPixelForTick(index) { | ||
const ticks = this.ticks; | ||
if (index < 0 || index > ticks.length - 1) { | ||
|
@@ -160,52 +147,30 @@ class LogarithmicScale extends Scale { | |
return this.getPixelForValue(ticks[index].value); | ||
} | ||
|
||
/** | ||
* Returns the value of the first tick. | ||
* @param {number} value - The minimum not zero value. | ||
* @return {number} The first tick value. | ||
* @private | ||
*/ | ||
_getFirstTickValue(value) { | ||
const exp = Math.floor(log10(value)); | ||
const significand = Math.floor(value / Math.pow(10, exp)); | ||
|
||
return significand * Math.pow(10, exp); | ||
} | ||
|
||
_configure() { | ||
const me = this; | ||
let start = me.min; | ||
let offset = 0; | ||
|
||
Scale.prototype._configure.call(me); | ||
|
||
if (start === 0) { | ||
start = me._getFirstTickValue(me.minNotZero); | ||
offset = valueOrDefault(me.options.ticks.fontSize, defaults.fontSize) / me._length; | ||
} | ||
|
||
me._startValue = log10(start); | ||
me._valueOffset = offset; | ||
me._valueRange = (log10(me.max) - log10(start)) / (1 - offset); | ||
me._valueRange = log10(me.max) - log10(start); | ||
} | ||
|
||
getPixelForValue(value) { | ||
const me = this; | ||
let decimal = 0; | ||
|
||
if (value > me.min && value > 0) { | ||
decimal = (log10(value) - me._startValue) / me._valueRange + me._valueOffset; | ||
if (value === undefined || value === 0) { | ||
value = me.min; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. can we return There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Returning min value of scale for 0 (and parsed 0 that will be undefined), so those values are drawn. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why use the scale min instead of There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Lets make one thing clear, do you think we should continue to draw zero on a log chart or not? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I would say we should not There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There are few fixed issues about that. #4913 fixed some for v2.7.1 If its not clear already, this is not something I want to spend my time with. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ok. I'm not sure how this will work in all cases, but if we need to change it later I suppose we can |
||
} | ||
return me.getPixelForDecimal(decimal); | ||
return me.getPixelForDecimal(value === me.min | ||
? 0 | ||
: (log10(value) - me._startValue) / me._valueRange); | ||
} | ||
|
||
getValueForPixel(pixel) { | ||
const me = this; | ||
const decimal = me.getDecimalForPixel(pixel); | ||
return decimal === 0 && me.min === 0 | ||
? 0 | ||
: Math.pow(10, me._startValue + (decimal - me._valueOffset) * me._valueRange); | ||
return Math.pow(10, me._startValue + decimal * me._valueRange); | ||
} | ||
} | ||
|
||
|
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 think I prefer calculating
minPositive
in_getMinMax
rather than handling0
specially here. (Actually probably what would be nicest is overriding_getMinMax
in the log scale, but the_getMinMax
logic has gotten so complicated that's a bit harder).E.g. what if someone has a toggle between linear and log scales and wants to set
parsing: false
? I think the more we can keep scales out of the parsing business the better.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.
Data with only positive values (and 0) works fine without parsing, if its in the correct format. One needs to set min value to scale manually then, but that would be expected when parsing=false, wouldn't it?
I have a follow-up to make
beginAtZero
work nicely on log scale, but I'm getting bored with this.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.
In my mind
parsing
is just that. It doesn't include validation or anything else. But I don't necessarily want to spend more time on this, so if you and @etimberg are both okay with this then so am I