ScalarChart.tsx 7.0 KB
Newer Older
1 2 3
import {
    Dataset,
    Range,
4
    RangeParams,
5
    TransformParams,
6
    chartData,
7
    nearestPoint,
8
    range,
9
    singlePointRange,
10 11 12 13
    sortingMethodMap,
    tooltip,
    transform,
    xAxisMap
14
} from '~/resource/scalars';
15 16 17
import LineChart, {LineChartRef} from '~/components/LineChart';
import React, {FunctionComponent, useCallback, useMemo, useRef, useState} from 'react';
import {rem, size} from '~/utils/style';
18

19
import ChartToolbox from '~/components/ChartToolbox';
20
import {EChartOption} from 'echarts';
21
import {Run} from '~/types';
22
import {cycleFetcher} from '~/utils/fetch';
23
import ee from '~/utils/event';
24 25 26 27 28
import queryString from 'query-string';
import styled from 'styled-components';
import useHeavyWork from '~/hooks/useHeavyWork';
import {useRunningRequest} from '~/hooks/useRequest';
import {useTranslation} from '~/utils/i18n';
29

30
const smoothWasm = () =>
31
    import('@visualdl/wasm').then(({transform}) => (params: TransformParams) =>
32 33 34
        (transform(params.datasets, params.smoothing) as unknown) as Dataset[]
    );
const rangeWasm = () =>
35
    import('@visualdl/wasm').then(({range}) => (params: RangeParams) =>
36 37 38 39 40 41
        (range(params.datasets, params.outlier) as unknown) as Range
    );

const smoothWorker = () => new Worker('~/worker/scalars/smooth.worker.ts', {type: 'module'});
const rangeWorker = () => new Worker('~/worker/scalars/range.worker.ts', {type: 'module'});

42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57
const Wrapper = styled.div`
    ${size('100%', '100%')}
    display: flex;
    flex-direction: column;
    align-items: stretch;
    justify-content: space-between;

    .echarts td.run .run-indicator {
        ${size(12, 12)}
        display: inline-block;
        border-radius: 6px;
        vertical-align: middle;
        margin-right: 5px;
    }
`;

58
const StyledLineChart = styled(LineChart)`
59 60 61 62 63 64
    flex-grow: 1;
`;

const Toolbox = styled(ChartToolbox)`
    margin-left: ${rem(20)};
    margin-right: ${rem(20)};
65 66
`;

67
const Error = styled.div`
68
    ${size('100%', '100%')}
69 70 71 72 73
    display: flex;
    justify-content: center;
    align-items: center;
`;

74 75 76 77 78 79 80 81 82 83 84
enum XAxisType {
    value = 'value',
    log = 'log',
    time = 'time'
}

enum YAxisType {
    value = 'value',
    log = 'log'
}

85
type ScalarChartProps = {
86 87
    cid: symbol;
    runs: Run[];
88 89 90 91 92 93
    tag: string;
    smoothing: number;
    xAxis: keyof typeof xAxisMap;
    sortingMethod: keyof typeof sortingMethodMap;
    outlier?: boolean;
    running?: boolean;
94
    onToggleMaximized?: (maximized: boolean) => void;
95 96 97
};

const ScalarChart: FunctionComponent<ScalarChartProps> = ({
98
    cid,
99 100 101 102 103 104 105 106
    runs,
    tag,
    smoothing,
    xAxis,
    sortingMethod,
    outlier,
    running
}) => {
P
Peter Pan 已提交
107
    const {t, i18n} = useTranslation(['scalars', 'common']);
108

109 110
    const echart = useRef<LineChartRef>(null);

111
    const {data: datasets, error, loading} = useRunningRequest<(Dataset | null)[]>(
112
        runs.map(run => `/scalars/list?${queryString.stringify({run: run.label, tag})}`),
113 114
        !!running,
        (...urls) => cycleFetcher(urls)
115 116
    );

117
    const smooth = false;
118 119 120 121 122 123 124 125 126 127 128 129
    const [maximized, setMaximized] = useState<boolean>(false);
    const toggleMaximized = useCallback(() => {
        ee.emit('toggle-chart-size', cid, !maximized);
        setMaximized(m => !m);
    }, [cid, maximized]);

    const xAxisType = useMemo(() => (xAxis === 'wall' ? XAxisType.time : XAxisType.value), [xAxis]);

    const [yAxisType, setYAxisType] = useState<YAxisType>(YAxisType.value);
    const toggleYAxisType = useCallback(() => {
        setYAxisType(t => (t === YAxisType.log ? YAxisType.value : YAxisType.log));
    }, [setYAxisType]);
130

131 132
    const transformParams = useMemo(
        () => ({
133
            datasets: datasets?.map(data => data ?? []) ?? [],
134 135 136 137 138 139 140 141 142 143 144 145 146 147
            smoothing
        }),
        [datasets, smoothing]
    );
    const smoothedDatasets = useHeavyWork(smoothWasm, smoothWorker, transform, transformParams) ?? [];

    const rangeParams = useMemo(
        () => ({
            datasets: smoothedDatasets,
            outlier: !!outlier
        }),
        [smoothedDatasets, outlier]
    );
    const yRange = useHeavyWork(rangeWasm, rangeWorker, range, rangeParams);
148

149 150 151 152 153 154
    const ranges: Record<'x' | 'y', Range | undefined> = useMemo(() => {
        let x: Range | undefined = undefined;
        let y: Range | undefined = yRange;

        // if there is only one point, place it in the middle
        if (smoothedDatasets.length === 1 && smoothedDatasets[0].length === 1) {
155
            if ([XAxisType.value, XAxisType.log].includes(xAxisType)) {
156 157 158 159 160
                x = singlePointRange(smoothedDatasets[0][0][xAxisMap[xAxis]]);
            }
            y = singlePointRange(smoothedDatasets[0][0][2]);
        }
        return {x, y};
161
    }, [smoothedDatasets, yRange, xAxisType, xAxis]);
162

163 164
    const data = useMemo(
        () =>
165
            chartData({
166
                data: smoothedDatasets.slice(0, runs.length),
167 168 169 170 171
                runs,
                smooth,
                xAxis
            }),
        [smoothedDatasets, runs, smooth, xAxis]
172 173 174 175 176 177
    );

    const formatter = useCallback(
        (params: EChartOption.Tooltip.Format | EChartOption.Tooltip.Format[]) => {
            const data = Array.isArray(params) ? params[0].data : params.data;
            const step = data[1];
178
            const points = nearestPoint(smoothedDatasets ?? [], runs, step);
179
            const sort = sortingMethodMap[sortingMethod];
P
Peter Pan 已提交
180
            return tooltip(sort ? sort(points, data) : points, i18n);
181
        },
P
Peter Pan 已提交
182
        [smoothedDatasets, runs, sortingMethod, i18n]
183 184
    );

185 186 187
    // display error only on first fetch
    if (!data && error) {
        return <Error>{t('common:error')}</Error>;
188 189
    }

190
    return (
191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207
        <Wrapper>
            <StyledLineChart
                ref={echart}
                title={tag}
                xRange={ranges.x}
                yRange={ranges.y}
                xType={xAxisType}
                yType={yAxisType}
                tooltip={formatter}
                data={data}
                loading={loading}
            />
            <Toolbox
                items={[
                    {
                        icon: 'maximize',
                        activeIcon: 'minimize',
P
Peter Pan 已提交
208 209
                        tooltip: t('scalars:maximize'),
                        activeTooltip: t('scalars:minimize'),
210 211 212 213 214
                        toggle: true,
                        onClick: toggleMaximized
                    },
                    {
                        icon: 'restore-size',
P
Peter Pan 已提交
215
                        tooltip: t('scalars:restore'),
216 217 218 219
                        onClick: () => echart.current?.restore()
                    },
                    {
                        icon: 'log-axis',
P
Peter Pan 已提交
220
                        tooltip: t('scalars:axis'),
221 222 223 224 225
                        toggle: true,
                        onClick: toggleYAxisType
                    },
                    {
                        icon: 'download',
P
Peter Pan 已提交
226
                        tooltip: t('scalars:download-image'),
227 228 229 230 231
                        onClick: () => echart.current?.saveAsImage()
                    }
                ]}
            />
        </Wrapper>
232 233 234 235
    );
};

export default ScalarChart;