diff --git a/demos/index.html b/demos/index.html index 9939bcc9..b35dcf1e 100644 --- a/demos/index.html +++ b/demos/index.html @@ -70,7 +70,8 @@

Drawing

Zooming

Different zoom variants (adaptive, uni/omnidirectional) Fetch & update data on zoom - Secondary sync'd overview chart for zoom ranging + Secondary sync'd overview chart for x-axis zoom ranging + Secondary sync'd overview chart for x/y-axis zoom ranging Pinch zooming/panning plugin Mouswheel zooming plugin diff --git a/demos/zoom-ranger-xy.html b/demos/zoom-ranger-xy.html new file mode 100644 index 00000000..fda3b289 --- /dev/null +++ b/demos/zoom-ranger-xy.html @@ -0,0 +1,123 @@ + + + + + Zoom Ranger XY + + + + + + + + + + \ No newline at end of file diff --git a/demos/zoom-ranger.html b/demos/zoom-ranger.html index d61b0b03..c1048c45 100644 --- a/demos/zoom-ranger.html +++ b/demos/zoom-ranger.html @@ -34,13 +34,11 @@ let initXmin = 1; let initXmax = 4.5; - let viaRanger = false; - let viaZoom = false; - const rangerOpts = { width: 800, height: 100, cursor: { + y: false, points: { show: false, }, @@ -49,6 +47,9 @@ x: true, y: false, }, + sync: { + key: "moo" + } }, legend: { show: false @@ -65,22 +66,11 @@ } ], hooks: { - setSelect: [ - uRanger => { - if (!viaZoom) { - viaRanger = true; - let min = uRanger.posToVal(uRanger.select.left, 'x'); - let max = uRanger.posToVal(uRanger.select.left + uRanger.select.width, 'x'); - uZoomed.setScale('x', {min, max}); - viaRanger = false; - } - } - ], ready: [ uRanger => { let left = Math.round(uRanger.valToPos(initXmin, 'x')); let width = Math.round(uRanger.valToPos(initXmax, 'x')) - left; - let height = uRanger.root.querySelector(".over").getBoundingClientRect().height; + let height = uRanger.bbox.height / devicePixelRatio; uRanger.setSelect({left, width, height}, false); } ] @@ -93,6 +83,15 @@ // title: "Zoomed Area", width: 800, height: 400, + cursor: { + drag: { + x: true, + y: false + }, + sync: { + key: "moo" + } + }, scales: { x: { time: false, @@ -108,20 +107,7 @@ label: "sin(x)", stroke: "red", } - ], - hooks: { - setScale: [ - (uZoomed, key) => { - if (key == 'x' && !viaRanger) { - viaZoom = true; - let left = Math.round(uRanger.valToPos(uZoomed.scales.x.min, 'x')); - let right = Math.round(uRanger.valToPos(uZoomed.scales.x.max, 'x')); - uRanger.setSelect({left, width: right - left}); - viaZoom = false; - } - } - ] - } + ] }; let uZoomed = new uPlot(zoomedOpts, data, document.body); diff --git a/dist/uPlot.d.ts b/dist/uPlot.d.ts index e1d98f2a..864f65f6 100644 --- a/dist/uPlot.d.ts +++ b/dist/uPlot.d.ts @@ -115,6 +115,8 @@ declare class uPlot { declare namespace uPlot { export type AlignedData = readonly (number | null)[][]; + export type SyncScales = [string, string]; + export type MinMax = [number, number]; export interface DateNames { @@ -245,6 +247,8 @@ declare namespace uPlot { key: string; /** determines if series toggling and focus via cursor is synced across charts */ setSeries?: boolean; // true + /** sets the x and y scales to sync by values. null will sync by relative (%) position */ + scales?: SyncScales; // [xScaleKey, null] }; /** focus series closest to cursor */ diff --git a/src/uPlot.js b/src/uPlot.js index cbb25e94..ccb22cd5 100644 --- a/src/uPlot.js +++ b/src/uPlot.js @@ -1343,7 +1343,7 @@ export default function uPlot(opts, data, then) { const drag = FEAT_CURSOR && cursor.drag; let dragX = FEAT_CURSOR && drag.x; - let dragY = FEAT_CURSOR && drag.y;; + let dragY = FEAT_CURSOR && drag.y; if (FEAT_CURSOR && cursor.show) { let c = "cursor-"; @@ -1477,8 +1477,14 @@ export default function uPlot(opts, data, then) { } function scaleValueAtPos(pos, scale) { - let dim = scale == xScaleKey ? plotWidCss : plotHgtCss; - let pct = clamp(pos / dim, 0, 1); + let dim = plotWidCss; + if (scale != xScaleKey) { + dim = plotHgtCss; + // invert the pos on the y axis + pos = dim - pos; + } + + let pct = pos / dim; let sc = scales[scale]; let d = sc.max - sc.min; @@ -1492,7 +1498,7 @@ export default function uPlot(opts, data, then) { self.valToIdx = val => closestIdx(val, data[0]); self.posToIdx = closestIdxFromXpos; - self.posToVal = (pos, scale) => scaleValueAtPos(scale == xScaleKey ? pos : plotHgtCss - pos, scale); + self.posToVal = scaleValueAtPos; self.valToPos = (val, scale, can) => ( scale == xScaleKey ? getXPos(val, scales[scale], @@ -1532,7 +1538,7 @@ export default function uPlot(opts, data, then) { let cursorRaf = 0; - function updateCursor(ts) { + function updateCursor(ts, src) { if (inBatch) { shouldUpdateCursor = true; return; @@ -1612,51 +1618,86 @@ export default function uPlot(opts, data, then) { } // nit: cursor.drag.setSelect is assumed always true - if (mouseLeft1 >= 0 && select.show && dragging) { - // setSelect should not be triggered on move events - - dragX = drag.x; - dragY = drag.y; + if (select.show && dragging) { + if (src != null) { + let [xKey, yKey] = syncOpts.scales; - let uni = drag.uni; + if (xKey) { + let sc = scales[xKey]; + let srcLeft = src.posToVal(src.select[LEFT], xKey); + let srcRight = src.posToVal(src.select[LEFT] + src.select[WIDTH], xKey); - if (uni != null) { - let dx = abs(mouseLeft0 - mouseLeft1); - let dy = abs(mouseTop0 - mouseTop1); + select[LEFT] = getXPos(srcLeft, sc, plotWidCss, 0); + select[WIDTH] = abs(select[LEFT] - getXPos(srcRight, sc, plotWidCss, 0)); - dragX = dx >= uni; - dragY = dy >= uni; + setStylePx(selectDiv, LEFT, select[LEFT]); + setStylePx(selectDiv, WIDTH, select[WIDTH]); - // force unidirectionality when both are under uni limit - if (!dragX && !dragY) { - if (dy > dx) - dragY = true; - else - dragX = true; + if (!yKey) { + setStylePx(selectDiv, TOP, select[TOP] = 0); + setStylePx(selectDiv, HEIGHT, select[HEIGHT] = plotHgtCss); + } } - } - if (dragX) { - let minX = min(mouseLeft0, mouseLeft1); - let maxX = max(mouseLeft0, mouseLeft1); - setStylePx(selectDiv, LEFT, select[LEFT] = minX); - setStylePx(selectDiv, WIDTH, select[WIDTH] = maxX - minX); + if (yKey) { + let sc = scales[yKey]; + let srcTop = src.posToVal(src.select[TOP], yKey); + let srcBottom = src.posToVal(src.select[TOP] + src.select[HEIGHT], yKey); - if (uni != null && !dragY) { - setStylePx(selectDiv, TOP, select[TOP] = 0); - setStylePx(selectDiv, HEIGHT, select[HEIGHT] = plotHgtCss); - } - } + select[TOP] = getYPos(srcTop, sc, plotHgtCss, 0); + select[HEIGHT] = abs(select[TOP] - getYPos(srcBottom, sc, plotHgtCss, 0)); + + setStylePx(selectDiv, TOP, select[TOP]); + setStylePx(selectDiv, HEIGHT, select[HEIGHT]); - if (dragY) { - let minY = min(mouseTop0, mouseTop1); - let maxY = max(mouseTop0, mouseTop1); - setStylePx(selectDiv, TOP, select[TOP] = minY); - setStylePx(selectDiv, HEIGHT, select[HEIGHT] = maxY - minY); + if (!xKey) { + setStylePx(selectDiv, LEFT, select[LEFT] = 0); + setStylePx(selectDiv, WIDTH, select[WIDTH] = plotWidCss); + } + } - if (uni != null && !dragX) { - setStylePx(selectDiv, LEFT, select[LEFT] = 0); - setStylePx(selectDiv, WIDTH, select[WIDTH] = plotWidCss); + } else { + // setSelect should not be triggered on move events + let uni = drag.uni; + + if (uni != null) { + let dx = abs(mouseLeft0 - mouseLeft1); + let dy = abs(mouseTop0 - mouseTop1); + + dragX = dx >= uni; + dragY = dy >= uni; + + // force unidirectionality when both are under uni limit + if (!dragX && !dragY) { + if (dy > dx) + dragY = true; + else + dragX = true; + } + } + + if (dragX) { + let minX = min(mouseLeft0, mouseLeft1); + let maxX = max(mouseLeft0, mouseLeft1); + setStylePx(selectDiv, LEFT, select[LEFT] = minX); + setStylePx(selectDiv, WIDTH, select[WIDTH] = maxX - minX); + + if (!dragY) { + setStylePx(selectDiv, TOP, select[TOP] = 0); + setStylePx(selectDiv, HEIGHT, select[HEIGHT] = plotHgtCss); + } + } + + if (dragY) { + let minY = min(mouseTop0, mouseTop1); + let maxY = max(mouseTop0, mouseTop1); + setStylePx(selectDiv, TOP, select[TOP] = minY); + setStylePx(selectDiv, HEIGHT, select[HEIGHT] = maxY - minY); + + if (!dragX) { + setStylePx(selectDiv, LEFT, select[LEFT] = 0); + setStylePx(selectDiv, WIDTH, select[WIDTH] = plotWidCss); + } } } } @@ -1707,17 +1748,32 @@ export default function uPlot(opts, data, then) { cursorRaf = rAF(updateCursor); } else - updateCursor(); + updateCursor(null, src); } function cacheMouse(e, src, _x, _y, _w, _h, _i, initial, snap) { + if (_x < 0 || _y < 0) { + mouseLeft1 = -10; + mouseTop1 = -10; + return; + } + if (e != null) { _x = e.clientX - rect.left; _y = e.clientY - rect.top; } else { - _x = plotWidCss * (_x/_w); - _y = plotHgtCss * (_y/_h); + let [xKey, yKey] = syncOpts.scales; + + if (xKey != null) + _x = getXPos(src.posToVal(_x, xKey), scales[xKey], plotWidCss, 0); + else + _x = plotWidCss * (_x/_w); + + if (yKey != null) + _y = getYPos(src.posToVal(_y, yKey), scales[yKey], plotHgtCss, 0); + else + _y = plotHgtCss * (_y/_h); } if (snap) { @@ -1740,19 +1796,16 @@ export default function uPlot(opts, data, then) { function hideSelect() { setSelect({ - width: !drag.x ? plotWidCss : 0, - height: !drag.y ? plotHgtCss : 0, + width: 0, + height: 0, }, false); } function mouseDown(e, src, _x, _y, _w, _h, _i) { - if (e == null || filtMouse(e)) { + if (src != null || filtMouse(e)) { dragging = true; - cacheMouse(e, src, _x, _y, _w, _h, _i, true, true); - - if (select.show && (drag.x || drag.y)) - hideSelect(); + cacheMouse(e, src, _x, _y, _w, _h, _i, true, false); if (e != null) { on(mouseup, doc, mouseUp); @@ -1762,51 +1815,54 @@ export default function uPlot(opts, data, then) { } function mouseUp(e, src, _x, _y, _w, _h, _i) { - if ((e == null || filtMouse(e))) { + if (src != null || filtMouse(e)) { dragging = false; cacheMouse(e, src, _x, _y, _w, _h, _i, false, true); + setSelect(select); - if (mouseLeft1 != mouseLeft0 || mouseTop1 != mouseTop0) { - setSelect(select); + if (drag.setScale && (select[WIDTH] || select[HEIGHT])) { - if (drag.setScale) { - batch(() => { - if (dragX) { - _setScale(xScaleKey, - scaleValueAtPos(select[LEFT], xScaleKey), - scaleValueAtPos(select[LEFT] + select[WIDTH], xScaleKey), - ); - } + if (syncKey != null) { + dragX = drag.x; + dragY = drag.y; + } + + batch(() => { + if (dragX) { + _setScale(xScaleKey, + scaleValueAtPos(select[LEFT], xScaleKey), + scaleValueAtPos(select[LEFT] + select[WIDTH], xScaleKey) + ); + } - if (dragY) { - for (let k in scales) { - let sc = scales[k]; + if (dragY) { + for (let k in scales) { + let sc = scales[k]; - if (k != xScaleKey && sc.from == null) { - _setScale(k, - scaleValueAtPos(plotHgtCss - select[TOP] - select[HEIGHT], k), - scaleValueAtPos(plotHgtCss - select[TOP], k), - ); - } + if (k != xScaleKey && sc.from == null) { + _setScale(k, + scaleValueAtPos(select[TOP] + select[HEIGHT], k), + scaleValueAtPos(select[TOP], k) + ); } } - }); + } + }); - hideSelect(); - } + hideSelect(); } else if (cursor.lock) { - cursor.locked = !cursor.locked + cursor.locked = !cursor.locked; if (!cursor.locked) updateCursor(); } + } - if (e != null) { - off(mouseup, doc, mouseUp); - sync.pub(mouseup, self, mouseLeft1, mouseTop1, plotWidCss, plotHgtCss, null); - } + if (e != null) { + off(mouseup, doc, mouseUp); + sync.pub(mouseup, self, mouseLeft1, mouseTop1, plotWidCss, plotHgtCss, null); } } @@ -1821,6 +1877,18 @@ export default function uPlot(opts, data, then) { function dblClick(e, src, _x, _y, _w, _h, _i) { autoScaleX(); + + if (src != null && select.show && (drag.x || drag.y)) { + if (drag.setScale) + hideSelect(); + else + setSelect({ + [LEFT]: 0, + [WIDTH]: plotWidCss, + [TOP]: 0, + [HEIGHT]: plotHgtCss + }); + } if (e != null) sync.pub(dblclick, self, mouseLeft1, mouseTop1, plotWidCss, plotHgtCss, null); @@ -1873,6 +1941,7 @@ export default function uPlot(opts, data, then) { const syncOpts = FEAT_CURSOR && assign({ key: null, setSeries: false, + scales: [xScaleKey, null] }, cursor.sync); const syncKey = FEAT_CURSOR && syncOpts.key; @@ -1936,4 +2005,4 @@ uPlot.rangeNum = rangeNum; if (FEAT_TIME) { uPlot.fmtDate = fmtDate; uPlot.tzDate = tzDate; -} \ No newline at end of file +}