import React, {FunctionComponent, useCallback, useMemo} from 'react'; import styled from 'styled-components'; import useSWR from 'swr'; import compact from 'lodash/compact'; import minBy from 'lodash/minBy'; import maxBy from 'lodash/maxBy'; import sortBy from 'lodash/sortBy'; import {EChartOption} from 'echarts'; import {em, size} from '~/utils/style'; import {useTranslation} from '~/utils/i18n'; import {cycleFetcher} from '~/utils/fetch'; import {transform, range, tooltip, TooltipData} from '~/utils/scalars'; import * as chart from '~/utils/chart'; import LineChart from '~/components/LineChart'; const width = em(430); const height = em(320); const StyledLineChart = styled(LineChart)` ${size(height, width)} `; export const xAxisMap = { step: 1, relative: 4, wall: 0 }; export const sortingMethodMap = { default: null, descending: (points: TooltipData[]) => sortBy(points, point => point.item[3]).reverse(), ascending: (points: TooltipData[]) => sortBy(points, point => point.item[3]), // Compare other ponts width the trigger point, caculate the nearest sort. nearest: (points: TooltipData[], data: number[]) => sortBy(points, point => point.item[3] - data[2]) }; type DataSet = number[][]; type ScalarChartProps = { runs: string[]; tag: string; smoothing: number; xAxis: keyof typeof xAxisMap; sortingMethod: keyof typeof sortingMethodMap; outlier?: boolean; running?: boolean; }; const ScalarChart: FunctionComponent = ({ runs, tag, smoothing, xAxis, sortingMethod, outlier, running }) => { const {t} = useTranslation('scalars'); // TODO: maybe we can create a custom hook here const {data: datasets, error} = useSWR( runs.map(run => `/scalars/scalars?run=${encodeURIComponent(run)}&tag=${encodeURIComponent(tag)}`), (...urls) => cycleFetcher(urls), { refreshInterval: running ? 15 * 1000 : 0 } ); const type = xAxis === 'wall' ? 'time' : 'value'; const smooth = xAxis !== 'wall'; const smoothedDatasets = useMemo(() => datasets?.map(dataset => transform(dataset, smoothing)), [ datasets, smoothing ]); const data = useMemo( () => smoothedDatasets ?.map((dataset, i) => { // smoothed data: // [0] wall time // [1] step // [2] orginal value // [3] smoothed value // [4] relative const name = runs[i]; return [ { name, z: i, lineStyle: { width: chart.series.lineStyle.width, opacity: 0.5 }, data: dataset, encode: { x: [xAxisMap[xAxis]], y: [2] }, smooth }, { name, z: runs.length + i, data: dataset, encode: { x: [xAxisMap[xAxis]], y: [3] }, smooth } ]; }) .flat(), [runs, smooth, smoothedDatasets, xAxis] ); const yRange = useMemo(() => { const ranges = compact(smoothedDatasets?.map(dataset => range(dataset, outlier))); const min = minBy(ranges, range => range.min)?.min ?? 0; const max = maxBy(ranges, range => range.max)?.max ?? 0; if (!(min === 0 && max === 0)) { return { min: min > 0 ? min * 0.9 : min * 1.1, max: max > 0 ? max * 1.1 : max * 0.9 }; } }, [outlier, smoothedDatasets]); const formatter = useCallback( (params: EChartOption.Tooltip.Format | EChartOption.Tooltip.Format[]) => { const data = Array.isArray(params) ? params[0].data : params.data; const step = data[1]; const points = smoothedDatasets?.map((series, index) => { let nearestItem; if (step === 0) { nearestItem = series[0]; } else { for (let i = 0; i < series.length; i++) { const item = series[i]; if (item[1] === step) { nearestItem = item; break; } if (item[1] > step) { nearestItem = series[i - 1 >= 0 ? i - 1 : 0]; break; } if (!nearestItem) { nearestItem = series[series.length - 1]; } } } return { run: runs[index], item: nearestItem || [] }; }) ?? []; const sort = sortingMethodMap[sortingMethod]; return tooltip(sort ? sort(points, data) : points); }, [smoothedDatasets, runs, sortingMethod] ); return ( ); }; export default ScalarChart;