import * as chart from '~/utils/chart'; import {ChartDataParams, RangeParams, TooltipData, TransformParams, xAxisMap} from './types'; import {formatTime, quantile} from '~/utils'; import BigNumber from 'bignumber.js'; import {I18n} from '@visualdl/i18n'; import cloneDeep from 'lodash/cloneDeep'; import compact from 'lodash/compact'; import maxBy from 'lodash/maxBy'; import minBy from 'lodash/minBy'; import sortBy from 'lodash/sortBy'; BigNumber.config({EXPONENTIAL_AT: [-6, 7]}); export * from './types'; 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); let last = new BigNumber(data.length > 0 ? 0 : Number.NaN); let numAccum = 0; let startValue = 0; const bigSmoothing = new BigNumber(smoothing); data.forEach((d, i) => { const nextVal = new BigNumber(d[2]); // second to millisecond. const millisecond = (d[0] = Math.floor(d[0] * 1000)); if (i === 0) { startValue = millisecond; } // Relative time, millisecond to hours. d[4] = Math.floor(millisecond - startValue) / (60 * 60 * 1000); if (!nextVal.isFinite()) { d[3] = nextVal.toNumber(); } else { // last = last * smoothing + (1 - smoothing) * nextVal; last = last.multipliedBy(bigSmoothing).plus(bigSmoothing.minus(1).negated().multipliedBy(nextVal)); numAccum++; let debiasWeight = new BigNumber(1); if (!bigSmoothing.isEqualTo(1)) { //debiasWeight = 1.0 - Math.pow(smoothing, numAccum); debiasWeight = bigSmoothing.exponentiatedBy(numAccum).minus(1).negated(); } // d[3] = last / debiasWeight; d[3] = last.dividedBy(debiasWeight).toNumber(); } }); 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 const name = runs[i]; const color = chart.color[i % chart.color.length]; const colorAlt = chart.colorAlt[i % chart.colorAlt.length]; return [ { name, z: i, lineStyle: { color: colorAlt, width: chart.series.lineStyle.width }, data: dataset, encode: { x: [xAxisMap[xAxis]], y: [2] }, smooth }, { name, z: runs.length + i, itemStyle: { color }, data: dataset, encode: { x: [xAxisMap[xAxis]], y: [3] }, smooth } ]; }) .flat(); export const singlePointRange = (value: number) => ({ min: value ? Math.min(value * 2, 0) : -0.5, max: value ? Math.max(value * 2, 0) : 0.5 }); export const range = ({datasets, outlier}: RangeParams) => { const ranges = compact( datasets?.map(dataset => { if (dataset.length == 0) return; const values = dataset.map(v => v[2]); if (!outlier) { // Get the orgin data range. return { min: Math.min(...values) ?? 0, max: Math.max(...values) ?? 0 }; } else { // Get the quantile range. const sorted = dataset.map(v => v[2]).sort(); return { min: quantile(sorted, 0.05), max: quantile(values, 0.95) }; } }) ); 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 }; } }; // TODO: make it better, don't concat html export const tooltip = (data: TooltipData[], i18n: I18n) => { const indexPropMap = { Time: 0, Step: 1, Value: 2, Smoothed: 3, Relative: 4 }; const widthPropMap = { Run: 60, Time: 120, Step: 40, Value: 60, Smoothed: 60, Relative: 60 }; const translatePropMap = { 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' }; const transformedData = data.map(item => { const data = item.item; return { Run: item.run, // use precision then toString to remove trailling 0 Smoothed: new BigNumber(data[indexPropMap.Smoothed] ?? Number.NaN).precision(5).toString(), Value: new BigNumber(data[indexPropMap.Smoothed] ?? Number.NaN).precision(5).toString(), Step: data[indexPropMap.Step], Time: formatTime(data[indexPropMap.Time], i18n.language), // Relative display value should take easy-read into consideration. // Better to tranform data to 'day:hour', 'hour:minutes', 'minute: seconds' and second only. Relative: Math.floor(data[indexPropMap.Relative] * 60 * 60) + 's' }; }); let headerHtml = ''; headerHtml += (Object.keys(transformedData[0]) as (keyof typeof transformedData[0])[]) .map(key => { return `${i18n.t( translatePropMap[key] )}`; }) .join(''); headerHtml += ''; const content = transformedData .map(item => { let str = ''; str += Object.keys(item) .map(val => { return `${item[val as keyof typeof item]}`; }) .join(''); str += ''; return str; }) .join(''); // eslint-disable-next-line return `${headerHtml}${content}
`; };