Skip to content

Commit

Permalink
expose an advanced scale.range & uPlot.rangeNum() config format, with…
Browse files Browse the repository at this point in the history
… softMin/softMax support. close #328.
  • Loading branch information
leeoniya committed Nov 2, 2020
1 parent d866b53 commit f6f034b
Show file tree
Hide file tree
Showing 9 changed files with 447 additions and 133 deletions.
142 changes: 142 additions & 0 deletions demos/soft-minmax.html
@@ -0,0 +1,142 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>Soft Min/Max</title>
<meta name="viewport" content="width=device-width, initial-scale=1">

<link rel="stylesheet" href="../dist/uPlot.min.css">
<script src="../dist/uPlot.iife.min.js"></script>
<style>
.uplot {
display: inline-block;
width: min-content;
vertical-align: top;
}

p {
margin: 0;
padding: 10px;
}

button {
position: absolute;
left: 100px;
top: 100px;
}
</style>
</head>
<body>
<script>
function h1(text) {
let el = document.createElement("h1");
el.textContent = text;
document.body.appendChild(el);
};

const opts = {
width: 400,
height: 400,
scales: {
x: {time: false}
},
series: [
{},
{
stroke: "blue",
fill: "rgba(0,0,255,0.1)",
},
],
};

// h1("");

let data = [
[0,10],
[5,12],
];

let plots = [
{
title: "min: {soft: 0, mode: 0}",
descr: "With min.soft: null or min.mode: 0, the scaleMin will always be a constant % [of the full range] below dataMin.",
scales: {
y: {
range: {
min: {
pad: 0.2,
soft: 0,
mode: 0,
},
max: {
pad: 0.2,
soft: 0,
mode: 2,
},
}
},
},
},
{
title: "min: {soft: 0, mode: 1}",
descr: "With min.mode: 1, the scaleMin will be min.soft unless dataMin goes below it. This is probably how most would expect a softMin setting to behave.",
scales: {
y: {
range: {
min: {
pad: 0.2,
soft: 0,
mode: 1,
},
max: {
pad: 0.2,
soft: 0,
mode: 2,
},
}
},
},
},
{
title: "min: {soft: 0, mode: 2}",
descr: "With min.mode: 2, the scaleMin will be a constant % [of the full range] below dataMin until (dataMin - pad) goes below it. This is uPlot's default mode - it provides a conditioned softMin - keeping more vertical resolution when the value range is small and far from softMin.",
scales: {
y: {
range: {
min: {
pad: 0.2,
soft: 0,
mode: 2,
},
max: {
pad: 0.2,
soft: 0,
mode: 2,
},
}
},
},
},
];

plots = plots.map(o => {
let u = uPlot(uPlot.assign(opts, o), data, document.body);
let p = document.createElement("p");
p.textContent = o.descr;
u.root.appendChild(p);
return u;
});

function dataMaxIncr() {
setInterval(() => {
// data[1][0] -= 0.1;
data[1][1] += .1;
plots.forEach(u => {
u.setData(data);
});
}, 50);
}
</script>
<button onclick="dataMaxIncr()">▶ dataMax++</button>
</body>
</html>
102 changes: 70 additions & 32 deletions dist/uPlot.cjs.js
Expand Up @@ -111,40 +111,70 @@ function rangeLog(min, max, base, fullMags) {
return [min, max];
}

var _eqRangePart = {
pad: 0,
soft: null,
mode: 0,
};

var _eqRange = {
min: _eqRangePart,
max: _eqRangePart,
};

// this ensures that non-temporal/numeric y-axes get multiple-snapped padding added above/below
// TODO: also account for incrs when snapping to ensure top of axis gets a tick & value
function rangeNum(min, max, mult, extra) {
var delta = max - min;
var nonZeroDelta = delta || abs(max) || 1e3;
var mag = log10(nonZeroDelta);
var base = pow(10, floor(mag));

var padding = nonZeroDelta * (delta == 0 ? (min == 0 ? .1 : 1) : mult);
var newMin = min - padding;
var newMax = max + padding;

var snappedMin = roundDec(incrRoundDn(newMin, base/100), 6);
var snappedMax = roundDec(incrRoundUp(newMax, base/100), 6);

if (extra) {
// for flat data, always use 0 as one chart extreme & place data in center
if (delta == 0) {
if (max > 0)
{ snappedMin = 0; }
else if (max < 0)
{ snappedMax = 0; }
}
else {
// if original data never crosses 0, use 0 as one chart extreme
if (min >= 0 && snappedMin < 0)
{ snappedMin = 0; }
function rangeNum(_min, _max, mult, extra) {
if (isObj(mult))
{ return _rangeNum(_min, _max, mult); }

if (max <= 0 && snappedMax > 0)
{ snappedMax = 0; }
}
}
_eqRangePart.pad = mult;
_eqRangePart.soft = extra ? 0 : null;
_eqRangePart.mode = extra ? 2 : 0;

return _rangeNum(_min, _max, _eqRange);
}

// nullish coalesce
function ifNull(lh, rh) {
return lh == null ? rh : lh;
}

return [snappedMin, snappedMax];
function _rangeNum(_min, _max, cfg) {
var cmin = cfg.min;
var cmax = cfg.max;

var padMin = ifNull(cmin.pad, 0);
var padMax = ifNull(cmax.pad, 0);

var hardMin = ifNull(cmin.hard, -inf);
var hardMax = ifNull(cmax.hard, inf);

var softMin = ifNull(cmin.soft, inf);
var softMax = ifNull(cmax.soft, -inf);

var softMinMode = ifNull(cmin.mode, 0);
var softMaxMode = ifNull(cmax.mode, 0);

var delta = _max - _min;
var nonZeroDelta = delta || abs(_max) || 1e3;
var mag = log10(nonZeroDelta);
var base = pow(10, floor(mag));

var _padMin = nonZeroDelta * (delta == 0 ? (_min == 0 ? .1 : 1) : padMin);
var _newMin = roundDec(incrRoundDn(_min - _padMin, base/100), 6);
var _softMin = _min >= softMin && (softMinMode == 1 || softMinMode == 2 && _newMin < softMin) ? softMin : inf;
var minLim = max(hardMin, _newMin < _softMin && _min >= _softMin ? _softMin : min(_softMin, _newMin));

var _padMax = nonZeroDelta * (delta == 0 ? (_max == 0 ? .1 : 1) : padMax);
var _newMax = roundDec(incrRoundUp(_max + _padMax, base/100), 6);
var _softMax = _max <= softMax && (softMaxMode == 1 || softMaxMode == 2 && _newMax > softMax) ? softMax : -inf;
var maxLim = min(hardMax, _newMax > _softMax && _max <= _softMax ? _softMax : max(_softMax, _newMax));

if (minLim == maxLim && minLim == 0)
{ maxLim = 100; }

return [minLim, maxLim];
}

// alternative: https://stackoverflow.com/a/2254896
Expand Down Expand Up @@ -905,7 +935,7 @@ function numAxisSplits(self, axisIdx, scaleMin, scaleMax, foundIncr, foundSpace,
scaleMin = forceMin ? scaleMin : roundDec(incrRoundUp(scaleMin, foundIncr), numDec);

for (var val = scaleMin; val <= scaleMax; val = roundDec(val + foundIncr, numDec))
{ splits.push(val); }
{ splits.push(Object.is(val, -0) ? 0 : val); } // coalesces -0

return splits;
}
Expand Down Expand Up @@ -1196,7 +1226,15 @@ function uPlot(opts, data, then) {
var isTime = sc.time;
var isLog = sc.distr == 3;

sc.range = fnOrSelf(sc.range || (isTime ? snapTimeX : scaleKey == xScaleKey ? (isLog ? snapLogX : snapNumX) : (isLog ? snapLogY : snapNumY)));
var rn = sc.range;

if (scaleKey != xScaleKey && !isArr(rn) && isObj(rn)) {
var cfg = rn;
// this is similar to snapNumY
rn = function (self, dataMin, dataMax) { return dataMin == null ? nullMinMax : rangeNum(dataMin, dataMax, cfg); };
}

sc.range = fnOrSelf(rn || (isTime ? snapTimeX : scaleKey == xScaleKey ? (isLog ? snapLogX : snapNumX) : (isLog ? snapLogY : snapNumY)));
}
}
}
Expand Down
25 changes: 22 additions & 3 deletions dist/uPlot.d.ts
Expand Up @@ -106,7 +106,7 @@ declare class uPlot {
static assign(targ: object, ...srcs: object[]): object;

/** re-ranges a given min/max by a multiple of the range's magnitude (used internally to expand/snap/pad numeric y scales) */
static rangeNum(min: number, max: number, mult: number, extra: boolean): uPlot.MinMax;
static rangeNum: ((min: number, max: number, mult: number, extra: boolean) => uPlot.MinMax) | ((min: number, max: number, cfg: uPlot.RangeConfig) => uPlot.MinMax);

/** re-ranges a given min/max outwards to nearest 10% of given min/max's magnitudes, unless fullMags = true */
static rangeLog(min: number, max: number, fullMags: boolean): uPlot.MinMax;
Expand All @@ -124,6 +124,8 @@ declare class uPlot {
declare namespace uPlot {
export type AlignedData = readonly (number | null)[][];

// export type ScatteredData = readonly number[][][];

export type SyncScales = [string, string];

export type MinMax = [number, number];
Expand All @@ -144,7 +146,24 @@ declare namespace uPlot {
WWW: string[];
}

// export type ScatteredData = readonly number[][][];
interface RangeConfigPart {
/** initial multiplier for dataMax-dataMin delta */
pad?: number; // 0.1

/** soft limit */
soft?: number; // 0

/** soft mode - 0: off, 1: if data extreme falls within soft limit, 2: if data extreme & padding exceeds soft limit */
mode?: 0 | 1 | 2; // 2

/** hard limit */
hard?: number;
}

export interface RangeConfig {
min: RangeConfigPart;
max: RangeConfigPart;
}

export interface Options {
/** chart title */
Expand Down Expand Up @@ -327,7 +346,7 @@ declare namespace uPlot {
auto?: boolean;

/** can define a static scale range or re-range an initially-determined range from series data */
range?: MinMax | ((self: uPlot, initMin: number, initMax: number, scaleKey: string) => MinMax);
range?: MinMax | RangeConfig | ((self: uPlot, initMin: number, initMax: number, scaleKey: string) => MinMax);

/** scale key from which this scale is derived */
from?: string,
Expand Down

0 comments on commit f6f034b

Please sign in to comment.