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

feat: simplize histogram chart tooltip (#696)

上级 94849229
# Always validate the PR title AND all the commits
titleAndCommits: true
# Allows use of Merge commits (eg on github: "Merge branch 'master' into feature/ride-unicorns")
# this is only relevant when using commitsOnly: true (or titleAndCommits: true)
allowMergeCommits: true
...@@ -38,19 +38,19 @@ ...@@ -38,19 +38,19 @@
"version": "yarn format && git add -A" "version": "yarn format && git add -A"
}, },
"devDependencies": { "devDependencies": {
"@typescript-eslint/eslint-plugin": "3.4.0", "@typescript-eslint/eslint-plugin": "3.5.0",
"@typescript-eslint/parser": "3.4.0", "@typescript-eslint/parser": "3.5.0",
"eslint": "7.3.1", "eslint": "7.3.1",
"eslint-config-prettier": "6.11.0", "eslint-config-prettier": "6.11.0",
"eslint-plugin-prettier": "3.1.4", "eslint-plugin-prettier": "3.1.4",
"eslint-plugin-react": "7.20.0", "eslint-plugin-react": "7.20.3",
"eslint-plugin-react-hooks": "4.0.0", "eslint-plugin-react-hooks": "4.0.5",
"husky": "4.2.5", "husky": "4.2.5",
"lerna": "3.22.1", "lerna": "3.22.1",
"lint-staged": "10.2.11", "lint-staged": "10.2.11",
"prettier": "2.0.5", "prettier": "2.0.5",
"rimraf": "3.0.2", "rimraf": "3.0.2",
"typescript": "3.9.5", "typescript": "3.9.6",
"yarn": "1.22.4" "yarn": "1.22.4"
}, },
"engines": { "engines": {
......
...@@ -45,7 +45,7 @@ ...@@ -45,7 +45,7 @@
"@types/yargs": "15.0.5", "@types/yargs": "15.0.5",
"cross-env": "7.0.2", "cross-env": "7.0.2",
"ts-node": "8.10.2", "ts-node": "8.10.2",
"typescript": "3.9.5" "typescript": "3.9.6"
}, },
"engines": { "engines": {
"node": ">=10", "node": ">=10",
......
...@@ -144,37 +144,61 @@ const HistogramChart: FunctionComponent<HistogramChartProps> = ({cid, run, tag, ...@@ -144,37 +144,61 @@ const HistogramChart: FunctionComponent<HistogramChartProps> = ({cid, run, tag,
if (!data || highlight == null) { if (!data || highlight == null) {
return ''; return '';
} }
const series = (params as EChartOption.Tooltip.Format[]).filter( const series = (params as EChartOption.Tooltip.Format[]).find(
s => s.data[1] === (data as OverlayData).data[highlight][0][1] s => s.data[1] === (data as OverlayData).data[highlight][0][1]
); );
return [ return series?.seriesName ?? '';
series[0].seriesName,
...series.map(s => `${formatTooltipXValue(s.data[2])}, ${formatTooltipYValue(s.data[3])}`)
].join('<br />');
}, },
[highlight, data] [highlight, data]
), ),
[Modes.Offset]: useCallback( [Modes.Offset]: useCallback((dot: [number, number, number]) => dot[2], [])
(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; } as const;
const pointerFormatter = {
[Modes.Overlay]: useCallback((params: {value: number; axisDimension: 'x' | 'y'}) => {
if (params.axisDimension === 'x') {
return formatTooltipXValue(params.value);
}
if (params.axisDimension === 'y') {
return formatTooltipYValue(params.value);
}
return '' as never;
}, []),
[Modes.Offset]: useCallback((params: {axisDimension: 'x' | 'y'}, dot: [number, number, number]) => {
if (params.axisDimension === 'x') {
return formatTooltipXValue(dot?.[0]);
}
if (params.axisDimension === 'y') {
return formatTooltipYValue(dot?.[1]);
}
return '' as never;
}, [])
};
const options = useMemo( const options = useMemo(
() => ({ () => ({
...chartOptions[mode], ...chartOptions[mode],
color: [run.colors[0]], color: [run.colors[0]],
tooltip: { tooltip: {
formatter: formatter[mode] formatter: formatter[mode]
},
axisPointer: {
label: {
formatter: pointerFormatter[mode]
}
},
xAxis: {
axisPointer: {
snap: mode === Modes.Overlay
}
},
yAxis: {
axisPointer: {
snap: mode === Modes.Overlay
}
} }
}), }),
[mode, run, formatter] [mode, run, formatter, pointerFormatter]
); );
const mousemove = useCallback((echarts: ECharts, {offsetX, offsetY}: {offsetX: number; offsetY: number}) => { const mousemove = useCallback((echarts: ECharts, {offsetX, offsetY}: {offsetX: number; offsetY: number}) => {
......
...@@ -16,10 +16,13 @@ import {InitConfig} from '@visualdl/i18n'; ...@@ -16,10 +16,13 @@ import {InitConfig} from '@visualdl/i18n';
import Language from '~/components/Language'; import Language from '~/components/Language';
import ee from '~/utils/event'; import ee from '~/utils/event';
import {getApiToken} from '~/utils/fetch'; import {getApiToken} from '~/utils/fetch';
import queryString from 'query-string';
import styled from 'styled-components'; import styled from 'styled-components';
import useNavItems from '~/hooks/useNavItems'; import useNavItems from '~/hooks/useNavItems';
import {useRouter} from 'next/router'; import {useRouter} from 'next/router';
const API_TOKEN_KEY: string = globalThis.__vdl_api_token_key__ || '';
const Nav = styled.nav` const Nav = styled.nav`
background-color: ${navbarBackgroundColor}; background-color: ${navbarBackgroundColor};
color: ${textInvertColor}; color: ${textInvertColor};
...@@ -107,10 +110,12 @@ const Navbar: FunctionComponent = () => { ...@@ -107,10 +110,12 @@ const Navbar: FunctionComponent = () => {
path += `/${subpath}`; path += `/${subpath}`;
} }
path += '/index'; path += '/index';
if (process.env.API_TOKEN_KEY) { if (API_TOKEN_KEY) {
const id = getApiToken(); const id = getApiToken();
if (id) { if (id) {
path += `?${process.env.API_TOKEN_KEY}=${encodeURIComponent(id)}`; path += `?${queryString.stringify({
[API_TOKEN_KEY]: id
})}`;
} }
} }
return path; return path;
......
...@@ -20,7 +20,7 @@ type PreloaderProps = { ...@@ -20,7 +20,7 @@ type PreloaderProps = {
}; };
const Preloader: FunctionComponent<PreloaderProps> = ({url, as}) => const Preloader: FunctionComponent<PreloaderProps> = ({url, as}) =>
process.env.API_TOKEN_KEY ? null : ( process.env.API_TOKEN_KEY || globalThis.__vdl_api_token_key__ ? null : (
<Head> <Head>
<link rel="preload" href={process.env.API_URL + url} crossOrigin="anonymous" as={as || 'fetch'} /> <link rel="preload" href={process.env.API_URL + url} crossOrigin="anonymous" as={as || 'fetch'} />
</Head> </Head>
......
...@@ -120,6 +120,36 @@ const StackChart = React.forwardRef<StackChartRef, StackChartProps & WithStyled> ...@@ -120,6 +120,36 @@ const StackChart = React.forwardRef<StackChartRef, StackChartProps & WithStyled>
[makePolyPoints] [makePolyPoints]
); );
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 highLightRef = useRef(highlight);
const dotsRef = useRef(dots);
useEffect(() => {
highLightRef.current = highlight;
}, [highlight]);
useEffect(() => {
dotsRef.current = dots;
}, [dots]);
const pointerLabelFormatter = options?.axisPointer?.label?.formatter;
// formatter change will cause echarts rerender axis pointer label
// so we need to use 2 refs instead of dots and highlight to get rid of dependencies of these two variables
const axisPointerLabelFormatter = useCallback(
params => {
if (!pointerLabelFormatter || highLightRef.current == null) {
return '';
}
if ('string' === typeof pointerLabelFormatter) {
return pointerLabelFormatter;
}
return pointerLabelFormatter(params, dotsRef.current[highLightRef.current]);
},
[pointerLabelFormatter]
);
const chartOptions = useMemo<EChartOption>(() => { const chartOptions = useMemo<EChartOption>(() => {
// eslint-disable-next-line @typescript-eslint/no-unused-vars // eslint-disable-next-line @typescript-eslint/no-unused-vars
const {color, colorAlt, toolbox, series, ...defaults} = chart; const {color, colorAlt, toolbox, series, ...defaults} = chart;
...@@ -133,9 +163,17 @@ const StackChart = React.forwardRef<StackChartRef, StackChartProps & WithStyled> ...@@ -133,9 +163,17 @@ const StackChart = React.forwardRef<StackChartRef, StackChartProps & WithStyled>
min: minY, min: minY,
max: maxY max: maxY
}, },
axisPointer: {
label: {
formatter: axisPointerLabelFormatter
}
},
xAxis: { xAxis: {
min: minX, min: minX,
max: maxX max: maxX,
axisPointer: {
type: 'none'
}
}, },
yAxis: { yAxis: {
inverse: true, inverse: true,
...@@ -147,6 +185,9 @@ const StackChart = React.forwardRef<StackChartRef, StackChartProps & WithStyled> ...@@ -147,6 +185,9 @@ const StackChart = React.forwardRef<StackChartRef, StackChartProps & WithStyled>
}, },
axisLabel: { axisLabel: {
formatter: (value: number) => (value < minY ? '' : value + '') formatter: (value: number) => (value < minY ? '' : value + '')
},
axisPointer: {
type: 'none'
} }
}, },
grid: { grid: {
...@@ -174,12 +215,7 @@ const StackChart = React.forwardRef<StackChartRef, StackChartProps & WithStyled> ...@@ -174,12 +215,7 @@ const StackChart = React.forwardRef<StackChartRef, StackChartProps & WithStyled>
options, options,
defaults defaults
); );
}, [options, title, rawData, minX, maxX, minY, maxY, negativeY, renderItem]); }, [options, title, rawData, minX, maxX, minY, maxY, negativeY, renderItem, axisPointerLabelFormatter]);
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(() => { const mouseout = useCallback(() => {
setHighlight(null); setHighlight(null);
...@@ -220,7 +256,8 @@ const StackChart = React.forwardRef<StackChartRef, StackChartProps & WithStyled> ...@@ -220,7 +256,8 @@ const StackChart = React.forwardRef<StackChartRef, StackChartProps & WithStyled>
break; break;
} }
} }
setHighlight(step == null ? null : data.findIndex(row => row[1] === step)); const highlight = step == null ? null : data.findIndex(row => row[1] === step);
setHighlight(highlight);
// find nearest x axis point // find nearest x axis point
let dots: [number, number, number][] = []; let dots: [number, number, number][] = [];
...@@ -247,7 +284,7 @@ const StackChart = React.forwardRef<StackChartRef, StackChartProps & WithStyled> ...@@ -247,7 +284,7 @@ const StackChart = React.forwardRef<StackChartRef, StackChartProps & WithStyled>
if (chartOptions.tooltip?.formatter) { if (chartOptions.tooltip?.formatter) {
setTooltip( setTooltip(
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
step == null ? '' : (chartOptions.tooltip?.formatter as any)?.(step, dots) highlight == null ? '' : (chartOptions.tooltip?.formatter as any)?.(dots[highlight])
); );
if (tooltipRef.current) { if (tooltipRef.current) {
if (step == null) { if (step == null) {
......
...@@ -31,7 +31,6 @@ module.exports = { ...@@ -31,7 +31,6 @@ module.exports = {
DEFAULT_LANGUAGE, DEFAULT_LANGUAGE,
LOCALE_PATH, LOCALE_PATH,
LANGUAGES, LANGUAGES,
API_TOKEN_KEY: process.env.API_TOKEN_KEY || '',
PUBLIC_PATH: publicPath, PUBLIC_PATH: publicPath,
API_URL: apiUrl API_URL: apiUrl
}, },
......
...@@ -64,7 +64,7 @@ ...@@ -64,7 +64,7 @@
"tippy.js": "6.2.3" "tippy.js": "6.2.3"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "7.10.3", "@babel/core": "7.10.4",
"@types/d3-format": "1.3.1", "@types/d3-format": "1.3.1",
"@types/echarts": "4.6.3", "@types/echarts": "4.6.3",
"@types/enzyme": "3.10.5", "@types/enzyme": "3.10.5",
...@@ -82,7 +82,7 @@ ...@@ -82,7 +82,7 @@
"babel-plugin-emotion": "10.0.33", "babel-plugin-emotion": "10.0.33",
"babel-plugin-styled-components": "1.10.7", "babel-plugin-styled-components": "1.10.7",
"babel-plugin-typescript-to-proptypes": "1.3.2", "babel-plugin-typescript-to-proptypes": "1.3.2",
"copy-webpack-plugin": "6.0.2", "copy-webpack-plugin": "6.0.3",
"core-js": "3.6.5", "core-js": "3.6.5",
"cross-env": "7.0.2", "cross-env": "7.0.2",
"css-loader": "3.6.0", "css-loader": "3.6.0",
...@@ -93,7 +93,7 @@ ...@@ -93,7 +93,7 @@
"jest": "26.1.0", "jest": "26.1.0",
"ora": "4.0.4", "ora": "4.0.4",
"ts-jest": "26.1.1", "ts-jest": "26.1.1",
"typescript": "3.9.5", "typescript": "3.9.6",
"worker-plugin": "4.0.3" "worker-plugin": "4.0.3"
}, },
"engines": { "engines": {
......
...@@ -12,12 +12,14 @@ import {ToastContainer} from 'react-toastify'; ...@@ -12,12 +12,14 @@ import {ToastContainer} from 'react-toastify';
import queryString from 'query-string'; import queryString from 'query-string';
import {withRouter} from 'next/router'; import {withRouter} from 'next/router';
const API_TOKEN_KEY: string = globalThis.__vdl_api_token_key__ || '';
class VDLApp extends App { class VDLApp extends App {
constructor(props: AppProps) { constructor(props: AppProps) {
super(props); super(props);
if (process.browser && process.env.API_TOKEN_KEY) { if (process.browser && API_TOKEN_KEY) {
const query = queryString.parse(window.location.search); const query = queryString.parse(window.location.search);
setApiToken(query[process.env.API_TOKEN_KEY]); setApiToken(query[API_TOKEN_KEY]);
} }
} }
...@@ -25,16 +27,16 @@ class VDLApp extends App { ...@@ -25,16 +27,16 @@ class VDLApp extends App {
Router.events.on('routeChangeStart', () => NProgress.start()); Router.events.on('routeChangeStart', () => NProgress.start());
Router.events.on('routeChangeComplete', (url: string) => { Router.events.on('routeChangeComplete', (url: string) => {
NProgress.done(); NProgress.done();
if (process.env.API_TOKEN_KEY) { if (API_TOKEN_KEY) {
const id = getApiToken(); const id = getApiToken();
const parsed = queryString.parseUrl(url); const parsed = queryString.parseUrl(url);
if (id && !parsed.query[process.env.API_TOKEN_KEY]) { if (id && !parsed.query[API_TOKEN_KEY]) {
this.props.router.replace( this.props.router.replace(
queryString.stringifyUrl({ queryString.stringifyUrl({
url: parsed.url, url: parsed.url,
query: { query: {
...parsed.query, ...parsed.query,
[process.env.API_TOKEN_KEY]: id [API_TOKEN_KEY]: id
} }
}), }),
undefined, undefined,
...@@ -82,8 +84,8 @@ class VDLApp extends App { ...@@ -82,8 +84,8 @@ class VDLApp extends App {
static async getInitialProps(appContext: AppContext) { static async getInitialProps(appContext: AppContext) {
const appProps = await App.getInitialProps(appContext); const appProps = await App.getInitialProps(appContext);
if (process.env.API_TOKEN_KEY) { if (API_TOKEN_KEY) {
setApiToken(appContext.router.query[process.env.API_TOKEN_KEY]); setApiToken(appContext.router.query[API_TOKEN_KEY]);
} }
return {...appProps}; return {...appProps};
} }
......
...@@ -59,6 +59,9 @@ export default class VDLDocument extends Document<VDLDocumentProps> { ...@@ -59,6 +59,9 @@ export default class VDLDocument extends Document<VDLDocumentProps> {
<script <script
dangerouslySetInnerHTML={{__html: `__vdl_public_path__='${process.env.PUBLIC_PATH}'`}} dangerouslySetInnerHTML={{__html: `__vdl_public_path__='${process.env.PUBLIC_PATH}'`}}
></script> ></script>
<script
dangerouslySetInnerHTML={{__html: `__vdl_api_token_key__='${process.env.API_TOKEN_KEY}'`}}
></script>
<Main /> <Main />
<NextScript /> <NextScript />
</body> </body>
......
declare global { declare global {
interface Window { interface Window {
__visualdl_instance_id__?: string; __visualdl_instance_id__?: string | string[];
__vdl_api_token_key__?: string;
} }
namespace globalThis { namespace globalThis {
// eslint-disable-next-line no-var // eslint-disable-next-line no-var
var __visualdl_instance_id__: string | undefined; var __visualdl_instance_id__: string | string[] | undefined;
// eslint-disable-next-line no-var
var __vdl_api_token_key__: string | undefined;
} }
} }
declare namespace NodeJS { declare namespace NodeJS {
interface Global { interface Global {
__visualdl_instance_id__?: string; __visualdl_instance_id__?: string | string[];
__vdl_api_token_key__?: string;
} }
} }
......
...@@ -3,28 +3,33 @@ ...@@ -3,28 +3,33 @@
import fetch from 'isomorphic-unfetch'; import fetch from 'isomorphic-unfetch';
const API_TOKEN_KEY: string = globalThis.__vdl_api_token_key__ || '';
const API_TOKEN_HEADER = 'X-VisualDL-Instance-ID'; const API_TOKEN_HEADER = 'X-VisualDL-Instance-ID';
function addApiToken(options?: RequestInit): RequestInit | undefined { function addApiToken(options?: RequestInit): RequestInit | undefined {
if (!process.env.API_TOKEN_KEY || !globalThis.__visualdl_instance_id__) { const id = getApiToken();
if (!API_TOKEN_KEY || !id) {
return options; return options;
} }
const {headers, ...rest} = options || {}; const {headers, ...rest} = options || {};
const newHeaders = new Headers(headers);
if (Array.isArray(id)) {
id.forEach(value => newHeaders.append(API_TOKEN_HEADER, value));
} else {
newHeaders.append(API_TOKEN_HEADER, id);
}
return { return {
...rest, ...rest,
headers: { headers: newHeaders
...(headers || {}),
[API_TOKEN_HEADER]: globalThis.__visualdl_instance_id__
}
}; };
} }
export function setApiToken(id?: string | string[] | null) { export function setApiToken(id?: string | string[] | null) {
const instanceId = Array.isArray(id) ? id[0] : id; globalThis.__visualdl_instance_id__ = id || '';
globalThis.__visualdl_instance_id__ = instanceId || '';
} }
export function getApiToken() { export function getApiToken(): string | string[] | null {
return globalThis.__visualdl_instance_id__ || ''; return globalThis.__visualdl_instance_id__ || '';
} }
......
...@@ -37,11 +37,11 @@ ...@@ -37,11 +37,11 @@
"dependencies": { "dependencies": {
"detect-node": "2.0.4", "detect-node": "2.0.4",
"hoist-non-react-statics": "3.3.2", "hoist-non-react-statics": "3.3.2",
"i18next": "19.5.1", "i18next": "19.5.3",
"i18next-browser-languagedetector": "5.0.0", "i18next-browser-languagedetector": "5.0.0",
"i18next-fs-backend": "1.0.6", "i18next-fs-backend": "1.0.7",
"i18next-http-backend": "1.0.15", "i18next-http-backend": "1.0.16",
"i18next-http-middleware": "3.0.0", "i18next-http-middleware": "3.0.2",
"path-match": "1.2.4", "path-match": "1.2.4",
"prop-types": "15.7.2", "prop-types": "15.7.2",
"react-i18next": "11.7.0", "react-i18next": "11.7.0",
...@@ -53,7 +53,7 @@ ...@@ -53,7 +53,7 @@
"@types/node": "14.0.14", "@types/node": "14.0.14",
"@types/react": "16.9.41", "@types/react": "16.9.41",
"@types/react-dom": "16.9.8", "@types/react-dom": "16.9.8",
"typescript": "3.9.5" "typescript": "3.9.6"
}, },
"peerDependencies": { "peerDependencies": {
"express": "^4.17.1", "express": "^4.17.1",
......
...@@ -39,7 +39,7 @@ ...@@ -39,7 +39,7 @@
"@types/express": "4.17.6", "@types/express": "4.17.6",
"@types/faker": "4.1.12", "@types/faker": "4.1.12",
"@types/node": "14.0.14", "@types/node": "14.0.14",
"typescript": "3.9.5" "typescript": "3.9.6"
}, },
"peerDependencies": { "peerDependencies": {
"express": "^4.17.1" "express": "^4.17.1"
......
...@@ -41,14 +41,14 @@ ...@@ -41,14 +41,14 @@
}, },
"devDependencies": { "devDependencies": {
"autoprefixer": "9.8.4", "autoprefixer": "9.8.4",
"copy-webpack-plugin": "6.0.2", "copy-webpack-plugin": "6.0.3",
"css-loader": "3.6.0", "css-loader": "3.6.0",
"html-webpack-plugin": "4.3.0", "html-webpack-plugin": "4.3.0",
"mini-css-extract-plugin": "0.9.0", "mini-css-extract-plugin": "0.9.0",
"postcss-loader": "3.0.0", "postcss-loader": "3.0.0",
"rimraf": "3.0.2", "rimraf": "3.0.2",
"sass": "1.26.9", "sass": "1.26.9",
"sass-loader": "8.0.2", "sass-loader": "9.0.0",
"terser": "4.8.0", "terser": "4.8.0",
"webpack": "4.43.0" "webpack": "4.43.0"
}, },
......
...@@ -56,7 +56,7 @@ ...@@ -56,7 +56,7 @@
"shelljs": "0.8.4", "shelljs": "0.8.4",
"ts-loader": "7.0.5", "ts-loader": "7.0.5",
"ts-node": "8.10.2", "ts-node": "8.10.2",
"typescript": "3.9.5", "typescript": "3.9.6",
"webpack": "4.43.0", "webpack": "4.43.0",
"webpack-cli": "3.3.12", "webpack-cli": "3.3.12",
"webpack-dev-middleware": "3.7.2" "webpack-dev-middleware": "3.7.2"
......
...@@ -37,7 +37,7 @@ ...@@ -37,7 +37,7 @@
"cross-env": "7.0.2", "cross-env": "7.0.2",
"rimraf": "3.0.2", "rimraf": "3.0.2",
"ts-node": "8.10.2", "ts-node": "8.10.2",
"typescript": "3.9.5" "typescript": "3.9.6"
}, },
"engines": { "engines": {
"node": ">=10", "node": ">=10",
......
此差异已折叠。
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册