index.ts 9.1 KB
Newer Older
1 2
import * as chart from '~/utils/chart';

3
import {ChartDataParams, Dataset, RangeParams, TooltipData, TransformParams} from './types';
4 5
import {formatTime, quantile} from '~/utils';

6
import BigNumber from 'bignumber.js';
7
import {I18n} from '@visualdl/i18n';
8
import {Run} from '~/types';
9
import cloneDeep from 'lodash/cloneDeep';
10
import compact from 'lodash/compact';
11
import maxBy from 'lodash/maxBy';
12
import minBy from 'lodash/minBy';
13 14
import sortBy from 'lodash/sortBy';

15 16
BigNumber.config({EXPONENTIAL_AT: [-6, 7]});

17 18
export * from './types';

19 20 21 22 23 24
export const xAxisMap = {
    step: 1,
    relative: 4,
    wall: 0
};

25 26 27 28 29 30 31 32 33 34 35 36
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])
};

export const transform = ({datasets, smoothing}: TransformParams) =>
    // https://en.wikipedia.org/wiki/Moving_average
    datasets.map(seriesData => {
        const data = cloneDeep(seriesData);
37
        let last = new BigNumber(data.length > 0 ? 0 : Number.NaN);
38 39
        let numAccum = 0;
        let startValue = 0;
40
        const bigSmoothing = new BigNumber(smoothing);
41
        data.forEach((d, i) => {
42
            const nextVal = new BigNumber(d[2]);
43 44 45 46 47
            // second to millisecond.
            const millisecond = (d[0] = Math.floor(d[0] * 1000));
            if (i === 0) {
                startValue = millisecond;
            }
48
            // relative time, millisecond to hours.
49
            d[4] = Math.floor(millisecond - startValue) / (60 * 60 * 1000);
50 51
            if (!nextVal.isFinite()) {
                d[3] = nextVal.toNumber();
52
            } else {
53 54
                // last = last * smoothing + (1 - smoothing) * nextVal;
                last = last.multipliedBy(bigSmoothing).plus(bigSmoothing.minus(1).negated().multipliedBy(nextVal));
55
                numAccum++;
56 57 58 59
                let debiasWeight = new BigNumber(1);
                if (!bigSmoothing.isEqualTo(1)) {
                    //debiasWeight = 1.0 - Math.pow(smoothing, numAccum);
                    debiasWeight = bigSmoothing.exponentiatedBy(numAccum).minus(1).negated();
60
                }
61 62
                // d[3] = last / debiasWeight;
                d[3] = last.dividedBy(debiasWeight).toNumber();
63 64 65 66 67 68 69 70 71 72 73 74 75 76
            }
        });
        return data;
    });

export const chartData = ({data, runs, smooth, xAxis}: ChartDataParams) =>
    data
        .map((dataset, i) => {
            // smoothed data:
            // [0] wall time
            // [1] step
            // [2] orginal value
            // [3] smoothed value
            // [4] relative
77 78 79
            const name = runs[i].label;
            const color = runs[i].colors[0];
            const colorAlt = runs[i].colors[1];
80 81 82 83 84
            return [
                {
                    name,
                    z: i,
                    lineStyle: {
85 86
                        color: colorAlt,
                        width: chart.series.lineStyle.width
87 88 89 90 91 92 93 94 95 96 97
                    },
                    data: dataset,
                    encode: {
                        x: [xAxisMap[xAxis]],
                        y: [2]
                    },
                    smooth
                },
                {
                    name,
                    z: runs.length + i,
98 99 100
                    itemStyle: {
                        color
                    },
101 102 103 104 105 106 107 108 109 110 111
                    data: dataset,
                    encode: {
                        x: [xAxisMap[xAxis]],
                        y: [3]
                    },
                    smooth
                }
            ];
        })
        .flat();

112 113 114 115 116
export const singlePointRange = (value: number) => ({
    min: value ? Math.min(value * 2, 0) : -0.5,
    max: value ? Math.max(value * 2, 0) : 0.5
});

117 118 119 120
export const range = ({datasets, outlier}: RangeParams) => {
    const ranges = compact(
        datasets?.map(dataset => {
            if (dataset.length == 0) return;
121
            const values = dataset.map(v => v[2]);
122 123 124
            if (!outlier) {
                // Get the orgin data range.
                return {
125 126
                    min: Math.min(...values) ?? 0,
                    max: Math.max(...values) ?? 0
127 128 129
                };
            } else {
                // Get the quantile range.
130
                const sorted = dataset.map(v => v[2]).sort();
131
                return {
132 133
                    min: quantile(sorted, 0.05),
                    max: quantile(values, 0.95)
134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149
                };
            }
        })
    );

    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
        };
    }
};

150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176
export const nearestPoint = (data: Dataset[], runs: Run[], step: number) =>
    data.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 || []
        };
    });

177 178 179
// TODO: make it better, don't concat html
export const tooltip = (data: TooltipData[], i18n: I18n) => {
    const indexPropMap = {
180 181 182 183 184 185
        time: 0,
        step: 1,
        value: 2,
        smoothed: 3,
        relative: 4
    } as const;
186
    const widthPropMap = {
187 188 189 190 191 192 193
        run: [60, 180] as [number, number],
        time: 150,
        step: 40,
        value: 60,
        smoothed: 70,
        relative: 60
    } as const;
194
    const translatePropMap = {
195 196 197 198 199 200 201
        run: 'common:runs',
        time: 'scalars:x-axis-value.wall',
        step: 'scalars:x-axis-value.step',
        value: 'scalars:value',
        smoothed: 'scalars:smoothed',
        relative: 'scalars:x-axis-value.relative'
    } as const;
202 203 204
    const transformedData = data.map(item => {
        const data = item.item;
        return {
205
            run: item.run,
206
            // use precision then toString to remove trailling 0
207
            smoothed: new BigNumber(data[indexPropMap.smoothed] ?? Number.NaN).precision(5).toString(),
208
            value: new BigNumber(data[indexPropMap.value] ?? Number.NaN).precision(5).toString(),
209 210
            step: data[indexPropMap.step],
            time: formatTime(data[indexPropMap.time], i18n.language),
211 212
            // Relative display value should take easy-read into consideration.
            // Better to tranform data to 'day:hour', 'hour:minutes', 'minute: seconds' and second only.
213 214
            relative: Math.floor(data[indexPropMap.relative] * 60 * 60) + 's'
        } as const;
215 216
    });

217 218 219 220 221 222 223
    const renderContent = (content: string, width: number | [number, number]) =>
        `<div style="overflow: hidden; ${
            Array.isArray(width)
                ? `min-width:${(width as [number, number])[0]};max-width:${(width as [number, number])[1]};`
                : `width:${width as number}px;`
        }">${content}</div>`;

224 225 226
    let headerHtml = '<tr style="font-size:14px;">';
    headerHtml += (Object.keys(transformedData[0]) as (keyof typeof transformedData[0])[])
        .map(key => {
227 228 229 230
            return `<th style="padding: 0 4px; font-weight: bold;" class="${key}">${renderContent(
                i18n.t(translatePropMap[key]),
                widthPropMap[key]
            )}</th>`;
231 232 233 234 235 236 237 238
        })
        .join('');
    headerHtml += '</tr>';

    const content = transformedData
        .map(item => {
            let str = '<tr style="font-size:12px;">';
            str += Object.keys(item)
239 240 241 242 243 244 245 246 247 248 249 250 251 252
                .map(key => {
                    let content = '';
                    if (key === 'run') {
                        content += `<span class="run-indicator" style="background-color:${
                            item[key].colors?.[0] ?? 'transpanent'
                        }"></span>`;
                        content += `<span title="${item[key].label}">${item[key].label}</span>`;
                    } else {
                        content += item[key as keyof typeof item];
                    }
                    return `<td style="padding: 0 4px;" class="${key}">${renderContent(
                        content,
                        widthPropMap[key as keyof typeof item]
                    )}</td>`;
253 254 255 256 257 258 259
                })
                .join('');
            str += '</tr>';
            return str;
        })
        .join('');

260
    return `<table style="text-align: left;table-layout: fixed;"><thead>${headerHtml}</thead><tbody>${content}</tbody><table>`;
261
};