mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Chore: Restore some removed UI components to reduce BC surface area (#85546)
This commit is contained in:
parent
9c0f9f6ba4
commit
130d561924
@ -311,3 +311,6 @@ export { TimeSeries } from '../graveyard/TimeSeries/TimeSeries';
|
|||||||
export { useGraphNGContext } from '../graveyard/GraphNG/hooks';
|
export { useGraphNGContext } from '../graveyard/GraphNG/hooks';
|
||||||
export { preparePlotFrame, buildScaleKey } from '../graveyard/GraphNG/utils';
|
export { preparePlotFrame, buildScaleKey } from '../graveyard/GraphNG/utils';
|
||||||
export { type GraphNGLegendEvent } from '../graveyard/GraphNG/types';
|
export { type GraphNGLegendEvent } from '../graveyard/GraphNG/types';
|
||||||
|
|
||||||
|
export { ZoomPlugin } from '../graveyard/uPlot/plugins/ZoomPlugin';
|
||||||
|
export { TooltipPlugin } from '../graveyard/uPlot/plugins/TooltipPlugin';
|
||||||
|
@ -74,6 +74,13 @@ exports[`GraphNG utils preparePlotConfigBuilder 1`] = `
|
|||||||
"stroke": [Function],
|
"stroke": [Function],
|
||||||
"width": [Function],
|
"width": [Function],
|
||||||
},
|
},
|
||||||
|
"sync": {
|
||||||
|
"key": "__global_",
|
||||||
|
"scales": [
|
||||||
|
"x",
|
||||||
|
null,
|
||||||
|
],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
"focus": {
|
"focus": {
|
||||||
"alpha": 1,
|
"alpha": 1,
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import {
|
import {
|
||||||
createTheme,
|
createTheme,
|
||||||
|
DashboardCursorSync,
|
||||||
DataFrame,
|
DataFrame,
|
||||||
DefaultTimeZone,
|
DefaultTimeZone,
|
||||||
FieldColorModeId,
|
FieldColorModeId,
|
||||||
@ -213,6 +214,7 @@ describe('GraphNG utils', () => {
|
|||||||
theme: createTheme(),
|
theme: createTheme(),
|
||||||
timeZones: [DefaultTimeZone],
|
timeZones: [DefaultTimeZone],
|
||||||
getTimeRange: getDefaultTimeRange,
|
getTimeRange: getDefaultTimeRange,
|
||||||
|
sync: () => DashboardCursorSync.Tooltip,
|
||||||
allFrames: [frame!],
|
allFrames: [frame!],
|
||||||
}).getConfig();
|
}).getConfig();
|
||||||
expect(result).toMatchSnapshot();
|
expect(result).toMatchSnapshot();
|
||||||
|
@ -19,6 +19,7 @@ export class UnthemedTimeSeries extends Component<TimeSeriesProps> {
|
|||||||
declare context: React.ContextType<typeof PanelContextRoot>;
|
declare context: React.ContextType<typeof PanelContextRoot>;
|
||||||
|
|
||||||
prepConfig = (alignedFrame: DataFrame, allFrames: DataFrame[], getTimeRange: () => TimeRange) => {
|
prepConfig = (alignedFrame: DataFrame, allFrames: DataFrame[], getTimeRange: () => TimeRange) => {
|
||||||
|
const { sync } = this.context;
|
||||||
const { theme, timeZone, renderers, tweakAxis, tweakScale } = this.props;
|
const { theme, timeZone, renderers, tweakAxis, tweakScale } = this.props;
|
||||||
|
|
||||||
return preparePlotConfigBuilder({
|
return preparePlotConfigBuilder({
|
||||||
@ -26,6 +27,7 @@ export class UnthemedTimeSeries extends Component<TimeSeriesProps> {
|
|||||||
theme,
|
theme,
|
||||||
timeZones: Array.isArray(timeZone) ? timeZone : [timeZone],
|
timeZones: Array.isArray(timeZone) ? timeZone : [timeZone],
|
||||||
getTimeRange,
|
getTimeRange,
|
||||||
|
sync,
|
||||||
allFrames,
|
allFrames,
|
||||||
renderers,
|
renderers,
|
||||||
tweakScale,
|
tweakScale,
|
||||||
|
@ -2,6 +2,7 @@ import { isNumber } from 'lodash';
|
|||||||
import uPlot from 'uplot';
|
import uPlot from 'uplot';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
DashboardCursorSync,
|
||||||
DataFrame,
|
DataFrame,
|
||||||
FieldConfig,
|
FieldConfig,
|
||||||
FieldType,
|
FieldType,
|
||||||
@ -70,16 +71,21 @@ const defaultConfig: GraphFieldConfig = {
|
|||||||
axisPlacement: AxisPlacement.Auto,
|
axisPlacement: AxisPlacement.Auto,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const preparePlotConfigBuilder: UPlotConfigPrepFn = ({
|
export const preparePlotConfigBuilder: UPlotConfigPrepFn<{
|
||||||
|
sync?: () => DashboardCursorSync;
|
||||||
|
}> = ({
|
||||||
frame,
|
frame,
|
||||||
theme,
|
theme,
|
||||||
timeZones,
|
timeZones,
|
||||||
getTimeRange,
|
getTimeRange,
|
||||||
|
sync,
|
||||||
allFrames,
|
allFrames,
|
||||||
renderers,
|
renderers,
|
||||||
tweakScale = (opts) => opts,
|
tweakScale = (opts) => opts,
|
||||||
tweakAxis = (opts) => opts,
|
tweakAxis = (opts) => opts,
|
||||||
}) => {
|
}) => {
|
||||||
|
const eventsScope = '__global_';
|
||||||
|
|
||||||
const builder = new UPlotConfigBuilder(timeZones[0]);
|
const builder = new UPlotConfigBuilder(timeZones[0]);
|
||||||
|
|
||||||
let alignedFrame: DataFrame;
|
let alignedFrame: DataFrame;
|
||||||
@ -98,6 +104,7 @@ export const preparePlotConfigBuilder: UPlotConfigPrepFn = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const xScaleKey = 'x';
|
const xScaleKey = 'x';
|
||||||
|
let yScaleKey = '';
|
||||||
|
|
||||||
const xFieldAxisPlacement =
|
const xFieldAxisPlacement =
|
||||||
xField.config.custom?.axisPlacement !== AxisPlacement.Hidden ? AxisPlacement.Bottom : AxisPlacement.Hidden;
|
xField.config.custom?.axisPlacement !== AxisPlacement.Hidden ? AxisPlacement.Bottom : AxisPlacement.Hidden;
|
||||||
@ -258,6 +265,10 @@ export const preparePlotConfigBuilder: UPlotConfigPrepFn = ({
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (!yScaleKey) {
|
||||||
|
yScaleKey = scaleKey;
|
||||||
|
}
|
||||||
|
|
||||||
if (customConfig.axisPlacement !== AxisPlacement.Hidden) {
|
if (customConfig.axisPlacement !== AxisPlacement.Hidden) {
|
||||||
let axisColor: uPlot.Axis.Stroke | undefined;
|
let axisColor: uPlot.Axis.Stroke | undefined;
|
||||||
|
|
||||||
@ -533,6 +544,8 @@ export const preparePlotConfigBuilder: UPlotConfigPrepFn = ({
|
|||||||
r.init(builder, fieldIndices);
|
r.init(builder, fieldIndices);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
builder.scaleKeys = [xScaleKey, yScaleKey];
|
||||||
|
|
||||||
// if hovered value is null, how far we may scan left/right to hover nearest non-null
|
// if hovered value is null, how far we may scan left/right to hover nearest non-null
|
||||||
const hoverProximityPx = 15;
|
const hoverProximityPx = 15;
|
||||||
|
|
||||||
@ -585,12 +598,19 @@ export const preparePlotConfigBuilder: UPlotConfigPrepFn = ({
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (sync && sync() !== DashboardCursorSync.Off) {
|
||||||
|
cursor.sync = {
|
||||||
|
key: eventsScope,
|
||||||
|
scales: [xScaleKey, null],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
builder.setCursor(cursor);
|
builder.setCursor(cursor);
|
||||||
|
|
||||||
return builder;
|
return builder;
|
||||||
};
|
};
|
||||||
|
|
||||||
function getNamesToFieldIndex(frame: DataFrame, allFrames: DataFrame[]): Map<string, number> {
|
export function getNamesToFieldIndex(frame: DataFrame, allFrames: DataFrame[]): Map<string, number> {
|
||||||
const originNames = new Map<string, number>();
|
const originNames = new Map<string, number>();
|
||||||
frame.fields.forEach((field, i) => {
|
frame.fields.forEach((field, i) => {
|
||||||
const origin = field.state?.origin;
|
const origin = field.state?.origin;
|
||||||
|
@ -0,0 +1,297 @@
|
|||||||
|
import { css } from '@emotion/css';
|
||||||
|
import React, { useLayoutEffect, useRef, useState } from 'react';
|
||||||
|
import { useMountedState } from 'react-use';
|
||||||
|
import uPlot from 'uplot';
|
||||||
|
|
||||||
|
import {
|
||||||
|
arrayUtils,
|
||||||
|
CartesianCoords2D,
|
||||||
|
DashboardCursorSync,
|
||||||
|
DataFrame,
|
||||||
|
FALLBACK_COLOR,
|
||||||
|
FieldType,
|
||||||
|
formattedValueToString,
|
||||||
|
getDisplayProcessor,
|
||||||
|
getFieldDisplayName,
|
||||||
|
GrafanaTheme2,
|
||||||
|
TimeZone,
|
||||||
|
} from '@grafana/data';
|
||||||
|
import { TooltipDisplayMode, SortOrder } from '@grafana/schema';
|
||||||
|
|
||||||
|
import { Portal, SeriesTable, SeriesTableRowProps, UPlotConfigBuilder, VizTooltipContainer } from '../../../components';
|
||||||
|
import { findMidPointYPosition } from '../../../components/uPlot/utils';
|
||||||
|
import { useStyles2, useTheme2 } from '../../../themes/ThemeContext';
|
||||||
|
|
||||||
|
interface TooltipPluginProps {
|
||||||
|
timeZone: TimeZone;
|
||||||
|
data: DataFrame;
|
||||||
|
frames?: DataFrame[];
|
||||||
|
config: UPlotConfigBuilder;
|
||||||
|
mode?: TooltipDisplayMode;
|
||||||
|
sortOrder?: SortOrder;
|
||||||
|
sync?: () => DashboardCursorSync;
|
||||||
|
// Allows custom tooltip content rendering. Exposes aligned data frame with relevant indexes for data inspection
|
||||||
|
// Use field.state.origin indexes from alignedData frame field to get access to original data frame and field index.
|
||||||
|
renderTooltip?: (alignedFrame: DataFrame, seriesIdx: number | null, datapointIdx: number | null) => React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
const TOOLTIP_OFFSET = 10;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @alpha
|
||||||
|
*/
|
||||||
|
export const TooltipPlugin = ({
|
||||||
|
mode = TooltipDisplayMode.Single,
|
||||||
|
sortOrder = SortOrder.None,
|
||||||
|
sync,
|
||||||
|
timeZone,
|
||||||
|
config,
|
||||||
|
renderTooltip,
|
||||||
|
...otherProps
|
||||||
|
}: TooltipPluginProps) => {
|
||||||
|
const plotInstance = useRef<uPlot>();
|
||||||
|
const theme = useTheme2();
|
||||||
|
const [focusedSeriesIdx, setFocusedSeriesIdx] = useState<number | null>(null);
|
||||||
|
const [focusedPointIdx, setFocusedPointIdx] = useState<number | null>(null);
|
||||||
|
const [focusedPointIdxs, setFocusedPointIdxs] = useState<Array<number | null>>([]);
|
||||||
|
const [coords, setCoords] = useState<CartesianCoords2D | null>(null);
|
||||||
|
const [isActive, setIsActive] = useState<boolean>(false);
|
||||||
|
const isMounted = useMountedState();
|
||||||
|
let parentWithFocus: HTMLElement | null = null;
|
||||||
|
|
||||||
|
const style = useStyles2(getStyles);
|
||||||
|
|
||||||
|
// Add uPlot hooks to the config, or re-add when the config changed
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
let bbox: DOMRect | undefined = undefined;
|
||||||
|
|
||||||
|
const plotEnter = () => {
|
||||||
|
if (!isMounted()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setIsActive(true);
|
||||||
|
plotInstance.current?.root.classList.add('plot-active');
|
||||||
|
};
|
||||||
|
|
||||||
|
const plotLeave = () => {
|
||||||
|
if (!isMounted()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setCoords(null);
|
||||||
|
setIsActive(false);
|
||||||
|
plotInstance.current?.root.classList.remove('plot-active');
|
||||||
|
};
|
||||||
|
|
||||||
|
// cache uPlot plotting area bounding box
|
||||||
|
config.addHook('syncRect', (u, rect) => (bbox = rect));
|
||||||
|
|
||||||
|
config.addHook('init', (u) => {
|
||||||
|
plotInstance.current = u;
|
||||||
|
|
||||||
|
u.over.addEventListener('mouseenter', plotEnter);
|
||||||
|
u.over.addEventListener('mouseleave', plotLeave);
|
||||||
|
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
parentWithFocus = u.root.closest('[tabindex]');
|
||||||
|
|
||||||
|
if (parentWithFocus) {
|
||||||
|
parentWithFocus.addEventListener('focus', plotEnter);
|
||||||
|
parentWithFocus.addEventListener('blur', plotLeave);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sync && sync() === DashboardCursorSync.Crosshair) {
|
||||||
|
u.root.classList.add('shared-crosshair');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
config.addHook('setLegend', (u) => {
|
||||||
|
if (!isMounted()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setFocusedPointIdx(u.legend.idx!);
|
||||||
|
setFocusedPointIdxs(u.legend.idxs!.slice());
|
||||||
|
});
|
||||||
|
|
||||||
|
// default series/datapoint idx retireval
|
||||||
|
config.addHook('setCursor', (u) => {
|
||||||
|
if (!bbox || !isMounted()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { x, y } = positionTooltip(u, bbox);
|
||||||
|
if (x !== undefined && y !== undefined) {
|
||||||
|
setCoords({ x, y });
|
||||||
|
} else {
|
||||||
|
setCoords(null);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
config.addHook('setSeries', (_, idx) => {
|
||||||
|
if (!isMounted()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setFocusedSeriesIdx(idx);
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
setCoords(null);
|
||||||
|
|
||||||
|
if (plotInstance.current) {
|
||||||
|
plotInstance.current.over.removeEventListener('mouseleave', plotLeave);
|
||||||
|
plotInstance.current.over.removeEventListener('mouseenter', plotEnter);
|
||||||
|
|
||||||
|
if (parentWithFocus) {
|
||||||
|
parentWithFocus.removeEventListener('focus', plotEnter);
|
||||||
|
parentWithFocus.removeEventListener('blur', plotLeave);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [config, setCoords, setIsActive, setFocusedPointIdx, setFocusedPointIdxs]);
|
||||||
|
|
||||||
|
if (focusedPointIdx === null || (!isActive && sync && sync() === DashboardCursorSync.Crosshair)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// GraphNG expects aligned data, let's take field 0 as x field. FTW
|
||||||
|
let xField = otherProps.data.fields[0];
|
||||||
|
if (!xField) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const xFieldFmt = xField.display || getDisplayProcessor({ field: xField, timeZone, theme });
|
||||||
|
let tooltip: React.ReactNode = null;
|
||||||
|
|
||||||
|
let xVal = xFieldFmt(xField!.values[focusedPointIdx]).text;
|
||||||
|
|
||||||
|
if (!renderTooltip) {
|
||||||
|
// when interacting with a point in single mode
|
||||||
|
if (mode === TooltipDisplayMode.Single && focusedSeriesIdx !== null) {
|
||||||
|
const field = otherProps.data.fields[focusedSeriesIdx];
|
||||||
|
|
||||||
|
if (!field) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const dataIdx = focusedPointIdxs?.[focusedSeriesIdx] ?? focusedPointIdx;
|
||||||
|
xVal = xFieldFmt(xField!.values[dataIdx]).text;
|
||||||
|
const fieldFmt = field.display || getDisplayProcessor({ field, timeZone, theme });
|
||||||
|
const display = fieldFmt(field.values[dataIdx]);
|
||||||
|
|
||||||
|
tooltip = (
|
||||||
|
<SeriesTable
|
||||||
|
series={[
|
||||||
|
{
|
||||||
|
color: display.color || FALLBACK_COLOR,
|
||||||
|
label: getFieldDisplayName(field, otherProps.data, otherProps.frames),
|
||||||
|
value: display ? formattedValueToString(display) : null,
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
timestamp={xVal}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mode === TooltipDisplayMode.Multi) {
|
||||||
|
let series: SeriesTableRowProps[] = [];
|
||||||
|
const frame = otherProps.data;
|
||||||
|
const fields = frame.fields;
|
||||||
|
const sortIdx: unknown[] = [];
|
||||||
|
|
||||||
|
for (let i = 0; i < fields.length; i++) {
|
||||||
|
const field = frame.fields[i];
|
||||||
|
if (
|
||||||
|
!field ||
|
||||||
|
field === xField ||
|
||||||
|
field.type === FieldType.time ||
|
||||||
|
field.type !== FieldType.number ||
|
||||||
|
field.config.custom?.hideFrom?.tooltip ||
|
||||||
|
field.config.custom?.hideFrom?.viz
|
||||||
|
) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const v = otherProps.data.fields[i].values[focusedPointIdxs[i]!];
|
||||||
|
const display = field.display!(v);
|
||||||
|
|
||||||
|
sortIdx.push(v);
|
||||||
|
series.push({
|
||||||
|
color: display.color || FALLBACK_COLOR,
|
||||||
|
label: getFieldDisplayName(field, frame, otherProps.frames),
|
||||||
|
value: display ? formattedValueToString(display) : null,
|
||||||
|
isActive: focusedSeriesIdx === i,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sortOrder !== SortOrder.None) {
|
||||||
|
// create sort reference series array, as Array.sort() mutates the original array
|
||||||
|
const sortRef = [...series];
|
||||||
|
const sortFn = arrayUtils.sortValues(sortOrder);
|
||||||
|
|
||||||
|
series.sort((a, b) => {
|
||||||
|
// get compared values indices to retrieve raw values from sortIdx
|
||||||
|
const aIdx = sortRef.indexOf(a);
|
||||||
|
const bIdx = sortRef.indexOf(b);
|
||||||
|
return sortFn(sortIdx[aIdx], sortIdx[bIdx]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
tooltip = <SeriesTable series={series} timestamp={xVal} />;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
tooltip = renderTooltip(otherProps.data, focusedSeriesIdx, focusedPointIdx);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Portal className={isActive ? style.tooltipWrapper : undefined}>
|
||||||
|
{tooltip && coords && (
|
||||||
|
<VizTooltipContainer position={{ x: coords.x, y: coords.y }} offset={{ x: TOOLTIP_OFFSET, y: TOOLTIP_OFFSET }}>
|
||||||
|
{tooltip}
|
||||||
|
</VizTooltipContainer>
|
||||||
|
)}
|
||||||
|
</Portal>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
function isCursorOutsideCanvas({ left, top }: uPlot.Cursor, canvas: DOMRect) {
|
||||||
|
if (left === undefined || top === undefined) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return left < 0 || left > canvas.width || top < 0 || top > canvas.height;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Given uPlot cursor position, figure out position of the tooltip withing the canvas bbox
|
||||||
|
* Tooltip is positioned relatively to a viewport
|
||||||
|
* @internal
|
||||||
|
**/
|
||||||
|
export function positionTooltip(u: uPlot, bbox: DOMRect) {
|
||||||
|
let x, y;
|
||||||
|
const cL = u.cursor.left || 0;
|
||||||
|
const cT = u.cursor.top || 0;
|
||||||
|
|
||||||
|
if (isCursorOutsideCanvas(u.cursor, bbox)) {
|
||||||
|
const idx = u.posToIdx(cL);
|
||||||
|
// when cursor outside of uPlot's canvas
|
||||||
|
if (cT < 0 || cT > bbox.height) {
|
||||||
|
let pos = findMidPointYPosition(u, idx);
|
||||||
|
|
||||||
|
if (pos) {
|
||||||
|
y = bbox.top + pos;
|
||||||
|
if (cL >= 0 && cL <= bbox.width) {
|
||||||
|
// find x-scale position for a current cursor left position
|
||||||
|
x = bbox.left + u.valToPos(u.data[0][u.posToIdx(cL)], u.series[0].scale!);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
x = bbox.left + cL;
|
||||||
|
y = bbox.top + cT;
|
||||||
|
}
|
||||||
|
|
||||||
|
return { x, y };
|
||||||
|
}
|
||||||
|
|
||||||
|
const getStyles = (theme: GrafanaTheme2) => ({
|
||||||
|
tooltipWrapper: css({
|
||||||
|
'z-index': theme.zIndex.portal + 1 + ' !important',
|
||||||
|
}),
|
||||||
|
});
|
127
packages/grafana-ui/src/graveyard/uPlot/plugins/ZoomPlugin.tsx
Normal file
127
packages/grafana-ui/src/graveyard/uPlot/plugins/ZoomPlugin.tsx
Normal file
@ -0,0 +1,127 @@
|
|||||||
|
import { useLayoutEffect } from 'react';
|
||||||
|
|
||||||
|
import { UPlotConfigBuilder } from '../../../components';
|
||||||
|
|
||||||
|
interface ZoomPluginProps {
|
||||||
|
onZoom: (range: { from: number; to: number }) => void;
|
||||||
|
withZoomY?: boolean;
|
||||||
|
config: UPlotConfigBuilder;
|
||||||
|
}
|
||||||
|
|
||||||
|
// min px width that triggers zoom
|
||||||
|
const MIN_ZOOM_DIST = 5;
|
||||||
|
|
||||||
|
const maybeZoomAction = (e?: MouseEvent | null) => e != null && !e.ctrlKey && !e.metaKey;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @alpha
|
||||||
|
*/
|
||||||
|
export const ZoomPlugin = ({ onZoom, config, withZoomY = false }: ZoomPluginProps) => {
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
let yZoomed = false;
|
||||||
|
let yDrag = false;
|
||||||
|
|
||||||
|
if (withZoomY) {
|
||||||
|
config.addHook('init', (u) => {
|
||||||
|
u.over!.addEventListener(
|
||||||
|
'mousedown',
|
||||||
|
(e) => {
|
||||||
|
if (!maybeZoomAction(e)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (e.button === 0 && e.shiftKey) {
|
||||||
|
yDrag = true;
|
||||||
|
|
||||||
|
u.cursor!.drag!.x = false;
|
||||||
|
u.cursor!.drag!.y = true;
|
||||||
|
|
||||||
|
let onUp = (e: MouseEvent) => {
|
||||||
|
u.cursor!.drag!.x = true;
|
||||||
|
u.cursor!.drag!.y = false;
|
||||||
|
document.removeEventListener('mouseup', onUp, true);
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener('mouseup', onUp, true);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
true
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
config.addHook('setSelect', (u) => {
|
||||||
|
const isXAxisHorizontal = u.scales.x.ori === 0;
|
||||||
|
if (maybeZoomAction(u.cursor!.event)) {
|
||||||
|
if (withZoomY && yDrag) {
|
||||||
|
if (u.select.height >= MIN_ZOOM_DIST) {
|
||||||
|
for (let key in u.scales!) {
|
||||||
|
if (key !== 'x') {
|
||||||
|
const maxY = isXAxisHorizontal
|
||||||
|
? u.posToVal(u.select.top, key)
|
||||||
|
: u.posToVal(u.select.left + u.select.width, key);
|
||||||
|
const minY = isXAxisHorizontal
|
||||||
|
? u.posToVal(u.select.top + u.select.height, key)
|
||||||
|
: u.posToVal(u.select.left, key);
|
||||||
|
u.setScale(key, { min: minY, max: maxY });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
yZoomed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
yDrag = false;
|
||||||
|
} else {
|
||||||
|
if (u.select.width >= MIN_ZOOM_DIST) {
|
||||||
|
const minX = isXAxisHorizontal
|
||||||
|
? u.posToVal(u.select.left, 'x')
|
||||||
|
: u.posToVal(u.select.top + u.select.height, 'x');
|
||||||
|
const maxX = isXAxisHorizontal
|
||||||
|
? u.posToVal(u.select.left + u.select.width, 'x')
|
||||||
|
: u.posToVal(u.select.top, 'x');
|
||||||
|
|
||||||
|
onZoom({ from: minX, to: maxX });
|
||||||
|
|
||||||
|
yZoomed = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// manually hide selected region (since cursor.drag.setScale = false)
|
||||||
|
u.setSelect({ left: 0, width: 0, top: 0, height: 0 }, false);
|
||||||
|
});
|
||||||
|
|
||||||
|
config.setCursor({
|
||||||
|
bind: {
|
||||||
|
dblclick: (u) => () => {
|
||||||
|
if (!maybeZoomAction(u.cursor!.event)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (withZoomY && yZoomed) {
|
||||||
|
for (let key in u.scales!) {
|
||||||
|
if (key !== 'x') {
|
||||||
|
// @ts-ignore (this is not typed correctly in uPlot, assigning nulls means auto-scale / reset)
|
||||||
|
u.setScale(key, { min: null, max: null });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
yZoomed = false;
|
||||||
|
} else {
|
||||||
|
let xScale = u.scales.x;
|
||||||
|
|
||||||
|
const frTs = xScale.min!;
|
||||||
|
const toTs = xScale.max!;
|
||||||
|
const pad = (toTs - frTs) / 2;
|
||||||
|
|
||||||
|
onZoom({ from: frTs - pad, to: toTs + pad });
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}, [config, onZoom, withZoomY]);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
Loading…
Reference in New Issue
Block a user