ScalarChart.tsx 10.2 KB
Newer Older
P
Peter Pan 已提交
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
/**
 * Copyright 2020 Baidu Inc. All Rights Reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

17 18 19
import type {Dataset, Range, ScalarDataset} from '~/resource/scalar';
import LineChart, {LineChartRef, XAxisType, YAxisType} from '~/components/LineChart';
import React, {FunctionComponent, useCallback, useMemo, useRef, useState} from 'react';
20
import {
P
Peter Pan 已提交
21 22
    SortingMethod,
    XAxis,
23
    chartData,
P
Peter Pan 已提交
24
    options as chartOptions,
25
    nearestPoint,
26
    singlePointRange,
27 28 29
    sortingMethodMap,
    tooltip,
    xAxisMap
P
Peter Pan 已提交
30
} from '~/resource/scalar';
31
import {rem, size} from '~/utils/style';
32

P
Peter Pan 已提交
33 34
import Chart from '~/components/Chart';
import {Chart as ChartLoader} from '~/components/Loader/ChartPage';
35
import ChartToolbox from '~/components/ChartToolbox';
36 37
import type {EChartOption} from 'echarts';
import type {Run} from '~/types';
P
Peter Pan 已提交
38
import TooltipTable from '~/components/TooltipTable';
39
import {cycleFetcher} from '~/utils/fetch';
P
Peter Pan 已提交
40
import {format} from 'd3-format';
41
import queryString from 'query-string';
P
Peter Pan 已提交
42
import {renderToStaticMarkup} from 'react-dom/server';
P
Peter Pan 已提交
43
import saveFile from '~/utils/saveFile';
44 45
import styled from 'styled-components';
import {useRunningRequest} from '~/hooks/useRequest';
46
import {useTranslation} from 'react-i18next';
P
Peter Pan 已提交
47
import useWebAssembly from '~/hooks/useWebAssembly';
48

49 50 51 52 53 54
const DownloadDataTypes = {
    csv: 'csv',
    tsv: 'tsv'
    // excel: 'xlsx'
} as const;

P
Peter Pan 已提交
55 56
const labelFormatter = format('.8');

57 58 59 60 61 62 63 64
const Wrapper = styled.div`
    ${size('100%', '100%')}
    display: flex;
    flex-direction: column;
    align-items: stretch;
    justify-content: space-between;
`;

65
const StyledLineChart = styled(LineChart)`
66 67 68 69 70 71
    flex-grow: 1;
`;

const Toolbox = styled(ChartToolbox)`
    margin-left: ${rem(20)};
    margin-right: ${rem(20)};
P
Peter Pan 已提交
72
    margin-bottom: ${rem(18)};
73 74
`;

75
const Error = styled.div`
76
    ${size('100%', '100%')}
77 78 79 80 81
    display: flex;
    justify-content: center;
    align-items: center;
`;

P
Peter Pan 已提交
82 83 84 85 86 87 88 89 90
const chartSize = {
    width: 430,
    height: 337
};
const chartSizeInRem = {
    width: rem(chartSize.width),
    height: rem(chartSize.height)
};

91
type ScalarChartProps = {
92
    runs: Run[];
93 94
    tag: string;
    smoothing: number;
P
Peter Pan 已提交
95 96
    xAxis: XAxis;
    sortingMethod: SortingMethod;
97
    outlier?: boolean;
98
    smoothedOnly?: boolean;
99
    showMostValue?: boolean;
100 101 102 103 104 105 106 107 108 109
    running?: boolean;
};

const ScalarChart: FunctionComponent<ScalarChartProps> = ({
    runs,
    tag,
    smoothing,
    xAxis,
    sortingMethod,
    outlier,
110
    smoothedOnly,
111
    showMostValue,
112 113
    running
}) => {
P
Peter Pan 已提交
114
    const {t, i18n} = useTranslation(['scalar', 'common']);
115

116 117
    const echart = useRef<LineChartRef>(null);

P
Peter Pan 已提交
118
    const {data: datasets, error, loading} = useRunningRequest<(ScalarDataset | null)[]>(
P
Peter Pan 已提交
119
        runs.map(run => `/scalar/list?${queryString.stringify({run: run.label, tag})}`),
120 121
        !!running,
        (...urls) => cycleFetcher(urls)
122 123
    );

124 125
    const [maximized, setMaximized] = useState<boolean>(false);

P
Peter Pan 已提交
126
    const xAxisType = useMemo(() => (xAxis === XAxis.WallTime ? XAxisType.time : XAxisType.value), [xAxis]);
127 128 129 130 131

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

P
Peter Pan 已提交
133 134
    const transformParams = useMemo(() => [datasets?.map(data => data ?? []) ?? [], smoothing], [datasets, smoothing]);
    const {data: smoothedDatasetsOrUndefined} = useWebAssembly<Dataset[]>('scalar_transform', transformParams);
135 136 137 138
    const smoothedDatasets = useMemo<NonNullable<typeof smoothedDatasetsOrUndefined>>(
        () => smoothedDatasetsOrUndefined ?? [],
        [smoothedDatasetsOrUndefined]
    );
139

P
Peter Pan 已提交
140 141
    const axisRangeParams = useMemo(() => [smoothedDatasets, !!outlier], [smoothedDatasets, outlier]);
    const {data: yRange} = useWebAssembly<Range>('scalar_axis_range', axisRangeParams);
142

P
Peter Pan 已提交
143 144
    const datasetRangesParams = useMemo(() => [smoothedDatasets], [smoothedDatasets]);
    const {data: datasetRanges} = useWebAssembly<Range[]>('scalar_range', datasetRangesParams);
145

146 147 148 149 150 151
    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) {
152
            if ([XAxisType.value, XAxisType.log].includes(xAxisType)) {
153 154 155 156 157
                x = singlePointRange(smoothedDatasets[0][0][xAxisMap[xAxis]]);
            }
            y = singlePointRange(smoothedDatasets[0][0][2]);
        }
        return {x, y};
158
    }, [smoothedDatasets, yRange, xAxisType, xAxis]);
159

160 161
    const data = useMemo(
        () =>
162
            chartData({
163
                data: smoothedDatasets.slice(0, runs.length),
164
                ranges: showMostValue ? datasetRanges ?? [] : [],
165
                runs,
166 167
                xAxis,
                smoothedOnly
168
            }),
169
        [smoothedDatasets, datasetRanges, runs, xAxis, smoothedOnly, showMostValue]
170 171
    );

P
Peter Pan 已提交
172 173 174 175 176
    const maxStepLength = useMemo(
        () => String(Math.max(...smoothedDatasets.map(i => Math.max(...i.map(j => j[1]))))).length,
        [smoothedDatasets]
    );

177 178
    const formatter = useCallback(
        (params: EChartOption.Tooltip.Format | EChartOption.Tooltip.Format[]) => {
P
Peter Pan 已提交
179
            const series: Dataset[number] = Array.isArray(params) ? params[0].data : params.data;
P
Peter Pan 已提交
180 181
            const idx = xAxisMap[xAxis];
            const points = nearestPoint(smoothedDatasets ?? [], runs, idx, series[idx]).map((point, index) => ({
182 183 184
                ...point,
                ...datasetRanges?.[index]
            }));
185
            const sort = sortingMethodMap[sortingMethod];
P
Peter Pan 已提交
186
            const sorted = sort(points, series);
187
            const {columns, data} = tooltip(sorted, maxStepLength, i18n);
P
Peter Pan 已提交
188 189 190
            return renderToStaticMarkup(
                <TooltipTable run={t('common:runs')} runs={sorted.map(i => i.run)} columns={columns} data={data} />
            );
191
        },
P
Peter Pan 已提交
192
        [smoothedDatasets, datasetRanges, runs, sortingMethod, xAxis, maxStepLength, t, i18n]
193 194
    );

P
Peter Pan 已提交
195 196 197 198 199
    const options = useMemo(
        () => ({
            ...chartOptions,
            tooltip: {
                ...chartOptions.tooltip,
200 201 202
                formatter,
                hideDelay: 300,
                enterable: true
P
Peter Pan 已提交
203 204 205
            },
            xAxis: {
                type: xAxisType,
P
Peter Pan 已提交
206 207 208 209
                ...ranges.x,
                axisPointer: {
                    label: {
                        formatter:
P
Peter Pan 已提交
210 211 212
                            xAxisType === XAxisType.time
                                ? undefined
                                : ({value}: {value: number}) => labelFormatter(value)
P
Peter Pan 已提交
213 214
                    }
                }
P
Peter Pan 已提交
215 216 217 218 219 220 221 222 223
            },
            yAxis: {
                type: yAxisType,
                ...ranges.y
            }
        }),
        [formatter, ranges, xAxisType, yAxisType]
    );

224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241
    const downloadData = useCallback(
        (type: keyof typeof DownloadDataTypes) => {
            saveFile(
                runs.map(
                    run =>
                        `/scalar/data?${queryString.stringify({
                            run: run.label,
                            tag,
                            type
                        })}`
                ),
                runs.map(run => `visualdl-scalar-${run.label}-${tag}.${DownloadDataTypes[type]}`),
                `visualdl-scalar-${tag}.zip`
            );
        },
        [runs, tag]
    );

P
Peter Pan 已提交
242 243 244 245 246 247 248 249
    const toolbox = useMemo(
        () => [
            {
                icon: 'maximize',
                activeIcon: 'minimize',
                tooltip: t('scalar:maximize'),
                activeTooltip: t('scalar:minimize'),
                toggle: true,
P
Peter Pan 已提交
250
                onClick: () => setMaximized(m => !m)
P
Peter Pan 已提交
251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271
            },
            {
                icon: 'restore-size',
                tooltip: t('scalar:restore'),
                onClick: () => echart.current?.restore()
            },
            {
                icon: 'log-axis',
                tooltip: t('scalar:toggle-log-axis'),
                toggle: true,
                onClick: toggleYAxisType
            },
            {
                icon: 'download',
                menuList: [
                    {
                        label: t('scalar:download-image'),
                        onClick: () => echart.current?.saveAsImage()
                    },
                    {
                        label: t('scalar:download-data'),
272 273 274 275 276 277
                        children: Object.keys(DownloadDataTypes)
                            .sort((a, b) => a.localeCompare(b))
                            .map(format => ({
                                label: t('scalar:download-data-format', {format}),
                                onClick: () => downloadData(format as keyof typeof DownloadDataTypes)
                            }))
P
Peter Pan 已提交
278 279 280 281
                    }
                ]
            }
        ],
P
Peter Pan 已提交
282
        [downloadData, t, toggleYAxisType]
P
Peter Pan 已提交
283 284
    );

285 286 287
    // display error only on first fetch
    if (!data && error) {
        return <Error>{t('common:error')}</Error>;
288 289
    }

290
    return (
P
Peter Pan 已提交
291 292 293 294 295 296
        <Chart maximized={maximized} {...chartSizeInRem}>
            <Wrapper>
                <StyledLineChart ref={echart} title={tag} options={options} data={data} loading={loading} zoom />
                <Toolbox items={toolbox} />
            </Wrapper>
        </Chart>
297 298 299 300
    );
};

export default ScalarChart;
P
Peter Pan 已提交
301 302 303 304 305 306 307 308 309 310 311

export const Loader: FunctionComponent = () => (
    <>
        <Chart {...chartSizeInRem}>
            <ChartLoader width={chartSize.width - 2} height={chartSize.height - 2} />
        </Chart>
        <Chart {...chartSizeInRem}>
            <ChartLoader width={chartSize.width - 2} height={chartSize.height - 2} />
        </Chart>
    </>
);