未验证 提交 b676286f 编写于 作者: P Peter Pan 提交者: GitHub

feat: add chart tooltip in histogram overlay (#691)

* test: add test case

* feat: add chart tooltip in histogram overlay

* feat: add tooltip in histogram chart
上级 c6d4cdd1
......@@ -24,5 +24,8 @@ module.exports = {
.map(p => `tsc -p ${p} --noEmit`),
// lint changed files
'**/*.(j|t)s?(x)': filenames => `eslint ${filenames.join(' ')}`
'**/*.(j|t)s?(x)': filenames => [
`eslint ${filenames.join(' ')}`,
`yarn test --silent --bail --findRelatedTests ${filenames.join(' ')}`
]
};
......@@ -26,7 +26,7 @@
"dev": "cross-env NODE_ENV=development ts-node index.ts",
"build": "tsc",
"start": "node index.js",
"test": "echo \"Error: no test specified\" && exit 0"
"test": "echo \"Error: no test specified\" && exit 0; #"
},
"bin": "dist/index.js",
"types": "dist/index.d.ts",
......
import Page404 from '../pages/404';
import React from 'react';
import {shallow} from 'enzyme';
describe('Page 404', () => {
test('page with content `404 - errors:page-not-found`', () => {
const page = shallow(<Page404 namespacesRequired={['errors']} />);
expect(page.text()).toEqual('404 - errors:page-not-found');
});
});
import Icon from '../components/Icon';
import React from 'react';
import {shallow} from 'enzyme';
describe('Icon component', () => {
const click = jest.fn();
const icon = shallow(<Icon type="close" onClick={click} />);
test('icon has one empty `i` element', () => {
expect(icon.is('i')).toBe(true);
expect(icon.children().length).toBe(0);
});
test('icon has class name `vdl-icon`', () => {
expect(icon.hasClass('vdl-icon')).toBe(true);
});
test('icon with type has class name `icon-${type}`', () => {
expect(icon.hasClass('icon-close')).toBe(true);
});
test('icon click', () => {
icon.simulate('click');
expect(click.mock.calls.length).toBe(1);
});
});
{
"extends": "../tsconfig.test.json"
}
import {HistogramData, Modes, OffsetData, OverlayData, options as chartOptions, transform} from '~/resource/histogram';
import {EChartOption, ECharts, EChartsConvertFinder} from 'echarts';
import {
HistogramData,
Modes,
OffsetData,
OverlayData,
OverlayDataItem,
options as chartOptions,
transform
} from '~/resource/histogram';
import LineChart, {LineChartRef} from '~/components/LineChart';
import React, {FunctionComponent, useCallback, useMemo, useRef, useState} from 'react';
import StackChart, {StackChartRef} from '~/components/StackChart';
import React, {FunctionComponent, useCallback, useEffect, useMemo, useRef, useState} from 'react';
import StackChart, {StackChartProps, StackChartRef} from '~/components/StackChart';
import {rem, size} from '~/utils/style';
import ChartToolbox from '~/components/ChartToolbox';
import {EChartOption} from 'echarts';
import {Run} from '~/types';
import {distance} from '~/utils';
import ee from '~/utils/event';
import {fetcher} from '~/utils/fetch';
import {format} from 'd3-format';
import minBy from 'lodash/minBy';
import queryString from 'query-string';
import styled from 'styled-components';
import useHeavyWork from '~/hooks/useHeavyWork';
import {useRunningRequest} from '~/hooks/useRequest';
import useThrottleFn from '~/hooks/useThrottleFn';
import {useTranslation} from '~/utils/i18n';
const formatTooltipXValue = format('.4f');
const formatTooltipYValue = format('.4');
const transformWasm = () =>
import('@visualdl/wasm').then(({histogram_transform}): typeof transform => params =>
histogram_transform(params.data, params.mode)
......@@ -64,7 +80,11 @@ const HistogramChart: FunctionComponent<HistogramChartProps> = ({cid, run, tag,
const {data: dataset, error, loading} = useRunningRequest<HistogramData>(
`/histogram/list?${queryString.stringify({run: run.label, tag})}`,
!!running
!!running,
fetcher,
{
refreshInterval: 60 * 1000
}
);
const [maximized, setMaximized] = useState<boolean>(false);
......@@ -84,12 +104,19 @@ const HistogramChart: FunctionComponent<HistogramChartProps> = ({cid, run, tag,
);
const data = useHeavyWork(transformWasm, transformWorker, transform, params);
const [highlight, setHighlight] = useState<number | null>(null);
useEffect(() => setHighlight(null), [mode]);
const chartData = useMemo(() => {
type Optional<T> = T | undefined;
if (mode === Modes.Overlay) {
return (data as Optional<OverlayData>)?.data.map(items => ({
name: `step${items[0][1]}`,
return (data as Optional<OverlayData>)?.data.map((items, index) => ({
name: `${t('common:time-mode.step')}${items[0][1]}`,
data: items,
lineStyle: {
color: run.colors[index === highlight || highlight == null ? 0 : 1]
},
z: index === highlight ? data?.data.length : index,
encode: {
x: [2],
y: [3]
......@@ -97,18 +124,99 @@ const HistogramChart: FunctionComponent<HistogramChartProps> = ({cid, run, tag,
}));
}
if (mode === Modes.Offset) {
const offset = data as Optional<OffsetData>;
return {
...((data as Optional<OffsetData>) ?? {})
minX: offset?.minX,
maxX: offset?.maxX,
minY: offset?.minStep,
maxY: offset?.maxStep,
minZ: offset?.minZ,
maxZ: offset?.maxZ,
data: offset?.data ?? []
};
}
}, [data, mode]);
return null as never;
}, [data, mode, run, highlight, t]);
const formatter = {
[Modes.Overlay]: useCallback(
(params: EChartOption.Tooltip.Format | EChartOption.Tooltip.Format[]) => {
if (!data || highlight == null) {
return '';
}
const series = (params as EChartOption.Tooltip.Format[]).filter(
s => s.data[1] === (data as OverlayData).data[highlight][0][1]
);
return [
series[0].seriesName,
...series.map(s => `${formatTooltipXValue(s.data[2])}, ${formatTooltipYValue(s.data[3])}`)
].join('<br />');
},
[highlight, data]
),
[Modes.Offset]: useCallback(
(step: number, dots: [number, number, number][]) =>
[
`${t('common:time-mode.step')}${step}`,
...dots
.filter(d => d[1] === step)
.map(d => `${formatTooltipXValue(d[0])}, ${formatTooltipYValue(d[2])}`)
].join('<br />'),
[t]
)
} as const;
const options = useMemo(
() => ({
...chartOptions[mode],
color: [run.colors[0]]
color: [run.colors[0]],
tooltip: {
formatter: formatter[mode]
}
}),
[mode, run]
[mode, run, formatter]
);
const mousemove = useCallback((echarts: ECharts, {offsetX, offsetY}: {offsetX: number; offsetY: number}) => {
const series = echarts.getOption().series;
const pt: [number, number] = [offsetX, offsetY];
if (series) {
type Distance = number;
type Index = number;
const npts: [number, number, Distance, Index][] = series.map((s, i) =>
(s.data as OverlayDataItem[])?.reduce(
(m, [, , x, y]) => {
const px = echarts.convertToPixel('grid' as EChartsConvertFinder, [x, y]) as [number, number];
const d = distance(px, pt);
if (d < m[2]) {
return [x, y, d, i];
}
return m;
},
[0, 0, Number.POSITIVE_INFINITY, i]
)
);
const npt = minBy(npts, p => p[2]);
setHighlight(npt?.[3] ?? null);
return;
}
}, []);
const throttled = useThrottleFn(mousemove, {wait: 200});
const onInit = useCallback(
(echarts: ECharts) => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const zr = (echarts as any).getZr();
if (zr) {
zr.on('mousemove', (e: {offsetX: number; offsetY: number}) => throttled.run(echarts, e));
zr.on('mouseout', () => {
throttled.cancel();
setHighlight(null);
});
}
},
[throttled]
);
const chart = useMemo(() => {
......@@ -118,8 +226,9 @@ const HistogramChart: FunctionComponent<HistogramChartProps> = ({cid, run, tag,
ref={echart as React.RefObject<LineChartRef>}
title={title}
data={chartData as EChartOption<EChartOption.SeriesLine>['series']}
options={options}
options={options as EChartOption}
loading={loading}
onInit={onInit}
/>
);
}
......@@ -128,14 +237,14 @@ const HistogramChart: FunctionComponent<HistogramChartProps> = ({cid, run, tag,
<StyledStackChart
ref={echart as React.RefObject<StackChartRef>}
title={title}
data={chartData as EChartOption<EChartOption.SeriesCustom>['series'] & OffsetData}
options={options}
data={chartData as StackChartProps['data']}
options={options as EChartOption}
loading={loading}
/>
);
}
return null;
}, [chartData, loading, mode, options, title]);
}, [chartData, loading, mode, options, title, onInit]);
// display error only on first fetch
if (!data && error) {
......
......@@ -8,7 +8,7 @@ type IconProps = {
};
const Icon: FunctionComponent<IconProps & WithStyled> = ({type, onClick, className}) => {
return <i className={`vdl-icon icon-${type} ${className ?? ''}`} onClick={() => onClick?.()} />;
return <i className={`vdl-icon icon-${type} ${className ?? ''}`} onClick={() => onClick?.()}></i>;
};
export default Icon;
......@@ -2,7 +2,7 @@ import * as chart from '~/utils/chart';
import React, {useEffect, useImperativeHandle} from 'react';
import {WithStyled, primaryColor} from '~/utils/style';
import useECharts, {Wrapper} from '~/hooks/useECharts';
import useECharts, {Options, Wrapper} from '~/hooks/useECharts';
import {EChartOption} from 'echarts';
import GridLoader from 'react-spinners/GridLoader';
......@@ -16,6 +16,7 @@ type LineChartProps = {
data?: Partial<NonNullable<EChartOption<EChartOption.SeriesLine>['series']>>;
loading?: boolean;
zoom?: boolean;
onInit?: Options['onInit'];
};
export enum XAxisType {
......@@ -35,13 +36,14 @@ export type LineChartRef = {
};
const LineChart = React.forwardRef<LineChartRef, LineChartProps & WithStyled>(
({options, data, title, loading, zoom, className}, ref) => {
({options, data, title, loading, zoom, className, onInit}, ref) => {
const {i18n} = useTranslation();
const {ref: echartRef, echart, wrapper, saveAsImage} = useECharts<HTMLDivElement>({
loading: !!loading,
zoom,
autoFit: true
autoFit: true,
onInit
});
useImperativeHandle(ref, () => ({
......
import * as chart from '~/utils/chart';
import React, {useCallback, useEffect, useImperativeHandle} from 'react';
import {EChartOption, ECharts, EChartsConvertFinder} from 'echarts';
import React, {useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState} from 'react';
import {WithStyled, primaryColor} from '~/utils/style';
import useECharts, {Wrapper} from '~/hooks/useECharts';
import useECharts, {Options, Wrapper} from '~/hooks/useECharts';
import {EChartOption} from 'echarts';
import GridLoader from 'react-spinners/GridLoader';
import defaultsDeep from 'lodash/defaultsDeep';
import {useTranslation} from '~/utils/i18n';
import styled from 'styled-components';
import useThrottleFn from '~/hooks/useThrottleFn';
const Tooltip = styled.div`
position: absolute;
z-index: 1;
background-color: rgba(0, 0, 0, 0.75);
color: #fff;
border-radius: 4px;
padding: 5px;
display: none;
`;
type renderItem = NonNullable<EChartOption.SeriesCustom['renderItem']>;
type renderItemArguments = NonNullable<renderItem['arguments']>;
......@@ -19,19 +30,21 @@ type RenderItem = (
type GetValue = (i: number) => number;
type GetCoord = (p: [number, number]) => [number, number];
type StackChartProps = {
export type StackChartProps = {
options?: EChartOption;
title?: string;
data?: Partial<NonNullable<EChartOption<EChartOption.SeriesCustom>['series']>[number]> & {
data?: Partial<Omit<NonNullable<EChartOption<EChartOption.SeriesCustom>['series']>[number], 'data'>> & {
minZ: number;
maxZ: number;
minX: number;
maxX: number;
minStep: number;
maxStep: number;
minY: number;
maxY: number;
data: number[][];
};
loading?: boolean;
zoom?: boolean;
onInit?: Options['onInit'];
};
export type StackChartRef = {
......@@ -39,117 +52,357 @@ export type StackChartRef = {
};
const StackChart = React.forwardRef<StackChartRef, StackChartProps & WithStyled>(
({options, data, title, loading, zoom, className}, ref) => {
const {i18n} = useTranslation();
const {ref: echartRef, echart, wrapper, saveAsImage} = useECharts<HTMLDivElement>({
loading: !!loading,
zoom,
autoFit: true
});
useImperativeHandle(ref, () => ({
saveAsImage: () => {
saveAsImage(title);
}
}));
const {minZ, maxZ, minStep, maxStep, minX, maxX, ...seriesData} = data ?? {
({options, data, title, loading, zoom, className, onInit}, ref) => {
const {minZ, maxZ, minY, maxY, minX, maxX, ...seriesData} = data ?? {
minZ: 0,
maxZ: 0,
minStep: 0,
maxStep: 0,
minY: 0,
maxY: 0,
minX: 0,
maxX: 0,
data: null
};
const rawData = (seriesData.data as number[][]) ?? [];
const rawData = seriesData.data ?? [];
const negativeY = useMemo(() => minY - (maxY - minY) * 0.4, [minY, maxY]);
const getPoint = useCallback(
(x: number, y: number, z: number, getCoord: GetCoord, yValueMapHeight: number) => {
(x: number, y: number, z: number, getCoord: GetCoord) => {
const pt = getCoord([x, y]);
// bug of echarts
if (!pt) {
return [0, 0];
}
// linear map in z axis
pt[1] -= ((z - minZ) / (maxZ - minZ)) * yValueMapHeight;
pt[1] -= ((z - minZ) / (maxZ - minZ)) * (getCoord([0, minY])[1] - getCoord([0, negativeY])[1]);
return pt;
},
[minZ, maxZ]
[minZ, maxZ, minY, negativeY]
);
const makePolyPoints = useCallback(
(dataIndex: number, getValue: GetValue, getCoord: GetCoord, yValueMapHeight: number) => {
(dataIndex: number, getValue: GetValue, getCoord: GetCoord) => {
const points = [];
let i = 0;
while (rawData[dataIndex] && i < rawData[dataIndex].length) {
const x = getValue(i++);
const y = getValue(i++);
const z = getValue(i++);
points.push(getPoint(x, y, z, getCoord, yValueMapHeight));
if (z !== 1 && i === 3) {
points.push(getPoint(x, y, 1, getCoord));
}
points.push(getPoint(x, y, z, getCoord));
if (z !== 1 && i === rawData[dataIndex].length) {
points.push(getPoint(x, y, 1, getCoord));
}
}
return points;
},
[getPoint, rawData]
);
const renderItem = useCallback<RenderItem>(
(params, api) => {
const points = makePolyPoints(params.dataIndex as number, api.value as GetValue, api.coord as GetCoord);
return {
type: 'polygon',
silent: true,
z: api.value(1),
shape: {
points
},
style: api.style({
stroke: chart.xAxis.axisLine.lineStyle.color,
lineWidth: 1
})
};
},
[makePolyPoints]
);
const chartOptions = useMemo<EChartOption>(() => {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const {color, colorAlt, toolbox, series, ...defaults} = chart;
return defaultsDeep(
{
title: {
text: title ?? ''
},
visualMap: {
min: minY,
max: maxY
},
xAxis: {
min: minX,
max: maxX
},
yAxis: {
inverse: true,
position: 'right',
min: negativeY,
max: maxY,
axisLine: {
onZero: false
},
axisLabel: {
formatter: (value: number) => (value < minY ? '' : value + '')
}
},
grid: {
left: defaults.grid.right,
right: defaults.grid.left
},
tooltip: {
trigger: 'none',
showContent: false,
axisPointer: {
axis: 'y',
snap: false
}
},
series: [
{
...series,
type: 'custom',
silent: true,
data: rawData,
renderItem
}
]
},
options,
defaults
);
}, [options, title, rawData, minX, maxX, minY, maxY, negativeY, renderItem]);
const [highlight, setHighlight] = useState<number | null>(null);
const [dots, setDots] = useState<[number, number, number][]>([]);
const tooltipRef = useRef<HTMLDivElement | null>(null);
const [tooltip, setTooltip] = useState('');
const mouseout = useCallback(() => {
setHighlight(null);
setDots([]);
if (chartOptions.tooltip?.formatter) {
setTooltip('');
if (tooltipRef.current) {
tooltipRef.current.style.display = 'none';
}
}
}, [chartOptions.tooltip]);
const mousemove = useCallback(
(echarts: ECharts, e: {offsetX: number; offsetY: number}) => {
try {
if (!echarts || !e) {
return;
}
const {offsetX, offsetY} = e;
if (offsetY < negativeY + ((chartOptions.grid as EChartOption.Grid).top as number) ?? 0) {
mouseout();
return;
}
const [x, y] = echarts.convertFromPixel('grid' as EChartsConvertFinder, [offsetX, offsetY]) as [
number,
number
];
const data = (echarts.getOption().series?.[0].data as number[][]) ?? [];
// find right on top step
const steps = data.map(row => row[1]).sort((a, b) => a - b);
let i = 0;
let step: number | null = null;
while (i < steps.length) {
if (y <= steps[i++]) {
step = steps[i - 1];
break;
}
}
setHighlight(step == null ? null : data.findIndex(row => row[1] === step));
// find nearest x axis point
let dots: [number, number, number][] = [];
if (step == null) {
setDots(dots);
} else {
dots = data.map(row => {
const pt: [number, number, number] = [row[0], row[1], row[2]];
let d = Number.POSITIVE_INFINITY;
for (let j = 0; j < row.length; j += 3) {
const d1 = Math.abs(row[j] - x);
if (d1 < d) {
d = d1;
pt[0] = row[j];
pt[2] = row[j + 2];
}
}
return pt;
});
setDots(dots);
}
// set tooltip
if (chartOptions.tooltip?.formatter) {
setTooltip(
// eslint-disable-next-line @typescript-eslint/no-explicit-any
step == null ? '' : (chartOptions.tooltip?.formatter as any)?.(step, dots)
);
if (tooltipRef.current) {
if (step == null) {
tooltipRef.current.style.display = 'none';
} else {
tooltipRef.current.style.left = `${offsetX + 10}px`;
tooltipRef.current.style.top = `${offsetY + 10}px`;
tooltipRef.current.style.display = 'block';
}
}
}
} catch {
mouseout();
}
},
[mouseout, negativeY, chartOptions.grid, chartOptions.tooltip]
);
const throttled = useThrottleFn(mousemove, {wait: 200});
const init = useCallback<NonNullable<Options['onInit']>>(
echarts => {
try {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const zr = (echarts as any).getZr();
if (zr) {
zr.on('mousemove', (e: {offsetX: number; offsetY: number}) => throttled.run(echarts, e));
zr.on('mouseout', () => {
throttled.cancel();
mouseout();
});
}
} catch {
throttled.cancel();
}
onInit?.(echarts);
},
[onInit, throttled, mouseout]
);
const {ref: echartRef, echart, wrapper, saveAsImage} = useECharts<HTMLDivElement>({
loading: !!loading,
zoom,
autoFit: true,
onInit: init
});
useImperativeHandle(ref, () => ({
saveAsImage: () => {
saveAsImage(title);
}
}));
useEffect(() => {
if (process.browser) {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const {color, colorAlt, ...defaults} = chart;
echart?.setOption(chartOptions, {notMerge: true});
}
}, [echart, chartOptions]);
const chartOptions: EChartOption = defaultsDeep(
{
title: {
text: title ?? ''
},
visualMap: {
min: minStep,
max: maxStep
},
xAxis: {
min: minX,
max: maxX
},
grid: {
top: '40%'
},
tooltip: {
axisPointer: {
axis: 'y',
snap: false
useEffect(() => {
if (echart) {
try {
if (highlight == null) {
echart.setOption({
graphic: {
elements: [
{
id: 'highlight',
type: 'polyline',
$action: 'remove'
}
]
}
},
series: [
{
type: 'custom',
dimensions: ['x', 'y'],
data: rawData as number[][],
renderItem: ((params, api) => {
const points = makePolyPoints(
params.dataIndex as number,
api.value as GetValue,
api.coord as GetCoord,
(params.coordSys.y as number) - 10
);
return {
type: 'polygon',
});
} else {
const data = (echart.getOption().series?.[0].data as number[][]) ?? [];
const getCoord: GetCoord = pt =>
echart.convertToPixel('grid' as EChartsConvertFinder, pt) as [number, number];
const getValue: GetValue = i => data[highlight][i];
echart.setOption({
graphic: {
elements: [
{
id: 'highlight',
type: 'polyline',
$action: 'replace',
silent: true,
cursor: 'default',
zlevel: 1,
z: 1,
shape: {
points
points: makePolyPoints(highlight, getValue, getCoord)
}
}
]
}
});
}
} catch {
// ignore
}
}
}, [highlight, echart, makePolyPoints]);
useEffect(() => {
if (echart) {
try {
if (!dots.length) {
echart.setOption({
graphic: {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
elements: ((echart.getOption()?.graphic as any[])?.[0]?.elements as any[])
?.filter(element => (element.id as string).startsWith('dot'))
.map(element => ({
id: element.id,
type: 'circle',
$action: 'remove'
}))
}
});
} else {
const getCoord: GetCoord = pt =>
echart.convertToPixel('grid' as EChartsConvertFinder, pt) as [number, number];
echart.setOption({
graphic: {
elements: dots.map((dot, i) => {
const pt = getPoint(dot[0], dot[1], dot[2], getCoord);
return {
type: 'circle',
id: `dot${i}`,
$action: 'replace',
cursor: 'default',
zlevel: 1,
z: 2,
shape: {
cx: pt[0],
cy: pt[1],
r: 3
},
style: api.style({
stroke: chart.xAxis.axisLine.lineStyle.color,
lineWidth: 1
})
style: {
fill: '#fff',
stroke: chartOptions.color?.[0],
lineWidth: 2
}
};
}) as RenderItem
})
}
]
},
options,
defaults
);
echart?.setOption(chartOptions, {notMerge: true});
});
}
} catch {
// ignore
}
}
}, [options, data, title, i18n.language, echart, rawData, minX, maxX, minStep, maxStep, makePolyPoints]);
}, [dots, echart, chartOptions.color, getPoint]);
return (
<Wrapper ref={wrapper} className={className}>
......@@ -159,6 +412,7 @@ const StackChart = React.forwardRef<StackChartRef, StackChartProps & WithStyled>
</div>
)}
<div className="echarts" ref={echartRef}></div>
<Tooltip className="tooltip" ref={tooltipRef} dangerouslySetInnerHTML={{__html: tooltip}} />
</Wrapper>
);
}
......
import {useRef} from 'react';
export default function useCreation<T>(factory: () => T, deps: unknown[]) {
const {current} = useRef({
deps,
obj: undefined as undefined | T,
initialized: false
});
if (current.initialized === false || !depsAreSame(current.deps, deps)) {
current.deps = deps;
current.obj = factory();
current.initialized = true;
}
return current.obj as T;
}
function depsAreSame(oldDeps: unknown[], deps: unknown[]): boolean {
if (oldDeps === deps) return true;
for (const i in oldDeps) {
if (oldDeps[i] !== deps[i]) return false;
}
return true;
}
......@@ -6,12 +6,18 @@ import {dataURL2Blob} from '~/utils/image';
import {saveAs} from 'file-saver';
import styled from 'styled-components';
const useECharts = <T extends HTMLElement, W extends HTMLElement = HTMLDivElement>(options: {
export type Options = {
loading?: boolean;
gl?: boolean;
zoom?: boolean;
autoFit?: boolean;
}): {
onInit?: (echarts: ECharts) => unknown;
onDispose?: (echarts: ECharts) => unknown;
};
const useECharts = <T extends HTMLElement, W extends HTMLElement = HTMLDivElement>(
options: Options
): {
ref: MutableRefObject<T | null>;
wrapper: MutableRefObject<W | null>;
echart: ECharts | null;
......@@ -21,6 +27,9 @@ const useECharts = <T extends HTMLElement, W extends HTMLElement = HTMLDivElemen
const echartInstance = useRef<ECharts | null>(null);
const [echart, setEchart] = useState<ECharts | null>(null);
const onInit = useRef(options.onInit);
const onDispose = useRef(options.onDispose);
const createChart = useCallback(() => {
(async () => {
const echarts = await import('echarts');
......@@ -31,20 +40,29 @@ const useECharts = <T extends HTMLElement, W extends HTMLElement = HTMLDivElemen
return;
}
echartInstance.current = echarts.init((ref.current as unknown) as HTMLDivElement);
if (options.zoom) {
setTimeout(() => {
setTimeout(() => {
if (options.zoom) {
echartInstance.current?.dispatchAction({
type: 'takeGlobalCursor',
key: 'dataZoomSelect',
dataZoomSelectActive: true
});
}, 0);
}
}
if (echartInstance.current) {
onInit.current?.(echartInstance.current);
}
}, 0);
setEchart(echartInstance.current);
})();
}, [options.gl, options.zoom]);
const destroyChart = useCallback(() => {
if (echartInstance.current) {
onDispose.current?.(echartInstance.current);
}
echartInstance.current?.dispose();
setEchart(null);
}, []);
......
......@@ -35,20 +35,20 @@ function useRunningRequest<D = unknown, E = unknown>(
key: keyInterface,
running: boolean,
fetcher?: fetcherFn<D>,
config?: Omit<ConfigInterface<D, E, fetcherFn<D>>, 'refreshInterval' | 'dedupingInterval' | 'errorRetryInterval'>
config?: Omit<ConfigInterface<D, E, fetcherFn<D>>, 'dedupingInterval' | 'errorRetryInterval'>
): Response<D, E>;
function useRunningRequest<D = unknown, E = unknown>(
key: keyInterface,
running: boolean,
fetcher?: fetcherFn<D>,
config?: Omit<ConfigInterface<D, E, fetcherFn<D>>, 'refreshInterval' | 'dedupingInterval' | 'errorRetryInterval'>
config?: Omit<ConfigInterface<D, E, fetcherFn<D>>, 'dedupingInterval' | 'errorRetryInterval'>
) {
const c = useMemo<ConfigInterface<D, E, fetcherFn<D>>>(
() => ({
...config,
refreshInterval: running ? 15 * 1000 : 0,
dedupingInterval: 15 * 1000,
errorRetryInterval: 15 * 1000
refreshInterval: running ? config?.refreshInterval ?? 15 * 1000 : 0,
dedupingInterval: config?.refreshInterval ?? 15 * 1000,
errorRetryInterval: config?.refreshInterval ?? 15 * 1000
}),
[running, config]
);
......
/* eslint-disable @typescript-eslint/no-explicit-any */
import throttle from 'lodash/throttle';
import useCreation from '~/hooks/useCreation';
import {useRef} from 'react';
export interface ThrottleOptions {
wait?: number;
leading?: boolean;
trailing?: boolean;
}
type Fn = (...args: any[]) => any;
interface ReturnValue<T extends Fn> {
run: T;
cancel: () => void;
}
function useThrottleFn<T extends Fn>(fn: T, options?: ThrottleOptions): ReturnValue<T> {
const fnRef = useRef<Fn>(fn);
fnRef.current = fn;
const wait = options?.wait ?? 1000;
const throttled = useCreation(
() =>
throttle(
(...args: any[]) => {
fnRef.current(...args);
},
wait,
options
),
[]
);
return {
run: (throttled as any) as T,
cancel: throttled.cancel
};
}
export default useThrottleFn;
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
moduleFileExtensions: ['ts', 'tsx', 'js'],
transform: {
'^.+\\.(ts|tsx)$': 'ts-jest'
},
testRegex: '(/__tests__/.*|(\\.|/)(test|spec))\\.[jt]sx?$',
setupFilesAfterEnv: ['<rootDir>/jest.setup.ts'],
coveragePathIgnorePatterns: ['<rootDir>/next.config.js', '<rootDir>/node_modules/'],
testPathIgnorePatterns: ['<rootDir>/dist/', '<rootDir>/out/', '<rootDir>/node_modules/'],
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'],
moduleNameMapper: {
'\\.css$': '<rootDir>/__mocks__/styleMock.js',
'~/(.*)': '<rootDir>/$1'
},
snapshotSerializers: ['enzyme-to-json/serializer'],
globals: {
'ts-jest': {
tsConfig: '<rootDir>/tsconfig.test.json'
}
}
};
import ReactSixteenAdapter from 'enzyme-adapter-react-16';
import {configure} from 'enzyme';
configure({adapter: new ReactSixteenAdapter()});
......@@ -29,7 +29,8 @@
"build:next": "next build",
"export": "next export",
"start": "next start",
"test": "echo \"Error: no test specified\" && exit 0"
"test": "NODE_OPTIONS=\"--max-old-space-size=4096\" jest",
"test:coverage": "NODE_OPTIONS=\"--max-old-space-size=4096\" jest --coverage"
},
"dependencies": {
"@tippyjs/react": "4.0.5",
......@@ -66,7 +67,10 @@
"@babel/core": "7.10.3",
"@types/d3-format": "1.3.1",
"@types/echarts": "4.6.3",
"@types/enzyme": "3.10.5",
"@types/enzyme-adapter-react-16": "1.0.6",
"@types/file-saver": "2.0.1",
"@types/jest": "26.0.3",
"@types/lodash": "4.14.157",
"@types/mime-types": "2.1.0",
"@types/node": "14.0.14",
......@@ -83,7 +87,12 @@
"cross-env": "7.0.2",
"css-loader": "3.6.0",
"enhanced-resolve": "4.2.0",
"enzyme": "3.11.0",
"enzyme-adapter-react-16": "1.15.2",
"enzyme-to-json": "3.5.0",
"jest": "26.1.0",
"ora": "4.0.4",
"ts-jest": "26.1.1",
"typescript": "3.9.5",
"worker-plugin": "4.0.3"
},
......
import {NextI18NextPage, useTranslation} from '~/utils/i18n';
import {headerHeight, rem} from '~/utils/style';
import React from 'react';
import styled from 'styled-components';
const Error = styled.div`
......
......@@ -5,9 +5,6 @@ import {Modes} from './types';
const baseOptions: EChartOption = {
legend: {
data: []
},
tooltip: {
showContent: false
}
};
......
......@@ -30,8 +30,9 @@
"isolatedModules": true
},
"exclude": [
"server",
"node_modules"
"node_modules",
"__tests__",
"__mocks__"
],
"include": [
"next-env.d.ts",
......
{
"compilerOptions": {
"jsx": "react",
"target": "es5",
"module": "esnext",
"moduleResolution": "node",
"lib": [
"esnext",
"esnext.asynciterable",
"dom",
"webworker"
],
"esModuleInterop": true,
"allowJs": true,
"resolveJsonModule": true,
"sourceMap": true,
"strict": true,
"noEmit": true,
"baseUrl": ".",
"paths": {
"~/*": [
"./*"
]
},
"types": [
"@types/node",
"@types/jest"
],
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"isolatedModules": true,
"allowSyntheticDefaultImports": true,
"noImplicitAny": true
}
}
......@@ -3,7 +3,7 @@ export interface Run {
colors: [string, string];
}
export interface Tag<R = Run> {
export interface Tag<R extends Run = Run> {
runs: R[];
label: string;
}
......
......@@ -21,3 +21,6 @@ export const quantile = (values: number[], p: number) => {
const value1 = new BigNumber(values[i0 + 1]);
return value0.plus(value1.minus(value0).multipliedBy(i.minus(i0))).toNumber();
};
export const distance = (p1: [number, number], p2: [number, number]): number =>
Math.sqrt((p1[0] - p2[0]) ** 2 + (p1[1] - p2[1]) ** 2);
......@@ -24,7 +24,7 @@
},
"scripts": {
"build": "tsc",
"test": "echo \"Error: no test specified\" && exit 0"
"test": "echo \"Error: no test specified\" && exit 0; #"
},
"files": [
"dist",
......
......@@ -29,7 +29,7 @@
],
"scripts": {
"build": "tsc",
"test": "echo \"Error: no test specified\" && exit 0"
"test": "echo \"Error: no test specified\" && exit 0; #"
},
"dependencies": {
"faker": "4.1.0",
......
......@@ -24,7 +24,7 @@
},
"scripts": {
"build": "rimraf dist && webpack",
"test": "echo \"Error: no test specified\" && exit 0"
"test": "echo \"Error: no test specified\" && exit 0; #"
},
"files": [
"dist"
......
......@@ -27,7 +27,7 @@
"build": "cross-env API_URL=/api ts-node --script-mode build.ts",
"build:webpack": "webpack",
"start": "pm2-runtime ecosystem.config.js",
"test": "echo \"Error: no test specified\" && exit 0"
"test": "echo \"Error: no test specified\" && exit 0; #"
},
"main": "dist/index.js",
"types": "dist/index.d.ts",
......
......@@ -28,7 +28,7 @@
],
"scripts": {
"build": "cross-env ts-node --script-mode build.ts",
"test": "echo \"Error: no test specified\" && exit 0"
"test": "echo \"Error: no test specified\" && exit 0; #"
},
"devDependencies": {
"@types/node": "14.0.14",
......
......@@ -32,7 +32,7 @@
"types": "dist/index.d.ts",
"scripts": {
"build": "wasm-pack build --release --out-dir dist --out-name index .",
"test": "echo \"Error: no test specified\" && exit 0"
"test": "echo \"Error: no test specified\" && exit 0; #"
},
"devDependencies": {
"wasm-pack": "0.9.1"
......
......@@ -75,7 +75,7 @@ fn compute_histogram(
let mut range: Vec<f64> = vec![];
let mut optional = Some(min);
while let Some(i) = optional {
if i > max {
if i >= max {
optional = None;
} else {
range.push(i);
......
此差异已折叠。
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册