未验证 提交 2631225a 编写于 作者: P Peter Pan 提交者: GitHub

feat(frontend): newly-designed scalar & sample page (#618)

* chore: update dependencies

* fix: wrong language detection in development

* feat: add new icons for future features

* feat: add refresh and language changing

* feat: remove scalar x-axis label

* feat: add scalar chart toolbox

* fix: move const out of type defination

* feat: maximize scalar chart

* feat: chart toolbox tooltip

* build: update dependencies

* feat: new scalar chart page

* pref: better component reuse in chart page

* fix: re-design of button component

* feat: new scalar side bar

* style: fix lint

* fix: remove samples page

* fix: runs color mismatch

* build: update dependencies

* feat: newly-designed image samples page

* feat: add run indicator in scalar chart tooltip

* feat: add empty tip when nothing selected in scalar and sample page
上级 2241c370
......@@ -33,6 +33,7 @@ module.exports = {
extends: [
'plugin:@typescript-eslint/recommended',
'plugin:react/recommended',
'plugin:react-hooks/recommended',
'prettier/@typescript-eslint',
'plugin:prettier/recommended'
],
......@@ -44,7 +45,6 @@ module.exports = {
ecmaVersion: 2018,
sourceType: 'module'
},
plugins: ['react-hooks'],
settings: {
react: {
version: 'detect'
......@@ -54,9 +54,7 @@ module.exports = {
'@typescript-eslint/explicit-function-return-type': 'off',
'@typescript-eslint/no-explicit-any': 'error',
'react/prop-types': 'off',
'react/react-in-jsx-scope': 'off',
'react-hooks/rules-of-hooks': 'error',
'react-hooks/exhaustive-deps': 'warn'
'react/react-in-jsx-scope': 'off'
}
}
]
......
......@@ -3,19 +3,24 @@ const fs = require('fs');
module.exports = {
// lint all files when global package.json or eslint config changes.
'./(package.json|.eslintrc.js)': () => 'yarn lint',
'./(package.json|.eslintrc.js)': () =>
`eslint --ext .tsx,.jsx.ts,.js --ignore-path ${path.join(__dirname, '.gitignore')} ${__dirname}`,
// check types when ts file or package.json changes.
'packages/**/(*.ts?(x)|package.json)': filenames =>
'./(packages/*/package.json|packages/*/**/*.ts?(x))': filenames =>
[
...new Set(
filenames.map(
filename => path.relative(path.join(process.cwd(), 'packages'), filename).split(path.sep)[0]
)
filenames.map(filename => path.relative(path.join(__dirname, 'packages'), filename).split(path.sep)[0])
)
]
.map(p => path.join(process.cwd(), 'packages', p, 'tsconfig.json'))
.filter(p => fs.statSync(p).isFile())
.map(p => path.join(__dirname, 'packages', p, 'tsconfig.json'))
.filter(p => {
try {
return fs.statSync(p).isFile();
} catch (e) {
return false;
}
})
.map(p => `tsc -p ${p} --noEmit`),
// lint changed files
......
......@@ -38,17 +38,17 @@
"version": "yarn format && git add -A"
},
"devDependencies": {
"@typescript-eslint/eslint-plugin": "2.27.0",
"@typescript-eslint/parser": "2.27.0",
"@typescript-eslint/eslint-plugin": "2.31.0",
"@typescript-eslint/parser": "2.31.0",
"eslint": "6.8.0",
"eslint-config-prettier": "6.10.1",
"eslint-plugin-prettier": "3.1.2",
"eslint-config-prettier": "6.11.0",
"eslint-plugin-prettier": "3.1.3",
"eslint-plugin-react": "7.19.0",
"eslint-plugin-react-hooks": "3.0.0",
"eslint-plugin-react-hooks": "4.0.0",
"husky": "4.2.5",
"lerna": "^3.20.2",
"lint-staged": "10.1.3",
"prettier": "2.0.4",
"lerna": "3.20.2",
"lint-staged": "10.2.2",
"prettier": "2.0.5",
"rimraf": "3.0.2",
"typescript": "3.8.3",
"yarn": "1.22.4"
......
......@@ -30,11 +30,11 @@
},
"dependencies": {
"@visualdl/server": "^2.0.0-beta.32",
"pm2": "4.2.3"
"pm2": "4.4.0"
},
"devDependencies": {
"electron": "8.2.1",
"electron-builder": "22.4.1"
"electron": "8.2.5",
"electron-builder": "22.6.0"
},
"engines": {
"node": ">=10",
......
......@@ -36,15 +36,15 @@
"dependencies": {
"@visualdl/server": "^2.0.0-beta.32",
"open": "7.0.3",
"ora": "4.0.3",
"pm2": "4.2.3",
"ora": "4.0.4",
"pm2": "4.4.0",
"yargs": "15.3.1"
},
"devDependencies": {
"@types/node": "13.11.1",
"@types/node": "13.13.5",
"@types/yargs": "15.0.4",
"cross-env": "7.0.2",
"ts-node": "8.8.2",
"ts-node": "8.10.1",
"typescript": "3.8.3"
},
"engines": {
......
import React, {FunctionComponent} from 'react';
import {
WithStyled,
borderActiveColor,
borderColor,
borderFocusedColor,
borderRadius,
dangerActiveColor,
dangerColor,
dangerFocusedColor,
......@@ -10,7 +14,10 @@ import {
primaryActiveColor,
primaryColor,
primaryFocusedColor,
sameBorder,
textColor,
textInvertColor,
textLighterColor,
transitionProps
} from '~/utils/style';
......@@ -31,26 +38,36 @@ const colors = {
}
};
const Wrapper = styled.a<{type: keyof typeof colors}>`
const Wrapper = styled.a<{type?: keyof typeof colors; rounded?: boolean; disabled?: boolean}>`
cursor: pointer;
height: ${height};
line-height: ${height};
border-radius: ${half(height)};
background-color: ${props => colors[props.type].default};
color: ${textInvertColor};
display: block;
border-radius: ${props => (props.rounded ? half(height) : borderRadius)};
${props => (props.type ? '' : sameBorder({color: borderColor}))}
background-color: ${props => (props.type ? colors[props.type].default : 'transparent')};
color: ${props => (props.disabled ? textLighterColor : props.type ? textInvertColor : textColor)};
cursor: ${props => (props.disabled ? 'not-allowed' : 'cursor')};
display: inline-block;
vertical-align: top;
text-align: center;
${transitionProps('background-color')}
padding: 0 ${em(20)};
${transitionProps(['background-color', 'border-color'])}
${ellipsis()}
${props =>
props.disabled
? ''
: `
&:hover,
&:focus {
background-color: ${props => colors[props.type].focused};
${props.type ? '' : sameBorder({color: borderFocusedColor})}
background-color: ${props.type ? colors[props.type].focused : 'transparent'};
}
&:active {
background-color: ${props => colors[props.type].active};
}
${props.type ? '' : sameBorder({color: borderActiveColor})}
background-color: ${props.type ? colors[props.type].active : 'transparent'};
}`}
`;
const Icon = styled(RawIcon)`
......@@ -58,13 +75,23 @@ const Icon = styled(RawIcon)`
`;
type ButtonProps = {
rounded?: boolean;
icon?: string;
type?: keyof typeof colors;
disabled?: boolean;
onClick?: () => unknown;
};
const Button: FunctionComponent<ButtonProps & WithStyled> = ({icon, type, children, className, onClick}) => (
<Wrapper className={className} onClick={onClick} type={type || 'primary'}>
const Button: FunctionComponent<ButtonProps & WithStyled> = ({
disabled,
rounded,
icon,
type,
children,
className,
onClick
}) => (
<Wrapper className={className} onClick={onClick} type={type} rounded={rounded} disabled={disabled}>
{icon && <Icon type={icon}></Icon>}
{children}
</Wrapper>
......
import React, {FunctionComponent} from 'react';
import React, {FunctionComponent, useCallback, useEffect, useState} from 'react';
import {
WithStyled,
backgroundColor,
borderRadius,
headerHeight,
math,
primaryColor,
rem,
sameBorder,
size,
transitionProps
} from '~/utils/style';
import ee from '~/utils/event';
import styled from 'styled-components';
const Div = styled.div`
const Div = styled.div<{maximized?: boolean; width?: string; height?: string}>`
${props =>
size(
props.maximized ? `calc(100vh - ${headerHeight} - ${rem(40)})` : props.height || 'auto',
props.maximized ? '100%' : props.width || '100%'
)}
background-color: ${backgroundColor};
${sameBorder({radius: math(`${borderRadius} * 2`)})}
${transitionProps(['border-color', 'box-shadow'])}
position: relative;
&:hover {
border-color: ${primaryColor};
......@@ -22,8 +32,34 @@ const Div = styled.div`
}
`;
const Chart: FunctionComponent<WithStyled> = ({className, children}) => {
return <Div className={className}>{children}</Div>;
type ChartProps = {
cid: symbol;
width?: string;
height?: string;
};
const Chart: FunctionComponent<ChartProps & WithStyled> = ({cid, width, height, className, children}) => {
const [maximized, setMaximized] = useState(false);
const toggleMaximze = useCallback(
(id: symbol, value: boolean) => {
if (id === cid) {
setMaximized(value);
}
},
[cid]
);
useEffect(() => {
ee.on('toggle-chart-size', toggleMaximze);
return () => {
ee.off('toggle-chart-size', toggleMaximze);
};
}, [toggleMaximze]);
return (
<Div maximized={maximized} width={width} height={height} className={className}>
{children}
</Div>
);
};
export default Chart;
import React, {FunctionComponent, useState} from 'react';
import {
backgroundColor,
borderRadius,
em,
rem,
size,
textColor,
textLighterColor,
transitionProps
} from '~/utils/style';
import Icon from '~/components/Icon';
import styled from 'styled-components';
const Wrapper = styled.div`
background-color: ${backgroundColor};
border-radius: ${borderRadius};
& + & {
margin-top: ${rem(4)};
}
`;
const Header = styled.div`
height: ${em(40)};
display: flex;
justify-content: space-between;
align-items: center;
padding: 0 ${em(20)};
color: ${textLighterColor};
cursor: pointer;
> h3 {
color: ${textColor};
flex-grow: 1;
margin: 0;
font-weight: 700;
}
> .total {
margin-right: ${em(20)};
}
`;
const Content = styled.div`
border-top: 1px solid #eee;
padding: ${rem(20)};
`;
const CollapseIcon = styled(Icon)<{opened?: boolean}>`
${size(em(14))}
display: block;
flex-shrink: 0;
transform: rotate(${props => (props.opened ? '180' : '0')}deg) scale(${10 / 14});
${transitionProps('transform')}
`;
type ChartCollapseProps = {
title: string;
opened?: boolean;
total?: number;
};
const ChartCollapse: FunctionComponent<ChartCollapseProps> = ({opened, title, total, children}) => {
const [isOpened, setOpened] = useState(opened !== false);
return (
<Wrapper>
<Header onClick={() => setOpened(o => !o)}>
<h3>{title}</h3>
{total != null ? <span className="total">{total}</span> : null}
<CollapseIcon type="chevron-down" opened={isOpened} />
</Header>
{isOpened ? <Content>{children}</Content> : null}
</Wrapper>
);
};
export default ChartCollapse;
import React, {FunctionComponent, useMemo, useState} from 'react';
import {WithStyled, primaryColor, rem} from '~/utils/style';
import React, {FunctionComponent, PropsWithChildren, useCallback, useEffect, useMemo, useState} from 'react';
import {Trans, useTranslation} from '~/utils/i18n';
import {WithStyled, backgroundColor, headerHeight, link, primaryColor, rem, textLighterColor} from '~/utils/style';
import BarLoader from 'react-spinners/BarLoader';
import Chart from '~/components/Chart';
import ChartCollapse from '~/components/ChartCollapse';
import Pagination from '~/components/Pagination';
import SearchInput from '~/components/SearchInput';
import groupBy from 'lodash/groupBy';
import styled from 'styled-components';
import {useTranslation} from '~/utils/i18n';
import useSearchValue from '~/hooks/useSearchValue';
const StyledPagination = styled(Pagination)`
margin-top: ${rem(20)};
`;
const Wrapper = styled.div`
display: flex;
......@@ -13,6 +21,7 @@ const Wrapper = styled.div`
justify-content: flex-start;
align-items: stretch;
align-content: flex-start;
margin-bottom: -${rem(20)};
> * {
margin: 0 ${rem(20)} ${rem(20)} 0;
......@@ -21,6 +30,11 @@ const Wrapper = styled.div`
}
`;
const Search = styled.div`
width: ${rem(280)};
margin-bottom: ${rem(16)};
`;
const Loading = styled.div`
display: flex;
justify-content: center;
......@@ -29,49 +43,168 @@ const Loading = styled.div`
padding: ${rem(40)} 0;
`;
const Empty = styled.div`
display: flex;
justify-content: center;
align-items: center;
font-size: ${rem(20)};
height: ${rem(150)};
flex-grow: 1;
const Empty = styled.div<{height?: string}>`
width: 100%;
text-align: center;
font-size: ${rem(16)};
color: ${textLighterColor};
line-height: ${rem(24)};
height: ${props => props.height ?? 'auto'};
padding: ${rem(320)} 0 ${rem(70)};
background-color: ${backgroundColor};
background-image: url(${`${process.env.PUBLIC_PATH}/images/empty.svg`});
background-repeat: no-repeat;
background-position: calc(50% + ${rem(25)}) ${rem(70)};
background-size: ${rem(280)} ${rem(244)};
${link}
`;
// TODO: add types
// eslint-disable-next-line @typescript-eslint/no-explicit-any
type ChartPageProps<T = any> = {
type Item = {
id?: string;
label: string;
};
export interface WithChart<T extends Item> {
(item: T & {cid: symbol}, index: number): React.ReactNode;
}
type ChartPageProps<T extends Item> = {
items?: T[];
running?: boolean;
loading?: boolean;
withChart?: (item: T) => React.ReactNode;
chartSize?: {
width?: string;
height?: string;
};
withChart?: WithChart<T>;
};
const ChartPage: FunctionComponent<ChartPageProps & WithStyled> = ({items, loading, withChart, className}) => {
const ChartPage = <T extends Item>({
items,
loading,
chartSize,
withChart,
className
}: PropsWithChildren<ChartPageProps<T> & WithStyled>): ReturnType<FunctionComponent> => {
const {t} = useTranslation('common');
const [page, setPage] = useState(1);
const pageSize = 12;
const total = Math.ceil((items?.length ?? 0) / pageSize);
const [page, setPage] = useState(1);
const [inputValue, setInputValue] = useState('');
const searchValue = useSearchValue(inputValue);
const pageItems = useMemo(() => items?.slice((page - 1) * pageSize, page * pageSize) ?? [], [items, page]);
const matchedTags = useMemo(() => {
try {
const pattern = new RegExp(searchValue);
return items?.filter(tag => pattern.test(tag.label)) ?? [];
} catch {
return [];
}
}, [items, searchValue]);
return (
<div className={className}>
{loading ? (
const pageMatchedTags = useMemo(() => matchedTags?.slice((page - 1) * pageSize, page * pageSize) ?? [], [
matchedTags,
page
]);
useEffect(() => {
setPage(1);
}, [searchValue]);
const groupedItems = useMemo(
() =>
Object.entries(groupBy<T>(items ?? [], item => item.label.split('/')[0])).sort(([a], [b]) => {
const ua = a.toUpperCase();
const ub = b.toUpperCase();
if (ua < ub) {
return -1;
}
if (ua > ub) {
return 1;
}
return 0;
}),
[items]
);
const total = useMemo(() => Math.ceil(matchedTags.length / pageSize), [matchedTags]);
const withCharts = useCallback(
(charts: T[], search?: boolean) =>
loading ? (
<Loading>
<BarLoader color={primaryColor} width="20%" height="4px" />
</Loading>
) : (
<Wrapper>
{pageItems.length ? (
pageItems.map((item, index) => <Chart key={index}>{withChart?.(item)}</Chart>)
{charts.length ? (
charts.map((item, j) => {
const cid = Symbol(item.label);
return (
<Chart
cid={cid}
key={item.id || item.label}
width={chartSize?.width ?? rem(430)}
height={chartSize?.height ?? rem(337)}
>
{withChart?.({...item, cid}, j)}
</Chart>
);
})
) : (
<Empty>{t('empty')}</Empty>
<Empty height={rem(500)}>
{search ? (
<Trans i18nKey="search-empty">
Nothing found. Please try again with another word.
<br />
Or you can <a onClick={() => setInputValue('')}>see all charts</a>.
</Trans>
) : (
t('empty')
)}
</Empty>
)}
</Wrapper>
),
[withChart, loading, chartSize, t]
);
return (
<div className={className}>
<Search>
<SearchInput
placeholder={t('search-tags')}
rounded
value={inputValue}
onChange={(value: string) => setInputValue(value)}
/>
</Search>
{searchValue ? (
<ChartCollapse title={t('search-result')} total={matchedTags.length}>
{withCharts(pageMatchedTags, true)}
{pageMatchedTags.length ? <StyledPagination page={page} total={total} onChange={setPage} /> : null}
</ChartCollapse>
) : groupedItems.length ? (
groupedItems.map((groupedItem, i) => (
<ChartCollapse
title={groupedItem[0]}
key={groupedItem[0]}
total={groupedItem[1].length}
opened={i === 0}
>
{withCharts(groupedItem[1])}
</ChartCollapse>
))
) : (
<Empty height={`calc(100vh - ${headerHeight} - ${rem(96)})`}>
<Trans i18nKey="unselected-empty">
Nothing selected.
<br />
Please select display data from right side.
</Trans>
</Empty>
)}
<Pagination page={page} total={total} onChange={setPage} />
</div>
);
};
......
import React, {FunctionComponent, useCallback, useState} from 'react';
import {
WithStyled,
em,
primaryActiveColor,
primaryColor,
primaryFocusedColor,
rem,
textColor,
textLightColor,
textLighterColor,
tooltipBackgroundColor,
tooltipTextColor,
transitionProps
} from '~/utils/style';
import Icon from '~/components/Icon';
import ReactTooltip from 'react-tooltip';
import {nanoid} from 'nanoid';
import styled from 'styled-components';
const Toolbox = styled.div`
font-size: ${em(16)};
height: 1em;
line-height: 1;
margin-bottom: ${rem(18)};
display: flex;
`;
const ToolboxItem = styled.a<{active?: boolean}>`
cursor: pointer;
color: ${props => (props.active ? primaryColor : textLighterColor)};
${transitionProps('color')}
&:hover {
color: ${props => (props.active ? primaryFocusedColor : textLightColor)};
}
&:active {
color: ${props => (props.active ? primaryActiveColor : textColor)};
}
& + & {
margin-left: ${rem(14)};
}
`;
type BaseChartToolboxItem = {
icon: string;
tooltip?: string;
};
type NormalChartToolboxItem = {
toggle?: false;
onClick?: () => unknown;
} & BaseChartToolboxItem;
type ToggleChartToolboxItem = {
toggle: true;
activeIcon?: string;
activeTooltip?: string;
onClick?: (value: boolean) => unknown;
} & BaseChartToolboxItem;
export type ChartTooboxItem = NormalChartToolboxItem | ToggleChartToolboxItem;
type ChartToolboxProps = {
cid?: string;
items: ChartTooboxItem[];
};
const ChartToolbox: FunctionComponent<ChartToolboxProps & WithStyled> = ({cid, items, className}) => {
const [activeStatus, setActiveStatus] = useState<boolean[]>(new Array(items.length).fill(false));
const onClick = useCallback(
(index: number) => {
const item = items[index];
if (item.toggle) {
item.onClick?.(!activeStatus[index]);
setActiveStatus(m => {
const n = [...m];
n.splice(index, 1, !m[index]);
return n;
});
} else {
item.onClick?.();
}
},
[items, activeStatus]
);
const [id] = useState(`chart-toolbox-tooltip-${cid || nanoid()}`);
return (
<>
<Toolbox className={className}>
{items.map((item, index) => (
<ToolboxItem
key={index}
active={item.toggle && !item.activeIcon && activeStatus[index]}
onClick={() => onClick(index)}
data-for={item.tooltip ? id : null}
data-tip={
item.tooltip
? item.toggle
? (activeStatus[index] && item.activeTooltip) || item.tooltip
: item.tooltip
: null
}
>
<Icon type={item.toggle ? (activeStatus[index] && item.activeIcon) || item.icon : item.icon} />
</ToolboxItem>
))}
</Toolbox>
<ReactTooltip
id={id}
place="top"
textColor={tooltipTextColor}
backgroundColor={tooltipBackgroundColor}
effect="solid"
/>
</>
);
};
export default ChartToolbox;
......@@ -47,7 +47,7 @@ const Inner = styled.div<{checked?: boolean; size?: string; disabled?: boolean}>
flex-shrink: 0;
${props => size(math(`${checkSize} * ${props.size === 'small' ? 0.875 : 1}`))}
margin: ${half(`${height} - ${checkSize}`)} 0;
margin-right: ${em(4)};
margin-right: ${em(10)};
${props => sameBorder({color: props.disabled || !props.checked ? textLighterColor : primaryColor})};
background-color: ${props =>
props.disabled
......
......@@ -5,7 +5,6 @@ import HashLoader from 'react-spinners/HashLoader';
import styled from 'styled-components';
const margin = rem(20);
const padding = rem(20);
const Section = styled.section`
/* trigger BFC */
......@@ -15,13 +14,10 @@ const Section = styled.section`
const Article = styled.article<{aside?: boolean}>`
margin: ${margin};
margin-right: ${props => (props.aside ? math(`${margin} + ${asideWidth}`) : margin)};
padding: ${padding};
background-color: ${backgroundColor};
min-height: calc(100vh - ${math(`${margin} * 2 + ${headerHeight}`)});
`;
const Aside = styled.aside`
padding: ${padding};
background-color: ${backgroundColor};
${size(`calc(100vh - ${headerHeight})`, asideWidth)}
${position('fixed', headerHeight, 0, null, null)}
......
import React, {FunctionComponent, useLayoutEffect, useState} from 'react';
import {BlobResponse, blobFetcher} from '~/utils/fetch';
import React, {useImperativeHandle, useLayoutEffect, useState} from 'react';
import GridLoader from 'react-spinners/GridLoader';
import {blobFetcher} from '~/utils/fetch';
import mime from 'mime-types';
import {primaryColor} from '~/utils/style';
import {saveAs} from 'file-saver';
import useRequest from '~/hooks/useRequest';
import {useTranslation} from '~/utils/i18n';
export type ImageRef = {
save(filename: string): void;
};
type ImageProps = {
src?: string;
cache?: number;
};
const Image: FunctionComponent<ImageProps> = ({src, cache}) => {
const Image = React.forwardRef<ImageRef, ImageProps>(({src, cache}, ref) => {
const {t} = useTranslation('common');
const [url, setUrl] = useState('');
const {data, error, loading} = useRequest<Blob>(src ?? null, blobFetcher, {
const {data, error, loading} = useRequest<BlobResponse>(src ?? null, blobFetcher, {
dedupingInterval: cache ?? 2000
});
useImperativeHandle(ref, () => ({
save: (filename: string) => {
if (data) {
const ext = data.type ? mime.extension(data.type) : null;
saveAs(data.data, filename.replace(/[/\\?%*:|"<>]/g, '_') + (ext ? `.${ext}` : ''));
}
}
}));
// use useLayoutEffect hook to prevent image render after url revoked
useLayoutEffect(() => {
if (process.browser && data) {
let objectUrl: string | null = null;
objectUrl = URL.createObjectURL(data);
objectUrl = URL.createObjectURL(data.data);
setUrl(objectUrl);
return () => {
objectUrl && URL.revokeObjectURL(objectUrl);
......@@ -41,6 +56,6 @@ const Image: FunctionComponent<ImageProps> = ({src, cache}) => {
}
return <img src={url} />;
};
});
export default Image;
......@@ -32,7 +32,9 @@ export type InputProps = {
onChange?: (value: string) => unknown;
};
const Input: FunctionComponent<InputProps & WithStyled> = ({rounded, placeholder, value, onChange, className}) => (
const Input: FunctionComponent<
InputProps & WithStyled & Omit<React.ComponentPropsWithoutRef<'input'>, keyof InputProps>
> = ({rounded, placeholder, value, onChange, className, ...props}) => (
<StyledInput
rounded={rounded}
placeholder={placeholder}
......@@ -40,7 +42,8 @@ const Input: FunctionComponent<InputProps & WithStyled> = ({rounded, placeholder
type="text"
className={className}
onChange={e => onChange?.(e.target.value)}
></StyledInput>
{...props}
/>
);
export default Input;
import React, {FunctionComponent} from 'react';
import {rem, size} from '~/utils/style';
import styled from 'styled-components';
import {useTranslation} from '~/utils/i18n';
const LANGUAGE_FLAGS = [
['zh', ''],
['en', 'En']
];
const Item = styled.span<{active: boolean}>`
display: inline-block;
color: currentColor;
opacity: ${props => (props.active ? 1 : 0.29)};
`;
const Divider = styled.span`
display: inline-block;
margin: 0 ${rem(5)};
${size('1em', '1px')}
background-color: currentColor;
`;
const Language: FunctionComponent = () => {
const {i18n} = useTranslation();
return (
<>
{LANGUAGE_FLAGS.map(([l, f], i) => (
<React.Fragment key={f}>
{i !== 0 && <Divider />}
<Item active={l === i18n.language}>{f}</Item>
</React.Fragment>
))}
</>
);
};
export default Language;
import * as chart from '~/utils/chart';
import React, {FunctionComponent, useCallback, useEffect} from 'react';
import React, {useCallback, useEffect, useImperativeHandle, useLayoutEffect, useRef} from 'react';
import {WithStyled, position, primaryColor, size} from '~/utils/style';
import {EChartOption} from 'echarts';
import GridLoader from 'react-spinners/GridLoader';
import {dataURL2Blob} from '~/utils/image';
import {formatTime} from '~/utils';
import {saveAs} from 'file-saver';
import styled from 'styled-components';
import useECharts from '~/hooks/useECharts';
import {useTranslation} from '~/utils/i18n';
......@@ -37,36 +39,47 @@ type LineChartProps = {
data?: Partial<NonNullable<EChartOption<EChartOption.SeriesLine>['series']>>;
xAxis?: string;
yAxis?: string;
type?: EChartOption.BasicComponents.CartesianAxis.Type;
xType?: EChartOption.BasicComponents.CartesianAxis.Type;
yType?: EChartOption.BasicComponents.CartesianAxis.Type;
xRange?: Range;
yRange?: Range;
tooltip?: string | EChartOption.Tooltip.Formatter;
loading?: boolean;
};
const LineChart: FunctionComponent<LineChartProps & WithStyled> = ({
title,
legend,
data,
xAxis,
yAxis,
type,
xRange,
yRange,
tooltip,
loading,
className
}) => {
export type LineChartRef = {
restore(): void;
saveAsImage(): void;
};
const LineChart = React.forwardRef<LineChartRef, LineChartProps & WithStyled>(
({title, legend, data, xAxis, yAxis, xType, yType, xRange, yRange, tooltip, loading, className}, ref) => {
const {i18n} = useTranslation();
const {ref, echart} = useECharts<HTMLDivElement>({
const {ref: echartRef, echart} = useECharts<HTMLDivElement>({
loading: !!loading,
zoom: true
});
useImperativeHandle(ref, () => ({
restore: () => {
echart?.current?.dispatchAction({
type: 'restore'
});
},
saveAsImage: () => {
if (echart?.current) {
const blob = dataURL2Blob(
echart.current.getDataURL({type: 'png', pixelRatio: 2, backgroundColor: '#FFF'})
);
saveAs(blob, `${title?.replace(/[/\\?%*:|"<>]/g, '_') || 'scalar'}.png`);
}
}
}));
const xAxisFormatter = useCallback(
(value: number) => (type === 'time' ? formatTime(value, i18n.language, 'LTS') : value),
[type, i18n.language]
(value: number) => (xType === 'time' ? formatTime(value, i18n.language, 'LTS') : value),
[xType, i18n.language]
);
useEffect(() => {
......@@ -95,7 +108,7 @@ const LineChart: FunctionComponent<LineChartProps & WithStyled> = ({
xAxis: {
...chart.xAxis,
name: xAxis || '',
type: type || 'value',
type: xType || 'value',
axisLabel: {
...chart.xAxis.axisLabel,
formatter: xAxisFormatter
......@@ -105,6 +118,7 @@ const LineChart: FunctionComponent<LineChartProps & WithStyled> = ({
yAxis: {
...chart.yAxis,
name: yAxis || '',
type: yType || 'value',
...(yRange || {})
},
series: data?.map(item => ({
......@@ -117,18 +131,33 @@ const LineChart: FunctionComponent<LineChartProps & WithStyled> = ({
{notMerge: true}
);
}
}, [data, title, legend, xAxis, yAxis, type, xAxisFormatter, xRange, yRange, tooltip, echart]);
}, [data, title, legend, xAxis, yAxis, xType, yType, xAxisFormatter, xRange, yRange, tooltip, echart]);
const wrapperRef = useRef<HTMLDivElement>(null);
useLayoutEffect(() => {
if (process.browser) {
const wrapper = wrapperRef.current;
if (wrapper) {
const observer = new ResizeObserver(() => {
echart?.current?.resize();
});
observer.observe(wrapper);
return () => observer.unobserve(wrapper);
}
}
});
return (
<Wrapper className={className}>
<Wrapper ref={wrapperRef} className={className}>
{!echart && (
<div className="loading">
<GridLoader color={primaryColor} size="10px" />
</div>
)}
<div className="echarts" ref={ref}></div>
<div className="echarts" ref={echartRef}></div>
</Wrapper>
);
};
}
);
export default LineChart;
import {Link, useTranslation} from '~/utils/i18n';
import {Link, config, i18n, useTranslation} from '~/utils/i18n';
import React, {FunctionComponent} from 'react';
import {
border,
......@@ -11,6 +11,9 @@ import {
transitionProps
} from '~/utils/style';
import Icon from '~/components/Icon';
import Language from '~/components/Language';
import ee from '~/utils/event';
import intersection from 'lodash/intersection';
import styled from 'styled-components';
import {useRouter} from 'next/router';
......@@ -29,9 +32,22 @@ const Nav = styled.nav`
color: ${textInvertColor};
${size('100%')}
padding: 0 ${rem(20)};
display: flex;
justify-content: space-between;
align-items: stretch;
> .left {
display: flex;
justify-content: flex-start;
align-items: center;
}
> .right {
display: flex;
justify-content: flex-end;
align-items: center;
margin-right: -${rem(20)};
}
`;
const Logo = styled.a`
......@@ -52,20 +68,21 @@ const Logo = styled.a`
}
`;
const NavItem = styled.a<{active: boolean}>`
const NavItem = styled.a<{active?: boolean}>`
padding: 0 ${rem(20)};
height: 100%;
display: inline-flex;
justify-content: center;
align-items: center;
background-color: ${navbarBackgroundColor};
cursor: pointer;
${transitionProps('background-color')}
&:hover {
background-color: ${navbarHoverBackgroundColor};
}
> span {
> .nav-text {
padding: ${rem(10)} 0 ${rem(7)};
${props => border('bottom', rem(3), 'solid', props.active ? navbarHighlightColor : 'transparent')}
${transitionProps('border-bottom')}
......@@ -73,12 +90,21 @@ const NavItem = styled.a<{active: boolean}>`
}
`;
const changeLanguage = () => {
const {language} = i18n;
const {allLanguages} = config;
const index = allLanguages.indexOf(language);
const nextLanguage = index < 0 || index >= allLanguages.length - 1 ? allLanguages[0] : allLanguages[index + 1];
i18n.changeLanguage(nextLanguage);
};
const Navbar: FunctionComponent = () => {
const {t} = useTranslation('common');
const {pathname} = useRouter();
return (
<Nav>
<div className="left">
<Logo href={process.env.PUBLIC_PATH || '/'}>
<img alt="PaddlePaddle" src={`${process.env.PUBLIC_PATH}/images/logo.svg`} />
<span>VisualDL</span>
......@@ -89,11 +115,20 @@ const Navbar: FunctionComponent = () => {
// https://nextjs.org/docs/api-reference/next/link#if-the-child-is-a-custom-component-that-wraps-an-a-tag
<Link href={href} key={name} passHref>
<NavItem active={pathname === href}>
<span>{t(name)}</span>
<span className="nav-text">{t(name)}</span>
</NavItem>
</Link>
);
})}
</div>
<div className="right">
<NavItem onClick={changeLanguage}>
<Language />
</NavItem>
<NavItem onClick={() => ee.emit('refresh-running')}>
<Icon type="refresh" />
</NavItem>
</div>
</Nav>
);
};
......
// cSpell:words hellip
import React, {FunctionComponent, useCallback, useMemo} from 'react';
import {
WithStyled,
backgroundColor,
borderColor,
borderFocusedColor,
em,
primaryColor,
sameBorder,
size,
textColor,
textInvertColor,
transitionProps
} from '~/utils/style';
import React, {FunctionComponent, useCallback, useEffect, useState} from 'react';
import {WithStyled, em} from '~/utils/style';
import Button from '~/components/Button';
import Input from '~/components/Input';
import styled from 'styled-components';
const height = em(36);
import {useTranslation} from '~/utils/i18n';
const Wrapper = styled.nav`
display: flex;
user-select: none;
`;
const Ul = styled.ul`
display: inline-flex;
list-style: none;
margin: 0;
padding: 0;
`;
const Li = styled.li`
list-style: none;
margin-left: ${em(10)};
justify-content: space-between;
align-items: center;
&:first-child {
margin-left: 0;
> div {
> a:not(:last-child),
> span {
margin-right: ${em(15)};
}
> input {
width: ${em(80)};
margin-right: ${em(6)};
}
`;
const A = styled.a<{current?: boolean}>`
cursor: pointer;
display: block;
background-color: ${props => (props.current ? primaryColor : backgroundColor)};
color: ${props => (props.current ? textInvertColor : textColor)};
height: ${height};
line-height: calc(${height} - 2px);
min-width: ${height};
padding: 0 ${em(10)};
text-align: center;
${props => sameBorder({color: props.current ? primaryColor : borderColor, radius: true})};
${transitionProps(['color', 'border-color', 'background-color'])}
&:hover {
border-color: ${props => (props.current ? primaryColor : borderFocusedColor)};
}
`;
const Span = styled.span`
display: block;
${size(height)}
line-height: ${height};
text-align: center;
`;
type PaginationProps = {
page: number;
page?: number;
total: number;
onChange?: (page: number) => unknown;
};
const Pagination: FunctionComponent<PaginationProps & WithStyled> = ({page, total, className, onChange}) => {
const padding = 2;
const around = 2;
const {t} = useTranslation('common');
const startEllipsis = useMemo(() => page - padding - around - 1 > 0, [page]);
const endEllipsis = useMemo(() => page + padding + around < total, [page, total]);
const start = useMemo(
() =>
page - around - 1 <= 0 ? [] : Array.from(new Array(Math.min(padding, page - around - 1)), (_v, i) => i + 1),
[page]
);
const end = useMemo(
() =>
page + around >= total
? []
: Array.from(
new Array(Math.min(padding, total - page - around)),
(_v, i) => total - padding + i + 1 + Math.max(padding - total + page + around, 0)
),
[page, total]
);
const before = useMemo(
() =>
page - 1 <= 0
? []
: Array.from(
new Array(Math.min(around, page - 1)),
(_v, i) => page - around + i + Math.max(around - page + 1, 0)
),
[page]
);
const after = useMemo(
() => (page >= total ? [] : Array.from(new Array(Math.min(around, total - page)), (_v, i) => page + i + 1)),
[page, total]
);
const [currentPage, setCurrentPage] = useState(page ?? 1);
const [jumpPage, setJumpPage] = useState('');
const genLink = useCallback(
(arr: number[]) =>
arr.map(i => (
<Li key={i}>
<A onClick={() => onChange?.(i)}>{i}</A>
</Li>
)),
[onChange]
);
useEffect(() => setCurrentPage(page ?? 1), [page]);
const hellip = (
<Li>
<Span>&hellip;</Span>
</Li>
const setPage = useCallback(
(value: unknown) => {
const p = 'number' === typeof value ? value : Number.parseInt(value + '');
if (Number.isNaN(p) || p > total || p < 1 || p === currentPage) {
return;
}
setCurrentPage(p);
setJumpPage('');
onChange?.(p);
},
[currentPage, onChange, total]
);
return (
<Wrapper className={className}>
<Ul>
{genLink(start)}
{startEllipsis && hellip}
{genLink(before)}
<Li>
<A current>{page}</A>
</Li>
{genLink(after)}
{endEllipsis && hellip}
{genLink(end)}
</Ul>
<div>
<Button disabled={currentPage <= 1} onClick={() => setPage(currentPage - 1)}>
{t('previous-page')}
</Button>
<Button disabled={currentPage >= total} onClick={() => setPage(currentPage + 1)}>
{t('next-page')}
</Button>
</div>
<div>
<span>{t('total-page', {count: total})}</span>
<Input
value={jumpPage}
onChange={value => setJumpPage(value)}
onKeyDown={e => e.key === 'Enter' && setPage(jumpPage)}
/>
<Button onClick={() => setPage(jumpPage)} type="primary">
{t('confirm')}
</Button>
</div>
</Wrapper>
);
};
......
import {EventContext, ValueContext} from '~/components/RadioGroup';
import React, {FunctionComponent, useCallback, useContext} from 'react';
import React, {FunctionComponent, PropsWithChildren, useCallback, useContext} from 'react';
import {
WithStyled,
backgroundColor,
......@@ -29,6 +29,7 @@ const Button = styled.a<{selected?: boolean}>`
height: ${height};
line-height: calc(${height} - 2px);
min-width: ${minWidth};
padding: 0 ${em(8)};
text-align: center;
${ellipsis(maxWidth)}
${props => sameBorder({color: props.selected ? primaryColor : borderColor})};
......@@ -54,19 +55,19 @@ const Button = styled.a<{selected?: boolean}>`
}
`;
type RadioButtonProps = {
type RadioButtonProps<T> = {
selected?: boolean;
title?: string;
value?: string | number | symbol;
value?: T;
};
const RadioButton: FunctionComponent<RadioButtonProps & WithStyled> = ({
const RadioButton = <T extends unknown>({
className,
value,
selected,
title,
children
}) => {
}: PropsWithChildren<RadioButtonProps<T>> & WithStyled): ReturnType<FunctionComponent> => {
const groupValue = useContext(ValueContext);
const onChange = useContext(EventContext);
......
import React, {FunctionComponent, createContext, useCallback, useState} from 'react';
import React, {FunctionComponent, PropsWithChildren, createContext, useCallback, useState} from 'react';
import {WithStyled} from '~/utils/style';
import styled from 'styled-components';
......@@ -12,19 +12,24 @@ const Wrapper = styled.div`
}
`;
export const ValueContext = createContext<string | number | symbol | undefined | null>(null);
// eslint-disable-next-line @typescript-eslint/no-empty-function
export const EventContext = createContext<((value: string | number | symbol) => unknown) | undefined>(() => {});
export const EventContext = createContext<(<V extends unknown>(value: V) => unknown) | undefined>(() => {});
export const ValueContext = createContext<unknown>(null);
type RadioGroupProps = {
value?: string | number | symbol;
onChange?: (value: string | number | symbol) => unknown;
type RadioGroupProps<T> = {
value?: T;
onChange?: (value: T) => unknown;
};
const RadioGroup: FunctionComponent<RadioGroupProps & WithStyled> = ({value, onChange, children, className}) => {
const RadioGroup = <T extends unknown>({
value,
onChange,
children,
className
}: PropsWithChildren<RadioGroupProps<T>> & WithStyled): ReturnType<FunctionComponent> => {
const [selected, setSelected] = useState(value);
const onSelectedChange = useCallback(
(value: string | number | symbol) => {
(value: T) => {
setSelected(value);
onChange?.(value);
},
......@@ -32,7 +37,7 @@ const RadioGroup: FunctionComponent<RadioGroupProps & WithStyled> = ({value, onC
);
return (
<EventContext.Provider value={onSelectedChange}>
<EventContext.Provider value={v => onSelectedChange(v as T)}>
<ValueContext.Provider value={selected}>
<Wrapper className={className}>{children}</Wrapper>
</ValueContext.Provider>
......
import React, {FunctionComponent, useCallback, useMemo, useState} from 'react';
import {borderColor, ellipsis, em, rem, size} from '~/utils/style';
import Checkbox from '~/components/Checkbox';
import Field from '~/components/Field';
import {Run} from '~/types';
import RunningToggle from '~/components/RunningToggle';
import SearchInput from '~/components/SearchInput';
import styled from 'styled-components';
import uniqBy from 'lodash/uniqBy';
import {useTranslation} from '~/utils/i18n';
const Aside = styled.div`
height: 100%;
overflow: hidden;
display: flex;
flex-direction: column;
> section {
margin: ${rem(20)} ${rem(20)} 0;
flex: 0 0 auto;
&:not(:last-child) {
border-bottom: 1px solid ${borderColor};
padding-bottom: ${rem(20)};
}
&.run-section {
flex: 1 1 auto;
overflow-x: hidden;
overflow-y: auto;
display: flex;
flex-direction: column;
.running-toggle {
flex: 0 0 auto;
box-shadow: 0 -${rem(5)} ${rem(16)} 0 rgba(0, 0, 0, 0.03);
}
.run-select {
flex: 1 1 auto;
overflow-x: hidden;
overflow-y: auto;
display: flex;
flex-direction: column;
> * {
flex: 0 0 auto;
}
.search-input {
margin-bottom: ${rem(15)};
}
.run-list {
flex: 1 1 auto;
overflow-x: hidden;
overflow-y: auto;
margin-top: ${rem(5)};
> div {
margin-top: ${rem(11)};
> * {
width: 100%;
}
.run-item {
display: flex;
align-items: center;
${ellipsis()}
> i {
display: inline-block;
${size(em(12), em(12))};
border-radius: ${em(6)};
margin-right: ${em(8)};
}
}
}
}
}
}
}
`;
type RunAsideProps = {
runs?: Run[];
selectedRuns?: Run[];
onChangeRuns?: (runs: Run[]) => unknown;
running?: boolean;
onToggleRunning?: (running: boolean) => unknown;
};
const RunAside: FunctionComponent<RunAsideProps> = ({
runs,
selectedRuns,
onChangeRuns,
running,
onToggleRunning,
children
}) => {
const {t} = useTranslation('common');
const [search, setSearch] = useState('');
const selectAll = useMemo(() => runs?.length === selectedRuns?.length, [runs, selectedRuns]);
const toggleSelectAll = useCallback(
(toggle: boolean) => {
onChangeRuns?.(toggle ? runs ?? [] : []);
},
[onChangeRuns, runs]
);
const filteredRuns = useMemo(() => (search ? runs?.filter(run => run.label.indexOf(search) >= 0) : runs) ?? [], [
runs,
search
]);
const setSelectedRuns = useCallback(
(run: Run, toggle) => {
let selected = selectedRuns ?? [];
if (toggle) {
selected = uniqBy([...selected, run], r => r.label);
} else {
selected = selected.filter(r => r.label !== run.label);
}
onChangeRuns?.(selected);
},
[onChangeRuns, selectedRuns]
);
return (
<Aside>
{children}
<section className="run-section">
<Field className="run-select" label={t('select-runs')}>
<SearchInput
className="search-input"
value={search}
onChange={setSearch}
placeholder={t('search-runs')}
rounded
/>
<Checkbox value={selectAll} onChange={toggleSelectAll}>
{t('select-all')}
</Checkbox>
<div className="run-list">
{filteredRuns.map((run, index) => (
<div key={index}>
<Checkbox
value={selectedRuns?.map(r => r.label)?.includes(run.label)}
title={run.label}
onChange={value => setSelectedRuns(run, value)}
>
<span className="run-item">
<i style={{backgroundColor: run.colors[0]}}></i>
{run.label}
</span>
</Checkbox>
</div>
))}
</div>
</Field>
<RunningToggle className="running-toggle" running={running} onToggle={onToggleRunning} />
</section>
</Aside>
);
};
export default RunAside;
import React, {FunctionComponent} from 'react';
import Select, {SelectValueType} from '~/components/Select';
import {rem} from '~/utils/style';
import styled from 'styled-components';
import {useTranslation} from '~/utils/i18n';
const Title = styled.div`
font-size: ${rem(16)};
line-height: ${rem(16)};
font-weight: 700;
margin-bottom: ${rem(10)};
`;
type RunSelectProps = {
runs?: string[];
value?: string[];
onChange?: (value: string[]) => unknown;
};
const RunSelect: FunctionComponent<RunSelectProps> = ({runs, value, onChange}) => {
const {t} = useTranslation('common');
return (
<>
<Title>{t('select-runs')}</Title>
<Select
multiple
list={runs}
value={value}
onChange={(value: SelectValueType | SelectValueType[]) => onChange?.(value as string[])}
/>
</>
);
};
export default RunSelect;
import React, {FunctionComponent, useEffect, useState} from 'react';
import {WithStyled, rem} from '~/utils/style';
import Button from '~/components/Button';
import {rem} from '~/utils/style';
import ReactTooltip from 'react-tooltip';
import {nanoid} from 'nanoid';
import styled from 'styled-components';
import {useTranslation} from '~/utils/i18n';
const Wrapper = styled.div`
padding: ${rem(20)} 0;
display: flex;
align-items: center;
> span {
width: ${rem(55)};
}
> div {
flex-grow: 1;
margin-left: ${rem(20)};
}
`;
const StyledButton = styled(Button)`
margin-top: ${rem(40)};
width: 100%;
text-transform: uppercase;
width: 100%;
`;
type RunningToggleProps = {
......@@ -16,7 +32,7 @@ type RunningToggleProps = {
onToggle?: (running: boolean) => unknown;
};
const RunningToggle: FunctionComponent<RunningToggleProps> = ({running, onToggle}) => {
const RunningToggle: FunctionComponent<RunningToggleProps & WithStyled> = ({running, onToggle, className}) => {
const {t} = useTranslation('common');
const [state, setState] = useState(!!running);
......@@ -25,10 +41,24 @@ const RunningToggle: FunctionComponent<RunningToggleProps> = ({running, onToggle
onToggle?.(state);
}, [onToggle, state]);
const [id] = useState(`running-toggle-tooltip-${nanoid()}`);
return (
<StyledButton onClick={() => setState(s => !s)} type={state ? 'primary' : 'danger'}>
{t(state ? 'running' : 'stopped')}
<Wrapper className={className}>
<span>{t(state ? 'running' : 'stopped')}</span>
<div data-for={id} data-tip>
<StyledButton onClick={() => setState(s => !s)} type={state ? 'danger' : 'primary'} rounded>
{t(state ? 'stop' : 'run')}
</StyledButton>
</div>
<ReactTooltip
id={id}
place="top"
type="dark"
effect="solid"
getContent={() => t(state ? 'stop-realtime-refresh' : 'start-realtime-refresh')}
/>
</Wrapper>
);
};
......
import Image, {ImageRef} from '~/components/Image';
import React, {FunctionComponent, useCallback, useEffect, useMemo, useRef, useState} from 'react';
import {ellipsis, em, primaryColor, size, textLightColor} from '~/utils/style';
import {ellipsis, em, primaryColor, size, textLightColor, transitionProps} from '~/utils/style';
import ChartToolbox from '~/components/ChartToolbox';
import GridLoader from 'react-spinners/GridLoader';
import Image from '~/components/Image';
import StepSlider from '~/components/SamplesPage/StepSlider';
import {formatTime} from '~/utils';
import isEmpty from 'lodash/isEmpty';
import queryString from 'query-string';
import styled from 'styled-components';
import {useRunningRequest} from '~/hooks/useRequest';
import {useTranslation} from '~/utils/i18n';
const width = em(430);
const height = em(384);
const Wrapper = styled.div`
${size(height, width)}
height: 100%;
padding: ${em(20)};
padding-bottom: 0;
display: flex;
flex-direction: column;
justify-content: flex-start;
......@@ -51,10 +51,10 @@ const Title = styled.div`
}
`;
const Container = styled.div<{fit?: boolean}>`
const Container = styled.div<{brightness?: number; contrast?: number; fit?: boolean}>`
flex-grow: 1;
flex-shrink: 1;
margin-top: ${em(20)};
margin: ${em(20)} 0;
display: flex;
justify-content: center;
align-items: center;
......@@ -62,6 +62,8 @@ const Container = styled.div<{fit?: boolean}>`
> img {
${size('100%')}
filter: brightness(${props => props.brightness ?? 1}) contrast(${props => props.contrast ?? 1});
${transitionProps('filter')}
object-fit: ${props => (props.fit ? 'contain' : 'scale-down')};
flex-shrink: 1;
}
......@@ -75,6 +77,8 @@ type ImageData = {
type SampleChartProps = {
run: string;
tag: string;
brightness?: number;
contrast?: number;
fit?: boolean;
running?: boolean;
};
......@@ -84,14 +88,18 @@ const getImageUrl = (index: number, run: string, tag: string, wallTime: number):
const cacheValidity = 5 * 60 * 1000;
const SampleChart: FunctionComponent<SampleChartProps> = ({run, tag, fit, running}) => {
const {t} = useTranslation('common');
const SampleChart: FunctionComponent<SampleChartProps> = ({run, tag, brightness, contrast, fit, running}) => {
const {t, i18n} = useTranslation(['samples', 'common']);
const image = useRef<ImageRef>(null);
const {data, error, loading} = useRunningRequest<ImageData[]>(
`/images/list?${queryString.stringify({run, tag})}`,
!!running
);
const steps = useMemo(() => data?.map(item => item.step) ?? [], [data]);
const [step, setStep] = useState(0);
const [src, setSrc] = useState<string>();
......@@ -104,11 +112,13 @@ const SampleChart: FunctionComponent<SampleChartProps> = ({run, tag, fit, runnin
cached.current = {};
}, [tag, run]);
const wallTime = useMemo(() => data?.[step].wallTime ?? 0, [data, step]);
const cacheImageSrc = useCallback(() => {
if (!data) {
return;
}
const imageUrl = getImageUrl(step, run, tag, data[step].wallTime);
const imageUrl = getImageUrl(step, run, tag, wallTime);
cached.current[step] = {
src: imageUrl,
timer: setTimeout(() => {
......@@ -116,7 +126,11 @@ const SampleChart: FunctionComponent<SampleChartProps> = ({run, tag, fit, runnin
}, cacheValidity)
};
setSrc(imageUrl);
}, [step, run, tag, data]);
}, [step, run, tag, wallTime, data]);
const saveImage = useCallback(() => {
image.current?.save(`${run}-${tag}-${steps[step]}-${wallTime.toString().replace(/\./, '_')}`);
}, [run, tag, steps, step, wallTime]);
useEffect(() => {
if (cached.current[step]) {
......@@ -139,12 +153,12 @@ const SampleChart: FunctionComponent<SampleChartProps> = ({run, tag, fit, runnin
return <GridLoader color={primaryColor} size="10px" />;
}
if (!data && error) {
return <span>{t('error')}</span>;
return <span>{t('common:error')}</span>;
}
if (isEmpty(data)) {
return <span>{t('empty')}</span>;
return <span>{t('common:empty')}</span>;
}
return <Image src={src} cache={cacheValidity} />;
return <Image ref={image} src={src} cache={cacheValidity} />;
}, [loading, error, data, step, src, t]);
return (
......@@ -153,13 +167,21 @@ const SampleChart: FunctionComponent<SampleChartProps> = ({run, tag, fit, runnin
<h4>{tag}</h4>
<span>{run}</span>
</Title>
<StepSlider
value={step}
steps={data?.map(item => item.step) ?? []}
onChange={setStep}
onChangeComplete={cacheImageSrc}
<StepSlider value={step} steps={steps} onChange={setStep} onChangeComplete={cacheImageSrc}>
{formatTime(wallTime, i18n.language)}
</StepSlider>
<Container brightness={brightness} contrast={contrast} fit={fit}>
{Content}
</Container>
<ChartToolbox
items={[
{
icon: 'download',
tooltip: t('download-image'),
onClick: saveImage
}
]}
/>
<Container fit={fit}>{Content}</Container>
</Wrapper>
);
};
......
......@@ -6,9 +6,15 @@ import styled from 'styled-components';
import {useTranslation} from '~/utils/i18n';
const Label = styled.div`
display: flex;
justify-content: space-between;
color: ${textLightColor};
font-size: ${em(12)};
margin-bottom: ${em(5)};
> :not(:first-child) {
flex-grow: 0;
}
`;
const FullWidthRangeSlider = styled(RangeSlider)`
......@@ -22,7 +28,7 @@ type StepSliderProps = {
onChangeComplete?: () => unknown;
};
const StepSlider: FunctionComponent<StepSliderProps> = ({onChange, onChangeComplete, value, steps}) => {
const StepSlider: FunctionComponent<StepSliderProps> = ({onChange, onChangeComplete, value, steps, children}) => {
const {t} = useTranslation('samples');
const [step, setStep] = useState(value);
......@@ -38,7 +44,10 @@ const StepSlider: FunctionComponent<StepSliderProps> = ({onChange, onChangeCompl
return (
<>
<Label>{`${t('step')}: ${steps[step] ?? '...'}`}</Label>
<Label>
<span>{`${t('step')}: ${steps[step] ?? '...'}`}</span>
{children && <span>{children}</span>}
</Label>
<FullWidthRangeSlider
min={0}
max={steps.length ? steps.length - 1 : 0}
......
......@@ -4,6 +4,7 @@ import {
RangeParams,
TransformParams,
chartData,
nearestPoint,
range,
singlePointRange,
sortingMethodMap,
......@@ -11,21 +12,21 @@ import {
transform,
xAxisMap
} from '~/resource/scalars';
import React, {FunctionComponent, useCallback, useMemo} from 'react';
import {em, size} from '~/utils/style';
import LineChart, {LineChartRef} from '~/components/LineChart';
import React, {FunctionComponent, useCallback, useMemo, useRef, useState} from 'react';
import {rem, size} from '~/utils/style';
import ChartToolbox from '~/components/ChartToolbox';
import {EChartOption} from 'echarts';
import LineChart from '~/components/LineChart';
import {Run} from '~/types';
import {cycleFetcher} from '~/utils/fetch';
import ee from '~/utils/event';
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';
const width = em(430);
const height = em(320);
const smoothWasm = () =>
import('@visualdl/wasm').then(({transform}) => (params: TransformParams) =>
(transform(params.datasets, params.smoothing) as unknown) as Dataset[]
......@@ -38,28 +39,63 @@ const rangeWasm = () =>
const smoothWorker = () => new Worker('~/worker/scalars/smooth.worker.ts', {type: 'module'});
const rangeWorker = () => new Worker('~/worker/scalars/range.worker.ts', {type: 'module'});
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;
}
`;
const StyledLineChart = styled(LineChart)`
${size(height, width)}
flex-grow: 1;
`;
const Toolbox = styled(ChartToolbox)`
margin-left: ${rem(20)};
margin-right: ${rem(20)};
`;
const Error = styled.div`
${size(height, width)}
${size('100%', '100%')}
display: flex;
justify-content: center;
align-items: center;
`;
enum XAxisType {
value = 'value',
log = 'log',
time = 'time'
}
enum YAxisType {
value = 'value',
log = 'log'
}
type ScalarChartProps = {
runs: string[];
cid: symbol;
runs: Run[];
tag: string;
smoothing: number;
xAxis: keyof typeof xAxisMap;
sortingMethod: keyof typeof sortingMethodMap;
outlier?: boolean;
running?: boolean;
onToggleMaximized?: (maximized: boolean) => void;
};
const ScalarChart: FunctionComponent<ScalarChartProps> = ({
cid,
runs,
tag,
smoothing,
......@@ -70,15 +106,27 @@ const ScalarChart: FunctionComponent<ScalarChartProps> = ({
}) => {
const {t, i18n} = useTranslation(['scalars', 'common']);
const echart = useRef<LineChartRef>(null);
const {data: datasets, error, loading} = useRunningRequest<(Dataset | null)[]>(
runs.map(run => `/scalars/list?${queryString.stringify({run, tag})}`),
runs.map(run => `/scalars/list?${queryString.stringify({run: run.label, tag})}`),
!!running,
(...urls) => cycleFetcher(urls)
);
const smooth = false;
const type = useMemo(() => (xAxis === 'wall' ? 'time' : 'value'), [xAxis]);
const xAxisLabel = useMemo(() => (xAxis === 'step' ? '' : t(`x-axis-value.${xAxis}`)), [xAxis, t]);
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]);
const transformParams = useMemo(
() => ({
......@@ -104,18 +152,18 @@ const ScalarChart: FunctionComponent<ScalarChartProps> = ({
// if there is only one point, place it in the middle
if (smoothedDatasets.length === 1 && smoothedDatasets[0].length === 1) {
if (['value', 'log'].includes(type)) {
if ([XAxisType.value, XAxisType.log].includes(xAxisType)) {
x = singlePointRange(smoothedDatasets[0][0][xAxisMap[xAxis]]);
}
y = singlePointRange(smoothedDatasets[0][0][2]);
}
return {x, y};
}, [smoothedDatasets, yRange, type, xAxis]);
}, [smoothedDatasets, yRange, xAxisType, xAxis]);
const data = useMemo(
() =>
chartData({
data: smoothedDatasets,
data: smoothedDatasets.slice(0, runs.length),
runs,
smooth,
xAxis
......@@ -127,32 +175,7 @@ const ScalarChart: FunctionComponent<ScalarChartProps> = ({
(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 points = nearestPoint(smoothedDatasets ?? [], runs, step);
const sort = sortingMethodMap[sortingMethod];
return tooltip(sort ? sort(points, data) : points, i18n);
},
......@@ -165,16 +188,47 @@ const ScalarChart: FunctionComponent<ScalarChartProps> = ({
}
return (
<Wrapper>
<StyledLineChart
ref={echart}
title={tag}
xAxis={xAxisLabel}
xRange={ranges.x}
yRange={ranges.y}
type={type}
xType={xAxisType}
yType={yAxisType}
tooltip={formatter}
data={data}
loading={loading}
/>
<Toolbox
items={[
{
icon: 'maximize',
activeIcon: 'minimize',
tooltip: t('maximize'),
activeTooltip: t('minimize'),
toggle: true,
onClick: toggleMaximized
},
{
icon: 'restore-size',
tooltip: t('restore'),
onClick: () => echart.current?.restore()
},
{
icon: 'log-axis',
tooltip: t('axis'),
toggle: true,
onClick: toggleYAxisType
},
{
icon: 'download',
tooltip: t('download-image'),
onClick: () => echart.current?.saveAsImage()
}
]}
/>
</Wrapper>
);
};
......
import React, {FunctionComponent, useState} from 'react';
import Field from '~/components/Field';
import RangeSlider from '~/components/RangeSlider';
import styled from 'styled-components';
import {useTranslation} from '~/utils/i18n';
const FullWidthRangeSlider = styled(RangeSlider)`
width: 100%;
`;
type SmoothingSliderProps = {
value: number;
onChange?: (value: number) => unknown;
};
const SmoothingSlider: FunctionComponent<SmoothingSliderProps> = ({onChange, value}) => {
const {t} = useTranslation('scalars');
const [smoothing, setSmoothing] = useState(value);
return (
<Field label={`${t('smoothing')}: ${Math.round(smoothing * 100) / 100}`}>
<FullWidthRangeSlider
min={0}
max={0.99}
step={0.01}
value={smoothing}
onChange={setSmoothing}
onChangeComplete={() => onChange?.(smoothing)}
/>
</Field>
);
};
export default SmoothingSlider;
import Input, {InputProps, padding} from '~/components/Input';
import React, {FunctionComponent} from 'react';
import {WithStyled, backgroundColor, em, math, position, textLighterColor} from '~/utils/style';
import {WithStyled, em, math, position, textLighterColor} from '~/utils/style';
import Icon from '~/components/Icon';
import styled from 'styled-components';
......@@ -8,27 +8,26 @@ import styled from 'styled-components';
const iconSize = em(16);
const StyledInput = styled(Input)`
padding-right: ${math(`${iconSize} + ${padding} * 2`)};
padding-left: ${math(`${iconSize} + ${padding} * 2`)};
width: 100%;
`;
const Control = styled.div`
background-color: ${backgroundColor};
position: relative;
`;
const SearchIcon = styled(Icon)`
font-size: ${iconSize};
display: block;
${position('absolute', padding, padding, null, null)}
${position('absolute', padding, null, null, padding)}
pointer-events: none;
color: ${textLighterColor};
`;
const SearchInput: FunctionComponent<InputProps & WithStyled> = ({className, ...props}) => (
<Control className={className}>
<StyledInput {...props} />
<SearchIcon type="search" />
<StyledInput {...props} />
</Control>
);
......
......@@ -27,12 +27,10 @@ import without from 'lodash/without';
export const padding = em(10);
export const height = em(36);
const minWidth = em(160);
const Wrapper = styled.div<{opened?: boolean}>`
height: ${height};
line-height: calc(${height} - 2px);
min-width: ${minWidth};
max-width: 100%;
display: inline-block;
position: relative;
......@@ -122,29 +120,38 @@ const MultipleListItem = styled(Checkbox)<{selected?: boolean}>`
align-items: center;
`;
export type SelectValueType = string | number | symbol;
type SelectListItem<T> = {
value: T;
label: string;
};
type SelectProps<T> = {
list?: (SelectListItem<T> | string)[];
value?: T | T[];
onChange?: (value: T | T[]) => unknown;
multiple?: boolean;
type OnSingleChange<T> = (value: T) => unknown;
type OnMultipleChange<T> = (value: T[]) => unknown;
export type SelectProps<T> = {
list?: (SelectListItem<T> | T)[];
placeholder?: string;
};
} & (
| {
value?: T;
onChange?: OnSingleChange<T>;
multiple?: false;
}
| {
value?: T[];
onChange?: OnMultipleChange<T>;
multiple: true;
}
);
const Select: FunctionComponent<SelectProps<SelectValueType> & WithStyled> = ({
const Select = <T extends unknown>({
list: propList,
value: propValue,
placeholder,
multiple,
className,
onChange
}) => {
}: SelectProps<T> & WithStyled): ReturnType<FunctionComponent> => {
const {t} = useTranslation('common');
const [isOpened, setIsOpened] = useState(false);
......@@ -158,15 +165,21 @@ const Select: FunctionComponent<SelectProps<SelectValueType> & WithStyled> = ({
setValue
]);
const isSelected = useMemo(
() => !!(multiple ? value && (value as SelectValueType[]).length !== 0 : (value as SelectValueType)),
[multiple, value]
);
const isSelected = useMemo(() => !!(multiple ? (value as T[]) && (value as T[]).length !== 0 : (value as T)), [
multiple,
value
]);
const changeValue = useCallback(
(mutateValue: SelectValueType, checked?: boolean) => {
let newValue;
if (multiple) {
newValue = value as SelectValueType[];
(mutateValue: T) => {
setValue(mutateValue);
(onChange as OnSingleChange<T>)?.(mutateValue);
setIsOpenedFalse();
},
[setIsOpenedFalse, onChange]
);
const changeMultipleValue = useCallback(
(mutateValue: T, checked: boolean) => {
let newValue = value as T[];
if (checked) {
if (!newValue.includes(mutateValue)) {
newValue = [...newValue, mutateValue];
......@@ -176,35 +189,32 @@ const Select: FunctionComponent<SelectProps<SelectValueType> & WithStyled> = ({
newValue = without(newValue, mutateValue);
}
}
} else {
newValue = mutateValue;
}
setValue(newValue);
onChange?.(newValue);
if (!multiple) {
setIsOpenedFalse();
}
(onChange as OnMultipleChange<T>)?.(newValue);
},
[multiple, value, setIsOpenedFalse, onChange]
[value, onChange]
);
const ref = useClickOutside(setIsOpenedFalse);
const list = useMemo(
() => propList?.map(item => ('string' === typeof item ? {value: item, label: item} : item)) ?? [],
const list = useMemo<SelectListItem<T>[]>(
() =>
propList?.map(item =>
['string', 'number'].includes(typeof item)
? {value: item as T, label: item + ''}
: (item as SelectListItem<T>)
) ?? [],
[propList]
);
const isListEmpty = useMemo(() => list.length === 0, [list]);
const findLabelByValue = useCallback((v: SelectValueType) => list.find(item => item.value === v)?.label ?? '', [
list
]);
const findLabelByValue = useCallback((v: T) => list.find(item => item.value === v)?.label ?? '', [list]);
const label = useMemo(
() =>
isSelected
? multiple
? (value as SelectValueType[]).map(findLabelByValue).join(' / ')
: findLabelByValue(value as SelectValueType)
? (value as T[]).map(findLabelByValue).join(' / ')
: findLabelByValue(value as T)
: placeholder || t('select'),
[multiple, value, findLabelByValue, isSelected, placeholder, t]
);
......@@ -222,11 +232,11 @@ const Select: FunctionComponent<SelectProps<SelectValueType> & WithStyled> = ({
if (multiple) {
return (
<MultipleListItem
value={(value as SelectValueType[]).includes(item.value)}
value={(value as T[]).includes(item.value)}
key={index}
title={item.label}
size="small"
onChange={checked => changeValue(item.value, checked)}
onChange={checked => changeMultipleValue(item.value, checked)}
>
{item.label}
</MultipleListItem>
......
import React, {FunctionComponent, useCallback, useState} from 'react';
import {borderColor, borderFocusedColor, rem, size, transitionProps} from '~/utils/style';
import {height, padding} from '~/components/Input';
import BigNumber from 'bignumber.js';
import RangeSlider from '~/components/RangeSlider';
import styled from 'styled-components';
const Wrapper = styled.div`
display: flex;
align-items: center;
`;
const Input = styled.input`
${size(height, rem(52))};
line-height: ${height};
display: inline-block;
outline: none;
padding: ${padding};
${transitionProps('border-color')}
border: none;
border-bottom: 1px solid ${borderColor};
text-align: center;
&:hover,
&:focus {
border-bottom-color: ${borderFocusedColor};
}
`;
const FullWidthRangeSlider = styled(RangeSlider)`
flex-grow: 1;
margin-right: ${rem(20)};
`;
type SliderProps = {
min: number;
max: number;
step: number;
value: number;
onChange?: (value: number) => unknown;
onChangeComplete?: (value: number) => unknown;
};
const Slider: FunctionComponent<SliderProps> = ({onChange, onChangeComplete, value, min, max, step}) => {
const fixNumber = useCallback(
(v: number) =>
new BigNumber(v).dividedBy(step).integerValue(BigNumber.ROUND_HALF_UP).multipliedBy(step).toNumber(),
[step]
);
const [sliderValue, setSliderValue] = useState(fixNumber(value));
const [inputValue, setInputValue] = useState(sliderValue + '');
const changeSliderValue = useCallback(
(value: number) => {
const v = fixNumber(value);
setInputValue(v + '');
setSliderValue(v);
onChange?.(v);
},
[fixNumber, onChange]
);
const changeInputValue = useCallback(
(value: string) => {
setInputValue(value);
const v = Number.parseFloat(value);
if (v < min || v > max || Number.isNaN(v)) {
return;
}
const result = fixNumber(v);
setSliderValue(result);
onChange?.(result);
onChangeComplete?.(result);
},
[onChange, onChangeComplete, min, max, fixNumber]
);
const confirmInput = useCallback(() => {
setInputValue(sliderValue + '');
}, [sliderValue]);
return (
<Wrapper>
<FullWidthRangeSlider
min={min}
max={max}
step={step}
value={sliderValue}
onChange={changeSliderValue}
onChangeComplete={() => onChangeComplete?.(sliderValue)}
/>
<Input
type="text"
value={inputValue}
onChange={e => changeInputValue(e.currentTarget.value)}
onBlur={confirmInput}
onKeyDown={e => e.key === 'Enter' && confirmInput()}
/>
</Wrapper>
);
};
export default Slider;
import React, {FunctionComponent, useCallback, useEffect, useMemo, useState} from 'react';
import {ellipsis, math, rem} from '~/utils/style';
import SearchInput from '~/components/SearchInput';
import Tag from '~/components/Tag';
import {Tag as TagType} from '~/types';
import groupBy from 'lodash/groupBy';
import sortBy from 'lodash/sortBy';
import styled from 'styled-components';
import {useTranslation} from '~/utils/i18n';
const margin = rem(16);
const Wrapper = styled.div`
display: flex;
justify-content: flex-start;
align-items: center;
flex-wrap: wrap;
`;
const Search = styled(SearchInput)`
width: ${rem(280)};
margin: 0 ${math(`${rem(5)} + ${margin}`)} ${margin} 0;
`;
const SearchTag = styled(Tag)`
margin: 0 ${margin} ${margin} 0;
vertical-align: middle;
`;
const SearchTagLabel = styled.span`
${ellipsis(rem(120))}
vertical-align: middle;
`;
type TagFilterProps = {
value?: string;
tags?: TagType[];
onChange?: (value: TagType[]) => unknown;
};
const TagFilter: FunctionComponent<TagFilterProps> = ({value, tags: propTags, onChange}) => {
type NonNullTags = NonNullable<typeof propTags>;
const {t} = useTranslation('common');
const tagGroups = useMemo(
() =>
sortBy(
Object.entries(groupBy<TagType>(propTags || [], tag => tag.label.split('/')[0])).map(
([label, tags]) => ({
label,
tags
})
),
tag => tag.label
),
[propTags]
);
const [matchedCount, setMatchedCount] = useState(propTags?.length ?? 0);
useEffect(() => setMatchedCount(propTags?.length ?? 0), [propTags, setMatchedCount]);
const [inputValue, setInputValue] = useState(value || '');
useEffect(() => setInputValue(value || ''), [value, setInputValue]);
const [selectedValue, setSelectedValue] = useState('');
const hasSelectedValue = useMemo(() => selectedValue !== '', [selectedValue]);
const allText = useMemo(() => inputValue || t('all'), [inputValue, t]);
const onInputChange = useCallback(
(value: string) => {
setInputValue(value);
setSelectedValue('');
try {
const pattern = new RegExp(value);
const matchedTags = propTags?.filter(tag => pattern.test(tag.label)) ?? [];
setMatchedCount(matchedTags.length);
onChange?.(matchedTags);
} catch {
setMatchedCount(0);
}
},
[propTags, onChange]
);
const onClickTag = useCallback(
({label, tags}: {label: string; tags: NonNullTags}) => {
setSelectedValue(label);
onChange?.(tags);
},
[onChange]
);
const onClickAllTag = useCallback(() => {
setSelectedValue('');
onInputChange(inputValue);
}, [inputValue, onInputChange]);
return (
<Wrapper>
<Search placeholder={t('searchTagPlaceholder')} rounded onChange={onInputChange}></Search>
<SearchTag active={!hasSelectedValue} onClick={onClickAllTag} title={allText}>
<SearchTagLabel>{allText}</SearchTagLabel> ({inputValue ? matchedCount : propTags?.length ?? 0})
</SearchTag>
{tagGroups.map(group => (
<SearchTag
active={hasSelectedValue && group.label === selectedValue}
onClick={() => onClickTag(group)}
key={group.label}
title={group.label}
>
<SearchTagLabel>{group.label}</SearchTagLabel> ({group.tags.length})
</SearchTag>
))}
</Wrapper>
);
};
TagFilter.defaultProps = {
tags: [] as TagType[]
};
export default TagFilter;
......@@ -21,6 +21,9 @@ const useECharts = <T extends HTMLElement>(options: {
if (options.gl) {
await import('echarts-gl');
}
if (!ref.current) {
return;
}
echartInstance.current = echarts.init((ref.current as unknown) as HTMLDivElement);
if (options.zoom) {
setTimeout(() => {
......
import {useEffect, useMemo} from 'react';
import useSWR, {ConfigInterface, keyInterface, responseInterface} from 'swr';
import ee from '~/utils/event';
import {fetcherFn} from 'swr/dist/types';
type Response<D, E> = responseInterface<D, E> & {
......@@ -61,6 +62,13 @@ function useRunningRequest<D = unknown, E = unknown>(
}
}, [running, mutate]);
useEffect(() => {
ee.on('refresh-running', mutate);
return () => {
ee.off('refresh-running', mutate);
};
}, [mutate]);
return {mutate, ...others};
}
......
import {Run, Tag} from '~/types';
import {color, colorAlt} from '~/utils/chart';
import {useCallback, useEffect, useMemo, useReducer} from 'react';
import {Tag} from '~/types';
import groupBy from 'lodash/groupBy';
import intersection from 'lodash/intersection';
import intersectionBy from 'lodash/intersectionBy';
import uniq from 'lodash/uniq';
import {useRouter} from 'next/router';
import {useRunningRequest} from '~/hooks/useRequest';
type Runs = string[];
type Tags = Record<string, string[]>;
type State = {
runs: Runs;
initRuns: string[];
runs: Run[];
selectedRuns: Run[];
initTags: Tags;
tags: Tag[];
filteredTags: Tag[];
selectedTags: Tag[];
};
enum ActionType {
initRuns,
setRuns,
setSelectedRuns,
initTags,
setTags,
setFilteredTags
setSelectedTags
}
type ActionInitRuns = {
type: ActionType.initRuns;
payload: string[];
};
type ActionSetRuns = {
type: ActionType.setRuns;
payload: Runs;
type: ActionType.setRuns | ActionType.setSelectedRuns;
payload: Run[];
};
type ActionInitTags = {
......@@ -35,31 +44,26 @@ type ActionInitTags = {
};
type ActionSetTags = {
type: ActionType.setTags;
payload: Tag[];
};
type ActionSetFilteredTags = {
type: ActionType.setFilteredTags;
type: ActionType.setTags | ActionType.setSelectedTags;
payload: Tag[];
};
type Action = ActionSetRuns | ActionInitTags | ActionSetTags | ActionSetFilteredTags;
type Action = ActionInitRuns | ActionSetRuns | ActionInitTags | ActionSetTags;
type SingleTag = {label: Tag['label']; run: Tag['runs'][number]};
const groupTags = (runs: Runs, tags?: Tags): Tag[] =>
const groupTags = (runs: Run[], tags?: Tags): Tag[] =>
Object.entries(
groupBy<SingleTag>(
runs
// get tags of selected runs
.filter(run => runs.includes(run))
.filter(run => !!runs.find(r => r.label === run.label))
// group by runs
.reduce<SingleTag[]>((prev, run) => {
if (tags && tags[run]) {
if (tags && tags[run.label]) {
Array.prototype.push.apply(
prev,
tags[run].map(label => ({label, run}))
tags[run.label].map(label => ({label, run}))
);
}
return prev;
......@@ -68,34 +72,66 @@ const groupTags = (runs: Runs, tags?: Tags): Tag[] =>
)
).map(([label, tags]) => ({label, runs: tags.map(tag => tag.run)}));
const attachRunColor = (runs: string[]): Run[] =>
runs?.map((run, index) => {
const i = index % color.length;
return {
label: run,
colors: [color[i], colorAlt[i]]
};
});
const reducer = (state: State, action: Action): State => {
switch (action.type) {
case ActionType.initRuns:
const initRuns = action.payload;
const initRunsRuns = attachRunColor(initRuns);
const initRunsSelectedRuns = state.selectedRuns.filter(run => initRuns.includes(run.label));
const initRunsTags = groupTags(initRunsSelectedRuns, state.initTags);
return {
...state,
initRuns,
runs: initRunsRuns,
selectedRuns: initRunsSelectedRuns,
tags: initRunsTags,
selectedTags: initRunsTags
};
case ActionType.setRuns:
const runTags = groupTags(action.payload, state.initTags);
const setRunsSelectedRuns = intersectionBy(state.selectedRuns, action.payload, r => r.label);
const setRunsTags = groupTags(setRunsSelectedRuns, state.initTags);
return {
...state,
runs: action.payload,
tags: runTags,
filteredTags: runTags
selectedRuns: setRunsSelectedRuns,
tags: setRunsTags,
selectedTags: setRunsTags
};
case ActionType.setSelectedRuns:
const setSelectedRunsTags = groupTags(action.payload, state.initTags);
return {
...state,
selectedRuns: action.payload,
tags: setSelectedRunsTags,
selectedTags: setSelectedRunsTags
};
case ActionType.initTags:
const newTags = groupTags(state.runs, action.payload);
const initTagsTags = groupTags(state.selectedRuns, action.payload);
return {
...state,
initTags: action.payload,
tags: newTags,
filteredTags: newTags
tags: initTagsTags,
selectedTags: initTagsTags
};
case ActionType.setTags:
return {
...state,
tags: action.payload,
filteredTags: action.payload
selectedTags: action.payload
};
case ActionType.setFilteredTags:
case ActionType.setSelectedTags:
return {
...state,
filteredTags: action.payload
selectedTags: action.payload
};
default:
throw new Error();
......@@ -105,48 +141,42 @@ const reducer = (state: State, action: Action): State => {
const useTagFilter = (type: string, running: boolean) => {
const router = useRouter();
const {data: runs, loading: loadingRuns} = useRunningRequest<Runs>('/runs', running);
const {data: runs, loading: loadingRuns} = useRunningRequest<string[]>('/runs', running);
const {data: tags, loading: loadingTags} = useRunningRequest<Tags>(`/${type}/tags`, running);
const selectedRuns = useMemo(
const [state, dispatch] = useReducer(reducer, {
initRuns: [],
runs: [],
selectedRuns: [],
initTags: {},
tags: [],
selectedTags: []
});
const queryRuns = useMemo(
() =>
runs
? router.query.runs
? intersection(
uniq(Array.isArray(router.query.runs) ? router.query.runs : router.query.runs.split(',')),
runs
)
: runs
router.query.runs
? uniq(Array.isArray(router.query.runs) ? router.query.runs : router.query.runs.split(','))
: [],
[router, runs]
[router]
);
const [state, dispatch] = useReducer(
reducer,
{
runs: selectedRuns,
initTags: {},
tags: groupTags(selectedRuns, tags)
},
initArgs => ({...initArgs, filteredTags: initArgs.tags})
const runsFromQuery = useMemo(
() => (queryRuns.length ? state.runs.filter(run => queryRuns.includes(run.label)) : state.runs),
[state.runs, queryRuns]
);
const onChangeRuns = useCallback((runs: Runs) => dispatch({type: ActionType.setRuns, payload: runs}), [dispatch]);
const onInitTags = useCallback((tags: Tags) => dispatch({type: ActionType.initTags, payload: tags}), [dispatch]);
const onFilterTags = useCallback((tags: Tag[]) => dispatch({type: ActionType.setFilteredTags, payload: tags}), [
dispatch
]);
const onChangeRuns = useCallback((runs: Run[]) => dispatch({type: ActionType.setSelectedRuns, payload: runs}), []);
const onChangeTags = useCallback((tags: Tag[]) => dispatch({type: ActionType.setSelectedTags, payload: tags}), []);
useEffect(() => onInitTags(tags || {}), [onInitTags, tags]);
useEffect(() => onChangeRuns(selectedRuns), [onChangeRuns, selectedRuns]);
useEffect(() => dispatch({type: ActionType.initRuns, payload: runs || []}), [runs]);
useEffect(() => dispatch({type: ActionType.setSelectedRuns, payload: runsFromQuery}), [runsFromQuery]);
useEffect(() => dispatch({type: ActionType.initTags, payload: tags || {}}), [tags]);
return {
runs,
tags: state.tags,
selectedRuns: state.runs,
selectedTags: state.filteredTags,
...state,
onChangeRuns,
onFilterTags,
onChangeTags,
loadingRuns,
loadingTags
};
......
......@@ -37,45 +37,52 @@
"dagre-d3": "0.6.4",
"echarts": "4.7.0",
"echarts-gl": "1.1.1",
"eventemitter3": "4.0.0",
"file-saver": "2.0.2",
"isomorphic-unfetch": "3.0.0",
"lodash": "4.17.15",
"moment": "2.24.0",
"next": "9.3.4",
"mime-types": "2.1.27",
"moment": "2.25.3",
"nanoid": "3.1.5",
"next": "9.3.6",
"nprogress": "0.2.0",
"polished": "3.5.1",
"polished": "3.6.2",
"prop-types": "15.7.2",
"query-string": "6.12.0",
"query-string": "6.12.1",
"react": "16.13.1",
"react-dom": "16.13.1",
"react-hooks-worker": "0.9.0",
"react-input-range": "1.3.0",
"react-is": "16.13.1",
"react-spinners": "0.8.1",
"react-spinners": "0.8.3",
"react-tooltip": "4.2.6",
"save-svg-as-png": "1.4.17",
"styled-components": "5.1.0",
"swr": "0.2.0"
},
"devDependencies": {
"@babel/core": "7.9.0",
"@babel/core": "7.9.6",
"@types/d3": "5.7.2",
"@types/dagre-d3": "0.4.39",
"@types/echarts": "4.4.5",
"@types/lodash": "4.14.149",
"@types/node": "13.11.1",
"@types/echarts": "4.6.0",
"@types/file-saver": "2.0.1",
"@types/lodash": "4.14.150",
"@types/mime-types": "2.1.0",
"@types/node": "13.13.5",
"@types/nprogress": "0.2.0",
"@types/react": "16.9.34",
"@types/react-dom": "16.9.6",
"@types/styled-components": "5.0.1",
"@types/react-dom": "16.9.7",
"@types/styled-components": "5.1.0",
"@visualdl/mock": "^2.0.0-beta.32",
"babel-plugin-emotion": "10.0.33",
"babel-plugin-styled-components": "1.10.7",
"babel-plugin-typescript-to-proptypes": "1.3.2",
"core-js": "3.6.5",
"cross-env": "7.0.2",
"css-loader": "3.5.2",
"ora": "4.0.3",
"css-loader": "3.5.3",
"ora": "4.0.4",
"typescript": "3.8.3",
"worker-plugin": "4.0.2"
"worker-plugin": "4.0.3"
},
"engines": {
"node": ">=10",
......
......@@ -18,6 +18,10 @@ import useRequest from '~/hooks/useRequest';
// eslint-disable-next-line @typescript-eslint/no-empty-function
const dumbFn = () => {};
const AsideSection = styled.section`
padding: ${rem(20)};
`;
const SubSection = styled.div`
margin-bottom: ${rem(30)};
`;
......@@ -257,12 +261,12 @@ const Graphs: NextI18NextPage = () => {
const {currentNode, downloadImage, fitScreen, scale, setScale} = useDagreD3(graph);
const aside = (
<section>
<AsideSection>
<SubSection>
<Button icon="download" onClick={downloadImage}>
<Button rounded type="primary" icon="download" onClick={downloadImage}>
{t('download-image')}
</Button>
<Button icon="revert" onClick={fitScreen}>
<Button rounded type="primary" icon="revert" onClick={fitScreen}>
{t('restore-image')}
</Button>
</SubSection>
......@@ -277,7 +281,7 @@ const Graphs: NextI18NextPage = () => {
<Field label={`${t('node-info')}:`} />
<NodeInfo node={currentNode} />
</SubSection>
</section>
</AsideSection>
);
const ContentInner = useMemo(() => {
......
import {Dimension, Reduction} from '~/resource/high-dimensional';
import {NextI18NextPage, useTranslation} from '~/utils/i18n';
import React, {useEffect, useState} from 'react';
import Select, {SelectValueType} from '~/components/Select';
import Select, {SelectProps} from '~/components/Select';
import {em, rem} from '~/utils/style';
import AsideDivider from '~/components/AsideDivider';
......@@ -24,6 +24,14 @@ import useSearchValue from '~/hooks/useSearchValue';
const dimensions = ['2d', '3d'];
const reductions = ['pca', 'tsne'];
const AsideSection = styled.section`
padding: ${rem(20)};
`;
const StyledSelect = styled<React.FunctionComponent<SelectProps<string>>>(Select)`
min-width: ${em(160)};
`;
const StyledIcon = styled(Icon)`
margin-right: ${em(4)};
vertical-align: middle;
......@@ -44,7 +52,7 @@ const HighDimensional: NextI18NextPage = () => {
const {query} = useRouter();
const queryRun = Array.isArray(query.run) ? query.run[0] : query.run;
const {data: runs, error, loading} = useRunningRequest<string[]>('/runs', running);
const selectedRun = runs?.includes(queryRun) ? queryRun : runs?.[0];
const selectedRun = queryRun && runs?.includes(queryRun) ? queryRun : runs?.[0];
const [run, setRun] = useState(selectedRun);
useEffect(() => setRun(selectedRun), [setRun, selectedRun]);
......@@ -56,12 +64,12 @@ const HighDimensional: NextI18NextPage = () => {
const [labelVisibility, setLabelVisibility] = useState(true);
const aside = (
<section>
<AsideSection>
<AsideTitle>{t('common:select-runs')}</AsideTitle>
<Select
<StyledSelect
list={runs}
value={run}
onChange={(value: SelectValueType | SelectValueType[]) => setRun(value as string)}
onChange={(value: NonNullable<typeof runs>[number]) => setRun(value)}
/>
<AsideDivider />
<Field>
......@@ -78,7 +86,7 @@ const HighDimensional: NextI18NextPage = () => {
{t('dimension')}
</AsideTitle>
<Field>
<RadioGroup value={dimension} onChange={value => setDimension(value as Dimension)}>
<RadioGroup value={dimension} onChange={value => setDimension(value)}>
{dimensions.map(item => (
<RadioButton key={item} value={item}>
{t(item)}
......@@ -92,7 +100,7 @@ const HighDimensional: NextI18NextPage = () => {
{t('reduction-method')}
</AsideTitle>
<Field>
<RadioGroup value={reduction} onChange={value => setReduction(value as Reduction)}>
<RadioGroup value={reduction} onChange={value => setReduction(value)}>
{reductions.map(item => (
<RadioButton key={item} value={item}>
{t(item)}
......@@ -101,7 +109,7 @@ const HighDimensional: NextI18NextPage = () => {
</RadioGroup>
</Field>
<RunningToggle running={running} onToggle={setRunning} />
</section>
</AsideSection>
);
return (
......
// cSpell:words ungrouped
import ChartPage, {WithChart} from '~/components/ChartPage';
import {NextI18NextPage, useTranslation} from '~/utils/i18n';
import React, {useCallback, useMemo, useState} from 'react';
// import {em, rem} from '~/utils/style';
import AsideDivider from '~/components/AsideDivider';
import ChartPage from '~/components/ChartPage';
import Checkbox from '~/components/Checkbox';
import Content from '~/components/Content';
// import Field from '~/components/Field';
// import Icon from '~/components/Icon';
import Field from '~/components/Field';
import Preloader from '~/components/Preloader';
import RunSelect from '~/components/RunSelect';
import RunningToggle from '~/components/RunningToggle';
import RunAside from '~/components/RunAside';
import SampleChart from '~/components/SamplesPage/SampleChart';
import TagFilter from '~/components/TagFilter';
import Slider from '~/components/Slider';
import Title from '~/components/Title';
// import {rem} from '~/utils/style';
// import styled from 'styled-components';
import {rem} from '~/utils/style';
import useTagFilter from '~/hooks/useTagFilter';
// const StyledIcon = styled(Icon)`
// font-size: ${rem(16)};
// margin-left: ${em(6)};
// margin-right: ${em(4)};
// vertical-align: middle;
// `;
// const CheckboxTitle = styled.span`
// font-size: ${rem(16)};
// font-weight: 700;
// vertical-align: text-top;
// `;
const chartSize = {
height: rem(406)
};
type Item = {
run: string;
......@@ -43,65 +29,62 @@ const Samples: NextI18NextPage = () => {
const [running, setRunning] = useState(true);
const {runs, tags, selectedRuns, selectedTags, onChangeRuns, onFilterTags, loadingRuns, loadingTags} = useTagFilter(
'images',
running
);
const {runs, tags, selectedRuns, onChangeRuns, loadingRuns, loadingTags} = useTagFilter('images', running);
const ungroupedSelectedTags = useMemo(
() =>
selectedTags.reduce<Item[]>((prev, {runs, ...item}) => {
tags.reduce<Item[]>((prev, {runs, ...item}) => {
Array.prototype.push.apply(
prev,
runs.map(run => ({...item, run}))
runs.map(run => ({...item, run: run.label, id: `${item.label}-${run.label}`}))
);
return prev;
}, []),
[selectedTags]
[tags]
);
const showImage = true;
// const [showImage, setShowImage] = useState(true);
// const [showAudio, setShowAudio] = useState(true);
// const [showText, setShowText] = useState(true);
const [showActualSize, setShowActualSize] = useState(false);
const [brightness, setBrightness] = useState(1);
const [contrast, setContrast] = useState(1);
const aside = (
<RunAside
runs={runs}
selectedRuns={selectedRuns}
onChangeRuns={onChangeRuns}
running={running}
onToggleRunning={setRunning}
>
<section>
<RunSelect runs={runs} value={selectedRuns} onChange={onChangeRuns} />
<AsideDivider />
{/* <Field>
<Checkbox value={showImage} onChange={setShowImage} disabled>
<StyledIcon type="image" />
<CheckboxTitle>{t('image')}</CheckboxTitle>
</Checkbox>
</Field> */}
{showImage && (
<Checkbox value={showActualSize} onChange={setShowActualSize}>
{t('show-actual-size')}
</Checkbox>
)}
{/* <AsideDivider />
<Field>
<Checkbox value={showAudio} onChange={setShowAudio}>
<StyledIcon type="audio" />
<CheckboxTitle>{t('audio')}</CheckboxTitle>
</Checkbox>
</section>
<section>
<Field label={t('brightness')}>
<Slider min={0} max={2} step={0.01} value={brightness} onChange={setBrightness} />
</Field>
</section>
<section>
<Field label={t('contrast')}>
<Slider min={0} max={2} step={0.01} value={contrast} onChange={setContrast} />
</Field>
<AsideDivider />
<Field>
<Checkbox value={showText} onChange={setShowText}>
<StyledIcon type="text" />
<CheckboxTitle>{t('text')}</CheckboxTitle>
</Checkbox>
</Field> */}
<RunningToggle running={running} onToggle={setRunning} />
</section>
</RunAside>
);
const withChart = useCallback(
({run, label}: Item) => <SampleChart run={run} tag={label} fit={!showActualSize} running={running} />,
[showActualSize, running]
const withChart = useCallback<WithChart<Item>>(
({run, label}) => (
<SampleChart
run={run}
tag={label}
fit={!showActualSize}
running={running}
brightness={brightness}
contrast={contrast}
/>
),
[showActualSize, running, brightness, contrast]
);
return (
......@@ -110,8 +93,12 @@ const Samples: NextI18NextPage = () => {
<Preloader url="/images/tags" />
<Title>{t('common:samples')}</Title>
<Content aside={aside} loading={loadingRuns}>
<TagFilter tags={tags} onChange={onFilterTags} />
<ChartPage items={ungroupedSelectedTags} withChart={withChart} loading={loadingRuns || loadingTags} />
<ChartPage
items={ungroupedSelectedTags}
chartSize={chartSize}
withChart={withChart}
loading={loadingRuns || loadingTags}
/>
</Content>
</>
);
......
import ChartPage, {WithChart} from '~/components/ChartPage';
import {NextI18NextPage, useTranslation} from '~/utils/i18n';
import React, {useCallback, useState} from 'react';
import Select, {SelectValueType} from '~/components/Select';
import {sortingMethodMap, xAxisMap} from '~/resource/scalars';
import AsideDivider from '~/components/AsideDivider';
import ChartPage from '~/components/ChartPage';
import Checkbox from '~/components/Checkbox';
import Content from '~/components/Content';
import Field from '~/components/Field';
import Preloader from '~/components/Preloader';
import RunSelect from '~/components/RunSelect';
import RunningToggle from '~/components/RunningToggle';
import RadioButton from '~/components/RadioButton';
import RadioGroup from '~/components/RadioGroup';
import RunAside from '~/components/RunAside';
import ScalarChart from '~/components/ScalarsPage/ScalarChart';
import SmoothingSlider from '~/components/ScalarsPage/SmoothingSlider';
import Select from '~/components/Select';
import Slider from '~/components/Slider';
import {Tag} from '~/types';
import TagFilter from '~/components/TagFilter';
import Title from '~/components/Title';
import useSearchValue from '~/hooks/useSearchValue';
import {rem} from '~/utils/style';
import styled from 'styled-components';
import useTagFilter from '~/hooks/useTagFilter';
type XAxis = keyof typeof xAxisMap;
const xAxisValues = ['step', 'relative', 'wall'];
const xAxisValues = ['step', 'relative', 'wall'] as const;
type TooltipSorting = keyof typeof sortingMethodMap;
const toolTipSortingValues = ['default', 'descending', 'ascending', 'nearest'];
const toolTipSortingValues = ['default', 'descending', 'ascending', 'nearest'] as const;
const TooltipSortingDiv = styled.div`
margin-top: ${rem(20)};
display: flex;
align-items: center;
> :last-child {
margin-left: ${rem(20)};
flex-shrink: 1;
flex-grow: 1;
}
`;
const Scalars: NextI18NextPage = () => {
const {t} = useTranslation(['scalars', 'common']);
const [running, setRunning] = useState(true);
const {runs, tags, selectedRuns, selectedTags, onChangeRuns, onFilterTags, loadingRuns, loadingTags} = useTagFilter(
'scalars',
running
);
const debounceTags = useSearchValue(selectedTags);
const {runs, tags, selectedRuns, onChangeRuns, loadingRuns, loadingTags} = useTagFilter('scalars', running);
const [smoothing, setSmoothing] = useState(0.6);
const [xAxis, setXAxis] = useState(xAxisValues[0] as XAxis);
const onChangeXAxis = (value: SelectValueType | SelectValueType[]) => setXAxis(value as XAxis);
const [xAxis, setXAxis] = useState<XAxis>(xAxisValues[0]);
const [tooltipSorting, setTooltipSorting] = useState(toolTipSortingValues[0] as TooltipSorting);
const onChangeTooltipSorting = (value: SelectValueType | SelectValueType[]) =>
setTooltipSorting(value as TooltipSorting);
const [tooltipSorting, setTooltipSorting] = useState<TooltipSorting>(toolTipSortingValues[0]);
const onChangeTooltipSorting = (value: TooltipSorting) => setTooltipSorting(value);
const [ignoreOutliers, setIgnoreOutliers] = useState(false);
const aside = (
<RunAside
runs={runs}
selectedRuns={selectedRuns}
onChangeRuns={onChangeRuns}
running={running}
onToggleRunning={setRunning}
>
<section>
<RunSelect runs={runs} value={selectedRuns} onChange={onChangeRuns} />
<AsideDivider />
<SmoothingSlider value={smoothing} onChange={setSmoothing} />
<Field label={t('x-axis')}>
<Select
list={xAxisValues.map(value => ({label: t(`x-axis-value.${value}`), value}))}
value={xAxis}
onChange={onChangeXAxis}
/>
</Field>
<Field label={t('tooltip-sorting')}>
<Checkbox value={ignoreOutliers} onChange={setIgnoreOutliers}>
{t('ignore-outliers')}
</Checkbox>
<TooltipSortingDiv>
<span>{t('tooltip-sorting')}</span>
<Select
list={toolTipSortingValues.map(value => ({label: t(`tooltip-sorting-value.${value}`), value}))}
value={tooltipSorting}
onChange={onChangeTooltipSorting}
/>
</TooltipSortingDiv>
</section>
<section>
<Field label={t('smoothing')}>
<Slider min={0} max={0.99} step={0.01} value={smoothing} onChangeComplete={setSmoothing} />
</Field>
<Field>
<Checkbox value={ignoreOutliers} onChange={setIgnoreOutliers}>
{t('ignore-outliers')}
</Checkbox>
</section>
<section>
<Field label={t('x-axis')}>
<RadioGroup value={xAxis} onChange={setXAxis}>
{xAxisValues.map(value => (
<RadioButton key={value} value={value}>
{t(`x-axis-value.${value}`)}
</RadioButton>
))}
</RadioGroup>
</Field>
<RunningToggle running={running} onToggle={setRunning} />
</section>
</RunAside>
);
const withChart = useCallback(
(item: Tag) => (
const withChart = useCallback<WithChart<Tag>>(
({label, runs, ...args}) => (
<ScalarChart
runs={item.runs}
tag={item.label}
runs={runs}
tag={label}
{...args}
smoothing={smoothing}
xAxis={xAxis}
sortingMethod={tooltipSorting}
......@@ -96,8 +114,7 @@ const Scalars: NextI18NextPage = () => {
<Preloader url="/scalars/tags" />
<Title>{t('common:scalars')}</Title>
<Content aside={aside} loading={loadingRuns}>
<TagFilter tags={tags} onChange={onFilterTags} />
<ChartPage items={debounceTags} withChart={withChart} loading={loadingRuns || loadingTags} />
<ChartPage items={tags} withChart={withChart} loading={loadingRuns || loadingTags} />
</Content>
</>
);
......
<svg height="244" viewBox="0 0 280 244" width="280" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><linearGradient id="a" x1="50%" x2="48.658415%" y1="61.213285%" y2="44.221263%"><stop offset="0" stop-color="#fff" stop-opacity="0"/><stop offset="1" stop-color="#dee2ea"/></linearGradient><linearGradient id="b" x1="47.364856%" x2="35.78808%" y1="8.992503%" y2="37.810374%"><stop offset="0" stop-color="#dee2ea"/><stop offset="1" stop-color="#abb2b9"/></linearGradient><linearGradient id="c" x1="50%" x2="44.131151%" y1="9.45978%" y2="50%"><stop offset="0" stop-color="#dee2ea"/><stop offset="1" stop-color="#bdc3cb"/></linearGradient><linearGradient id="d" x1="33.638931%" x2="74.896888%" y1="71.45323%" y2="22.628242%"><stop offset="0" stop-color="#fff" stop-opacity="0"/><stop offset="1"/></linearGradient><linearGradient id="e" x1="60.858945%" x2="39.239063%" y1="0%" y2="76.405382%"><stop offset="0" stop-color="#abb6c4"/><stop offset="1" stop-color="#fff" stop-opacity=".47076"/></linearGradient><path id="f" d="m3.84714229 0h4.36027147v4.3600388c0 2.12471802-1.72242428 3.84714229-3.84714229 3.84714229h-4.36027147v-4.3600388c0-2.12471802 1.72242427-3.84714229 3.84714229-3.84714229z"/><linearGradient id="g" x1="68.901915%" x2="25.608096%" y1="75.912656%" y2="-8.83115%"><stop offset="0" stop-color="#fff" stop-opacity="0"/><stop offset="1" stop-color="#343434"/></linearGradient><linearGradient id="h" x1="74.359708%" x2="33.018133%" y1="39.942826%" y2="71.704777%"><stop offset="0" stop-color="#f1f1f1"/><stop offset="1" stop-color="#dee2ea"/></linearGradient><linearGradient id="i" x1="57.363466%" x2="40.555293%" y1="45.484702%" y2="66.495962%"><stop offset="0" stop-color="#ebeef1"/><stop offset="1" stop-color="#dee2ea"/></linearGradient><linearGradient id="j" x1="50%" x2="90.846327%" y1="66.079045%" y2="-13.896429%"><stop offset="0" stop-color="#6d87cf"/><stop offset="1" stop-color="#cddafd"/></linearGradient><path id="k" d="m0 29.8838432c11.5353918-7.8924286 17.5826848-17.7255406 18.1418791-29.49933608 3.0289717-.45278604 8.8799038.11217194 23.5599768 2.67021581 1.367099.23822084 1.4879799 2.41157984.3626426 6.520077 4.9494085 1.81891287 7.2172366 3.55153047 6.8034844 5.19785287-3.5872247 14.2735895-8.3329897 20.7813066-15.4954379 23.8393978-8.2159505 1.0452991-19.3401322-1.8641034-33.372545-8.7282074z"/><filter id="l" height="162.2%" width="124.5%" x="-12.3%" y="-15.6%"><feOffset dx="0" dy="12" in="SourceAlpha" result="shadowOffsetOuter1"/><feColorMatrix in="shadowOffsetOuter1" type="matrix" values="0 0 0 0 0.105882353 0 0 0 0 0.207843137 0 0 0 0 0.329411765 0 0 0 0.272192029 0"/></filter><linearGradient id="m" x1="7.624573%" x2="71.770415%" y1="102.541703%" y2="33.462242%"><stop offset="0" stop-color="#bfcbeb"/><stop offset="1" stop-color="#607dcb"/></linearGradient><path id="n" d="m17.4502651 5.71309115c1.4952718 9.22895275-5.0346651 18.66368195-13.17511171 25.39013135 6.83616511 3.6997177 19.24765711 8.8536983 29.28243561 11.9794135.4773667.1486941 1.9761677-.0357439 2.4420549.0455318 2.8165867.4913635 4.4086441-1.5556191 5.4819664-2.1718978 4.4234951-4.650677 8.7750847-8.0097048 10.9904253-14.531276 1.1358473-3.3437338 1.778678-6.1909435 1.8067554-7.4134355-3.5798514-3.3280893-12.1374089-3.2306091-36.8285259-13.29846735z"/><filter id="o" height="164%" width="124%" x="-12%" y="-16%"><feOffset dx="0" dy="12" in="SourceAlpha" result="shadowOffsetOuter1"/><feColorMatrix in="shadowOffsetOuter1" type="matrix" values="0 0 0 0 0.105882353 0 0 0 0 0.207843137 0 0 0 0 0.329411765 0 0 0 0.270720109 0"/></filter><mask id="p" fill="#fff"><use fill="#fff" fill-rule="evenodd" xlink:href="#f"/></mask></defs><g fill="none" fill-rule="evenodd" transform="translate(0 -15)"><path d="m24.9582173 134.578947h158.3021077v68.141889c0 .622153.369703 1.184814.940751 1.431754l94.356626 40.802925c.571048.24694.940751.809601.940751 1.431755v35.990553h-105.276995l-149.9510584-73.667707c-.5338354-.262262-.8720709-.805279-.8720709-1.400057v-71.171223c0-.861503.6983859-1.559889 1.5598886-1.559889z" fill="url(#a)"/><path d="m22.6183844 97.8642659 68.9142541 23.5180841v85.927977l-68.1343098-23.518083z" fill="url(#b)" transform="matrix(-1 0 0 1 114.151022 0)"/><path d="m91.2534819 97.8642659 91.3910151 21.1745931v85.927978l-91.3910151-21.174593z" fill="url(#c)"/><path d="m22.6183844 121.299169 91.3910146 21.174593v89.833795l-91.3910146-25.08041z" fill="#dee2ea"/><path d="m22.6183844 121.299169 91.3910146 21.174593v89.833795l-91.3910146-25.08041z" fill="url(#d)" opacity=".1"/><path d="m25.2019236 114.077156c22.365796-14.5953137 16.7223374-32.0446458 22.1058781-46.9698127 5.1369321-14.2414765 20.4117109-22.9211138 31.0060354-15.6967118 3.9989621 2.7269422 2.1911749 18.9284442-19.1090743 13.8648296-7.5039319-1.7838768-12.7690674-8.834139-10.7730272-22.244632 3.0515976-20.5023075 35.2877255-20.2694701 36.243555-20.9249957" style="stroke-width:1.835918;opacity:.5;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:1.835918 4.589796;stroke:url(#e)" transform="matrix(-.73135369609 -.68199836077 -.68199836077 .73135369609 206.71566916786 72.38169924619)"/><g transform="matrix(-.12186933814 -.99254615162 .99254615162 -.12186933814 113.38923137115 43.45407750341)"><path d="m7.78442851 14.5238048c3.17220949-3.4338433 4.75890199-6.02490943 4.76007739-7.77319825.001761-2.62243322-2.1256319-4.746905-4.75166608-4.74514161-2.62603417.00176558-4.75628391 2.12909814-4.75804663 4.75153137-.00117427 1.74828882 1.5820375 4.33722499 4.74963532 7.76680849z" fill="#dee2ea" opacity=".4" transform="matrix(.70710678 -.70710678 .70710678 .70710678 -3.562444 7.92877)"/><path d="m19.8781294 26.1880778c3.1722095-3.4338433 4.7589019-6.0249094 4.7600774-7.7731982.0017609-2.6224333-2.1256319-4.746905-4.7516661-4.7451416-2.6260342.0017655-4.7562839 2.1290981-4.7580466 4.7515313-.0011743 1.7482888 1.5820375 4.337225 4.7496353 7.7668085z" fill="#dee2ea" opacity=".4" transform="matrix(-.70710678 .70710678 -.70710678 -.70710678 48.034868 19.961122)"/><g transform="translate(9.93529 9.547737)"><use fill="#b2b8bf" xlink:href="#f"/><g fill="#f5f9ff"><path d="m1.73063141.43558306 1.29677877-.00087133-.006381 9.49667079-1.29677876.00087133z" mask="url(#p)" transform="matrix(.70710678 -.70710678 .70710678 .70710678 -2.969411 3.198173)"/><path d="m1.15488015.12275076.91634493-.91777729 6.70918511 6.71967236-.91634493.91777729z" mask="url(#p)"/></g></g><path d="m22.549602 7.491456 3.152562-.751439" stroke="#b2b8bf" stroke-width=".5"/><path d="m21.253694 6.195586 1.212948-3.125431" stroke="#b2b8bf" stroke-width=".5"/><ellipse cx="19.438612" cy="8.683823" fill="#c6cad1" rx="3.455753" ry="3.455655"/></g><path d="m113.871866 118.955679 69.694199 23.518083v89.833795l-68.914254-27.4239z" fill="#dee2ea" transform="matrix(-1 0 0 1 297.43793 0)"/><path d="m113.871866 118.955679 69.694199 23.518083v89.833795l-68.914254-27.4239z" fill="url(#g)" opacity=".323288" transform="matrix(-1 0 0 1 297.43793 0)"/><path d="m138.050088 118.892428-24.365413 34.020282 70.530215 27.884346 23.427071-38.922624z" fill="url(#h)" transform="matrix(-1 0 0 1 321.326636 0)"/><path d="m22.9913998 0-22.9913998 35.5355472 88.8639608 23.8328739 25.7878502-38.1932219z" fill="url(#i)" transform="translate(0 120.518006)"/><g transform="translate(25.738162 66.617729)"><path d="m42.0561471 3.217186c1.5769413 1.55432988 2.9211924 3.06153671 4.0327533 4.52162047 2.3139749 3.03950693 3.00486 7.24076313 2.8439663 7.34686923-.8777648.5788683-5.8963537-2.7604242-6.8767196-5.47259239-.5877297-1.62594583-.5877297-3.7579116 0-6.39589731z" fill="#607dcb"/><use fill="#000" filter="url(#l)" xlink:href="#k"/><use fill="url(#j)" fill-rule="evenodd" xlink:href="#k"/></g><g transform="matrix(.75470958 .65605903 -.65605903 .75470958 173.43155 128.359633)"><g transform="matrix(.97029573 -.2419219 .2419219 .97029573 -5.047357 7.809258)"><use fill="#000" filter="url(#o)" xlink:href="#n"/><use fill="url(#m)" fill-rule="evenodd" xlink:href="#n"/></g><path d="m40.6338193 34.9432952c1.8392617 1.8060754.1356857 6.6259076-3.4786555 6.5414949 3.8398603.0517047 5.911095-1.3467347 7.4029126-2.7497916s8.4954982-7.91596 7.6369153-25.5820263c.1123833 14.9345807-3.5528686 22.2252315-11.5611724 21.790323z" fill="#4a69bb"/></g></g></svg>
\ No newline at end of file
......@@ -4,14 +4,27 @@
"graphs": "Graphs",
"high-dimensional": "High Dimensional",
"search": "Search",
"searchTagPlaceholder": "Search tags in RegExp",
"all": "ALL",
"search-tags": "Search tags in RegExp",
"search-result": "Search Result",
"search-empty": "Nothing found. Please try again with another word. <1/>Or you can <3>see all charts</3>.",
"unselected-empty": "Nothing selected. <1/>Please select display data from right side.",
"empty": "Empty",
"select": "Please Select",
"select-all": "Select All",
"runs": "Runs",
"select-runs": "Select Runs",
"search-runs": "Search runs",
"running": "Running",
"stopped": "Stopped",
"run": "Run",
"stop": "Stop",
"start-realtime-refresh": "Start realtime refresh",
"stop-realtime-refresh": "Stop realtime refresh",
"loading": "Loading",
"error": "Error occurred"
"error": "Error occurred",
"previous-page": "Prev Page",
"next-page": "Next Page",
"total-page": "{{count}} page, jump to",
"total-page_plural": "{{count}} pages, jump to",
"confirm": "Confirm"
}
{
"image": "Image",
"audio": "Audio",
"text": "Text",
"image": "image",
"audio": "audio",
"text": "text",
"show-actual-size": "Show Actual Image Size",
"step": "Step"
"step": "Step",
"download-image": "Download $t(image)",
"brightness": "Brightness",
"contrast": "Contrast"
}
......@@ -15,5 +15,10 @@
"ascending": "Ascending",
"nearest": "Nearest"
},
"ignore-outliers": "Ignore outliers in chart scaling"
"ignore-outliers": "Ignore outliers in chart scaling",
"maximize": "Maximize",
"minimize": "Minimize",
"restore": "Restore",
"axis": "Axis",
"download-image": "Download image"
}
......@@ -4,14 +4,28 @@
"graphs": "网络结构",
"high-dimensional": "高维数据映射",
"search": "搜索",
"searchTagPlaceholder": "搜索标签(支持正则)",
"search-tags": "搜索标签(支持正则)",
"search-result": "搜索结果",
"search-empty": "没有找到您期望的内容,你可以尝试其他搜索词<1/>或者点击<3>查看全部图表</3>",
"unselected-empty": "未选中任何数据<1/>请在右侧操作栏选择要展示的数据",
"all": "全部",
"empty": "空空如也",
"select": "请选择",
"select-all": "全选",
"runs": "数据流",
"select-runs": "选择数据流",
"search-runs": "搜索数据流",
"running": "运行中",
"stopped": "已停止",
"run": "运行",
"stop": "停止",
"start-realtime-refresh": "运行实时数据刷新",
"stop-realtime-refresh": "停止实时数据刷新",
"loading": "载入中",
"error": "发生错误"
"error": "发生错误",
"previous-page": "上一页",
"next-page": "下一页",
"total-page": "共 {{count}} 页,跳转至",
"total-page_plural": "共 {{count}} 页,跳转至",
"confirm": "确定"
}
......@@ -3,5 +3,8 @@
"audio": "音频",
"text": "文本",
"show-actual-size": "按真实大小展示",
"step": "Step"
"step": "Step",
"download-image": "下载$t(image)",
"brightness": "亮度",
"contrast": "对比度"
}
......@@ -15,5 +15,10 @@
"ascending": "升序",
"nearest": "最近"
},
"ignore-outliers": "图表缩放时忽略极端值"
"ignore-outliers": "图表缩放时忽略极端值",
"maximize": "最大化",
"minimize": "最小化",
"restore": "还原",
"axis": "坐标轴",
"download-image": "下载图片"
}
......@@ -13,6 +13,21 @@
-moz-osx-font-smoothing: grayscale;
}
.icon-refresh:before {
content: '\e90c';
}
.icon-restore-size:before {
content: '\e90d';
}
.icon-minimize:before {
content: '\e90e';
}
.icon-log-axis:before {
content: '\e90f';
}
.icon-maximize:before {
content: '\e910';
}
.icon-chevron-down:before {
content: '\e90a';
}
......
import * as chart from '~/utils/chart';
import {ChartDataParams, RangeParams, TooltipData, TransformParams, xAxisMap} from './types';
import {ChartDataParams, Dataset, RangeParams, TooltipData, TransformParams} from './types';
import {formatTime, quantile} from '~/utils';
import BigNumber from 'bignumber.js';
import {I18n} from '@visualdl/i18n';
import {Run} from '~/types';
import cloneDeep from 'lodash/cloneDeep';
import compact from 'lodash/compact';
import maxBy from 'lodash/maxBy';
......@@ -15,6 +16,12 @@ BigNumber.config({EXPONENTIAL_AT: [-6, 7]});
export * from './types';
export const xAxisMap = {
step: 1,
relative: 4,
wall: 0
};
export const sortingMethodMap = {
default: null,
descending: (points: TooltipData[]) => sortBy(points, point => point.item[3]).reverse(),
......@@ -38,7 +45,7 @@ export const transform = ({datasets, smoothing}: TransformParams) =>
if (i === 0) {
startValue = millisecond;
}
// Relative time, millisecond to hours.
// relative time, millisecond to hours.
d[4] = Math.floor(millisecond - startValue) / (60 * 60 * 1000);
if (!nextVal.isFinite()) {
d[3] = nextVal.toNumber();
......@@ -67,9 +74,9 @@ export const chartData = ({data, runs, smooth, xAxis}: ChartDataParams) =>
// [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];
const name = runs[i].label;
const color = runs[i].colors[0];
const colorAlt = runs[i].colors[1];
return [
{
name,
......@@ -140,52 +147,87 @@ export const range = ({datasets, outlier}: RangeParams) => {
}
};
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 || []
};
});
// 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
};
time: 0,
step: 1,
value: 2,
smoothed: 3,
relative: 4
} as const;
const widthPropMap = {
Run: 60,
Time: 120,
Step: 40,
Value: 60,
Smoothed: 60,
Relative: 60
};
run: [60, 180] as [number, number],
time: 150,
step: 40,
value: 60,
smoothed: 70,
relative: 60
} as const;
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'
};
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;
const transformedData = data.map(item => {
const data = item.item;
return {
Run: item.run,
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),
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'
};
relative: Math.floor(data[indexPropMap.relative] * 60 * 60) + 's'
} as const;
});
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>`;
let headerHtml = '<tr style="font-size:14px;">';
headerHtml += (Object.keys(transformedData[0]) as (keyof typeof transformedData[0])[])
.map(key => {
return `<td style="padding: 0 4px; font-weight: bold; width: ${widthPropMap[key]}px;">${i18n.t(
translatePropMap[key]
)}</td>`;
return `<th style="padding: 0 4px; font-weight: bold;" class="${key}">${renderContent(
i18n.t(translatePropMap[key]),
widthPropMap[key]
)}</th>`;
})
.join('');
headerHtml += '</tr>';
......@@ -194,8 +236,20 @@ export const tooltip = (data: TooltipData[], i18n: I18n) => {
.map(item => {
let str = '<tr style="font-size:12px;">';
str += Object.keys(item)
.map(val => {
return `<td style="padding: 0 4px; overflow: hidden;">${item[val as keyof typeof item]}</td>`;
.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>`;
})
.join('');
str += '</tr>';
......@@ -203,6 +257,5 @@ export const tooltip = (data: TooltipData[], i18n: I18n) => {
})
.join('');
// eslint-disable-next-line
return `<table style="text-align: left;table-layout: fixed;width: 500px;"><thead>${headerHtml}</thead><tbody>${content}</tbody><table>`;
return `<table style="text-align: left;table-layout: fixed;"><thead>${headerHtml}</thead><tbody>${content}</tbody><table>`;
};
import {Run} from '~/types';
import {xAxisMap} from './index';
export type Dataset = number[][];
export type Range = {
......@@ -5,14 +8,8 @@ export type Range = {
max: number;
};
export const xAxisMap = {
step: 1,
relative: 4,
wall: 0
};
export type TooltipData = {
run: string;
run: Run;
item: number[];
};
......@@ -23,7 +20,7 @@ export type TransformParams = {
export type ChartDataParams = {
data: Dataset[];
runs: string[];
runs: Run[];
smooth: boolean;
xAxis: keyof typeof xAxisMap;
};
......
/**
* The **ResizeObserver** interface reports changes to the dimensions of an
* [Element](https://developer.mozilla.org/en-US/docs/Web/API/Element)'s content
* or border box, or the bounding box of an
* [SVGElement](https://developer.mozilla.org/en-US/docs/Web/API/SVGElement).
*
* > **Note**: The content box is the box in which content can be placed,
* > meaning the border box minus the padding and border width. The border box
* > encompasses the content, padding, and border. See
* > [The box model](https://developer.mozilla.org/en-US/docs/Learn/CSS/Building_blocks/The_box_model)
* > for further explanation.
*
* `ResizeObserver` avoids infinite callback loops and cyclic dependencies that
* are often created when resizing via a callback function. It does this by only
* processing elements deeper in the DOM in subsequent frames. Implementations
* should, if they follow the specification, invoke resize events before paint
* and after layout.
*
* @see https://developer.mozilla.org/en-US/docs/Web/API/ResizeObserver
*/
declare class ResizeObserver {
/**
* The **ResizeObserver** constructor creates a new `ResizeObserver` object,
* which can be used to report changes to the content or border box of an
* `Element` or the bounding box of an `SVGElement`.
*
* @example
* var ResizeObserver = new ResizeObserver(callback)
*
* @param callback
* The function called whenever an observed resize occurs. The function is
* called with two parameters:
* * **entries**
* An array of
* [ResizeObserverEntry](https://developer.mozilla.org/en-US/docs/Web/API/ResizeObserverEntry)
* objects that can be used to access the new dimensions of the element
* after each change.
* * **observer**
* A reference to the `ResizeObserver` itself, so it will definitely be
* accessible from inside the callback, should you need it. This could be
* used for example to automatically unobserve the observer when a certain
* condition is reached, but you can omit it if you don't need it.
*
* The callback will generally follow a pattern along the lines of:
* ```js
* function(entries, observer) {
* for (let entry of entries) {
* // Do something to each entry
* // and possibly something to the observer itself
* }
* }
* ```
*
* The following snippet is taken from the
* [resize-observer-text.html](https://mdn.github.io/dom-examples/resize-observer/resize-observer-text.html)
* ([see source](https://github.com/mdn/dom-examples/blob/master/resize-observer/resize-observer-text.html))
* example:
* @example
* const resizeObserver = new ResizeObserver(entries => {
* for (let entry of entries) {
* if(entry.contentBoxSize) {
* h1Elem.style.fontSize = Math.max(1.5, entry.contentBoxSize.inlineSize/200) + 'rem';
* pElem.style.fontSize = Math.max(1, entry.contentBoxSize.inlineSize/600) + 'rem';
* } else {
* h1Elem.style.fontSize = Math.max(1.5, entry.contentRect.width/200) + 'rem';
* pElem.style.fontSize = Math.max(1, entry.contentRect.width/600) + 'rem';
* }
* }
* });
*
* resizeObserver.observe(divElem);
*/
constructor(callback: ResizeObserverCallback);
/**
* The **disconnect()** method of the
* [ResizeObserver](https://developer.mozilla.org/en-US/docs/Web/API/ResizeObserver)
* interface unobserves all observed
* [Element](https://developer.mozilla.org/en-US/docs/Web/API/Element) or
* [SVGElement](https://developer.mozilla.org/en-US/docs/Web/API/SVGElement)
* targets.
*/
disconnect: () => void;
/**
* The `observe()` method of the
* [ResizeObserver](https://developer.mozilla.org/en-US/docs/Web/API/ResizeObserver)
* interface starts observing the specified
* [Element](https://developer.mozilla.org/en-US/docs/Web/API/Element) or
* [SVGElement](https://developer.mozilla.org/en-US/docs/Web/API/SVGElement).
*
* @example
* resizeObserver.observe(target, options);
*
* @param target
* A reference to an
* [Element](https://developer.mozilla.org/en-US/docs/Web/API/Element) or
* [SVGElement](https://developer.mozilla.org/en-US/docs/Web/API/SVGElement)
* to be observed.
*
* @param options
* An options object allowing you to set options for the observation.
* Currently this only has one possible option that can be set.
*/
observe: (target: Element, options?: ResizeObserverObserveOptions) => void;
/**
* The **unobserve()** method of the
* [ResizeObserver](https://developer.mozilla.org/en-US/docs/Web/API/ResizeObserver)
* interface ends the observing of a specified
* [Element](https://developer.mozilla.org/en-US/docs/Web/API/Element) or
* [SVGElement](https://developer.mozilla.org/en-US/docs/Web/API/SVGElement).
*/
unobserve: (target: Element) => void;
}
interface ResizeObserverObserveOptions {
/**
* Sets which box model the observer will observe changes to. Possible values
* are `content-box` (the default), and `border-box`.
*
* @default "content-box"
*/
box?: 'content-box' | 'border-box';
}
/**
* The function called whenever an observed resize occurs. The function is
* called with two parameters:
*
* @param entries
* An array of
* [ResizeObserverEntry](https://developer.mozilla.org/en-US/docs/Web/API/ResizeObserverEntry)
* objects that can be used to access the new dimensions of the element after
* each change.
*
* @param observer
* A reference to the `ResizeObserver` itself, so it will definitely be
* accessible from inside the callback, should you need it. This could be used
* for example to automatically unobserve the observer when a certain condition
* is reached, but you can omit it if you don't need it.
*
* The callback will generally follow a pattern along the lines of:
* @example
* function(entries, observer) {
* for (let entry of entries) {
* // Do something to each entry
* // and possibly something to the observer itself
* }
* }
*
* @example
* const resizeObserver = new ResizeObserver(entries => {
* for (let entry of entries) {
* if(entry.contentBoxSize) {
* h1Elem.style.fontSize = Math.max(1.5, entry.contentBoxSize.inlineSize/200) + 'rem';
* pElem.style.fontSize = Math.max(1, entry.contentBoxSize.inlineSize/600) + 'rem';
* } else {
* h1Elem.style.fontSize = Math.max(1.5, entry.contentRect.width/200) + 'rem';
* pElem.style.fontSize = Math.max(1, entry.contentRect.width/600) + 'rem';
* }
* }
* });
*
* resizeObserver.observe(divElem);
*/
type ResizeObserverCallback = (entries: ResizeObserverEntry[], observer: ResizeObserver) => void;
/**
* The **ResizeObserverEntry** interface represents the object passed to the
* [ResizeObserver()](https://developer.mozilla.org/en-US/docs/Web/API/ResizeObserver/ResizeObserver)
* constructor's callback function, which allows you to access the new
* dimensions of the
* [Element](https://developer.mozilla.org/en-US/docs/Web/API/Element) or
* [SVGElement](https://developer.mozilla.org/en-US/docs/Web/API/SVGElement)
* being observed.
*/
interface ResizeObserverEntry {
/**
* An object containing the new border box size of the observed element when
* the callback is run.
*/
readonly borderBoxSize: ResizeObserverEntryBoxSize;
/**
* An object containing the new content box size of the observed element when
* the callback is run.
*/
readonly contentBoxSize: ResizeObserverEntryBoxSize;
/**
* A [DOMRectReadOnly](https://developer.mozilla.org/en-US/docs/Web/API/DOMRectReadOnly)
* object containing the new size of the observed element when the callback is
* run. Note that this is better supported than the above two properties, but
* it is left over from an earlier implementation of the Resize Observer API,
* is still included in the spec for web compat reasons, and may be deprecated
* in future versions.
*/
// node_modules/typescript/lib/lib.dom.d.ts
readonly contentRect: DOMRectReadOnly;
/**
* A reference to the
* [Element](https://developer.mozilla.org/en-US/docs/Web/API/Element) or
* [SVGElement](https://developer.mozilla.org/en-US/docs/Web/API/SVGElement)
* being observed.
*/
readonly target: Element;
}
/**
* The **borderBoxSize** read-only property of the
* [ResizeObserverEntry](https://developer.mozilla.org/en-US/docs/Web/API/ResizeObserverEntry)
* interface returns an object containing the new border box size of the
* observed element when the callback is run.
*/
interface ResizeObserverEntryBoxSize {
/**
* The length of the observed element's border box in the block dimension. For
* boxes with a horizontal
* [writing-mode](https://developer.mozilla.org/en-US/docs/Web/CSS/writing-mode),
* this is the vertical dimension, or height; if the writing-mode is vertical,
* this is the horizontal dimension, or width.
*/
blockSize: number;
/**
* The length of the observed element's border box in the inline dimension.
* For boxes with a horizontal
* [writing-mode](https://developer.mozilla.org/en-US/docs/Web/CSS/writing-mode),
* this is the horizontal dimension, or width; if the writing-mode is
* vertical, this is the vertical dimension, or height.
*/
inlineSize: number;
}
interface Window {
ResizeObserver: ResizeObserver;
}
export interface Run {
label: string;
colors: [string, string];
}
export interface Tag {
runs: string[];
runs: Run[];
label: string;
}
......@@ -50,7 +50,10 @@ export const title = {
export const tooltip = {
trigger: 'axis',
position: ['10%', '95%'],
position: ['10%', '100%'],
backgroundColor: 'rgba(0, 0, 0, 0.75)',
hideDelay: 100,
enterable: false,
axisPointer: {
type: 'cross',
label: {
......@@ -69,13 +72,13 @@ export const tooltip = {
export const toolbox = {
show: true,
orient: 'vertical',
showTitle: false,
top: 50,
right: 8,
itemSize: 0,
feature: {
saveAsImage: {
show: true
show: true,
type: 'png',
pixelRatio: 2
},
dataZoom: {
show: true
......@@ -83,9 +86,6 @@ export const toolbox = {
restore: {
show: true
}
},
tooltip: {
show: true
}
};
......@@ -103,8 +103,8 @@ export const legend = {
export const grid = {
left: 50,
top: 60,
right: 50,
bottom: 50
right: 30,
bottom: 30
};
export const xAxis = {
......
import EventEmitter from 'eventemitter3';
export default new EventEmitter();
......@@ -12,9 +12,15 @@ export const fetcher = async <T = any>(url: string, options?: any): Promise<T> =
return response && 'data' in response ? response.data : response;
};
export const blobFetcher = async (url: string, options?: any): Promise<Blob> => {
export type BlobResponse = {
data: Blob;
type: string | null;
};
export const blobFetcher = async (url: string, options?: any): Promise<BlobResponse> => {
const res = await fetch(process.env.API_URL + url, options);
return await res.blob();
const data = await res.blob();
return {data, type: res.headers.get('Content-Type')};
};
export const cycleFetcher = async <T = any>(urls: string[], options?: any): Promise<T[]> => {
......
......@@ -29,7 +29,7 @@ const nextI18Next = new NextI18Next({
export default nextI18Next;
export const {i18n, appWithTranslation, withTranslation, useTranslation, Router, Link, Trans} = nextI18Next;
export const {i18n, config, appWithTranslation, withTranslation, useTranslation, Router, Link, Trans} = nextI18Next;
// from ~/node_modules/next/types/index.d.ts
// https://gitlab.com/kachkaev/website-frontend/-/blob/master/src/i18n.ts#L64-68
......
export function dataURL2Blob(base64: string): Blob {
const parts = base64.split(';base64,');
const contentType = parts[0].split(':')[1];
const raw = window.atob(parts[1]);
const rawLength = raw.length;
const uInt8Array = new Uint8Array(rawLength);
for (let i = 0; i < rawLength; ++i) {
uInt8Array[i] = raw.charCodeAt(i);
}
return new Blob([uInt8Array], {type: contentType});
}
import * as polished from 'polished';
import {createGlobalStyle, keyframes} from 'styled-components';
import {css} from 'styled-components';
import vdlIcon from '!!css-loader!~/public/style/vdl-icon.css';
export {default as styled} from 'styled-components';
......@@ -41,11 +43,14 @@ export const backgroundColor = '#FFF';
export const backgroundFocusedColor = '#F6F6F6';
export const borderColor = '#DDD';
export const borderFocusedColor = darken(0.15, borderColor);
export const borderActiveColor = darken(0.3, borderColor);
export const navbarBackgroundColor = '#1527C2';
export const navbarHoverBackgroundColor = lighten(0.05, navbarBackgroundColor);
export const navbarHighlightColor = '#596cd6';
export const progressBarColor = '#FFF';
export const maskColor = 'rgba(255, 255, 255, 0.8)';
export const tooltipBackgroundColor = 'rgba(0, 0, 0, 0.6)';
export const tooltipTextColor = '#FFF';
// transitions
export const duration = '75ms';
......@@ -82,6 +87,21 @@ export const transitionProps = (props: string | string[], args?: string) => {
}
return transitions(props, args);
};
export const link = css`
a {
color: ${primaryColor};
cursor: pointer;
${transitionProps('color')};
&:hover {
color: ${primaryFocusedColor};
}
&:active {
color: ${primaryActiveColor};
}
}
`;
const spinner = keyframes`
0% {
......
export const enabled = () => process.env.NODE_ENV !== 'development' || !!process.env.WITH_WASM;
......@@ -37,22 +37,22 @@
"dependencies": {
"detect-node": "2.0.4",
"hoist-non-react-statics": "3.3.2",
"i18next": "19.4.1",
"i18next-browser-languagedetector": "4.0.2",
"i18next-express-middleware": "1.9.1",
"i18next-node-fs-backend": "2.1.3",
"i18next-xhr-backend": "3.2.2",
"i18next": "19.4.4",
"i18next-browser-languagedetector": "4.1.1",
"i18next-fs-backend": "1.0.2",
"i18next-http-backend": "1.0.8",
"i18next-http-middleware": "1.0.4",
"path-match": "1.2.4",
"prop-types": "15.7.2",
"react-i18next": "11.3.4",
"react-i18next": "11.4.0",
"url": "0.11.0"
},
"devDependencies": {
"@types/express": "4.17.6",
"@types/hoist-non-react-statics": "3.3.1",
"@types/node": "13.11.1",
"@types/node": "13.13.5",
"@types/react": "16.9.34",
"@types/react-dom": "16.9.6",
"@types/react-dom": "16.9.7",
"typescript": "3.8.3"
},
"peerDependencies": {
......
......@@ -2,7 +2,7 @@ import {Config, InitPromise} from '../types';
import I18nextBrowserLanguageDetector from 'i18next-browser-languagedetector';
import i18n from 'i18next';
import i18nextXHRBackend from 'i18next-xhr-backend';
import i18nextHttpBackend from 'i18next-http-backend';
import isNode from 'detect-node';
export default (config: Config) => {
......@@ -10,8 +10,8 @@ export default (config: Config) => {
if (!i18n.isInitialized) {
if (isNode) {
const i18nextNodeBackend = eval("require('i18next-node-fs-backend')");
const i18nextMiddleware = eval("require('i18next-express-middleware')");
const i18nextNodeBackend = eval('require("i18next-fs-backend")');
const i18nextMiddleware = eval('require("i18next-http-middleware")');
i18n.use(i18nextNodeBackend);
if (config.serverLanguageDetection) {
const serverDetectors = new i18nextMiddleware.LanguageDetector();
......@@ -19,7 +19,7 @@ export default (config: Config) => {
i18n.use(serverDetectors);
}
} else {
i18n.use(i18nextXHRBackend);
i18n.use(i18nextHttpBackend);
if (config.browserLanguageDetection) {
const browserDetectors = new I18nextBrowserLanguageDetector();
config.customDetectors?.forEach(detector => browserDetectors.addDetector(detector));
......
......@@ -101,7 +101,7 @@ export const appWithTranslation = function (this: NextI18Next, WrappedComponent:
if (req && !req.i18n) {
const {router} = ctx;
const result = router.asPath.match(/^\/(.*?)\//);
const lng = result ? result[1] : process.env.DEFAULT_LANGUAGE;
const lng = result ? result[1] : config.defaultLanguage;
req.i18n = i18n.cloneInstance({initImmediate: false, lng});
const res = ctx.ctx.res as (NextPageContext['res'] & I18nRes) | undefined;
const setContextLocale = (lng?: string) => {
......
......@@ -10,7 +10,7 @@ import {
} from '../utils';
import NextI18Next from '../index';
import i18nextMiddleware from 'i18next-express-middleware';
import i18nextMiddleware from 'i18next-http-middleware';
import pathMatch from 'path-match';
const route = pathMatch();
......
......@@ -2,6 +2,7 @@
declare module 'detect-node';
declare module 'path-match';
declare module 'i18next-http-middleware';
import * as React from 'react';
......@@ -73,6 +74,7 @@ declare global {
// eslint-disable-next-line @typescript-eslint/no-namespace
namespace Express {
interface Request {
i18n: I18n;
lng?: string;
}
}
......
......@@ -14,7 +14,8 @@ export default async (req: Request, res: Response) => {
const index = (+req.query.index ?? 0) % images.length;
const result = await fetch(images[index]);
if (result.headers.has('Content-Type')) {
res.type(result.headers.get('Content-Type') as string);
const ct = result.headers.get('Content-Type');
res.setHeader('Content-Type', ct);
}
return result.arrayBuffer();
};
......@@ -3,23 +3,23 @@ export default {
'input_reshape/input/image/7',
'input_reshape/input/image/4',
'input_reshape/input/image/5',
'input_reshape/input/image/2',
'input_reshape/input/image/3',
'input_reshape/input/image/0',
'input_reshape/input/image/1',
'input_reshape/input/image/8',
'input_reshape/input/image/9'
'hahaha/input/image/2',
'hahaha/input/image/3',
'hahaha/input/image/0',
'ohehe/input/image/1',
'😼/input/image/8',
'😼/input/image/9'
],
train: [
'input_reshape/input/image/6',
'input_reshape/input/image/7',
'input_reshape/input/image/4',
'input_reshape/input/image/5',
'input_reshape/input/image/2',
'input_reshape/input/image/3',
'input_reshape/input/image/0',
'input_reshape/input/image/1',
'input_reshape/input/image/8',
'input_reshape/input/image/9'
'hahaha/input/image/2',
'hahaha/input/image/3',
'oheihei/input/image/0',
'oheihei/input/image/1',
'😼/input/image/8',
'😼/input/image/9'
]
};
export default {
test: ['layer2/biases/summaries/mean'],
train: ['layer2/biases/summaries/mean', 'layer2/biases/summaries/accuracy', 'layer2/biases/summaries/cost']
test: ['layer2/biases/summaries/mean', 'test/1234', 'another'],
train: [
'layer2/biases/summaries/mean',
'layer2/biases/summaries/accuracy',
'layer2/biases/summaries/cost',
'test/431',
'others'
]
};
......@@ -38,7 +38,7 @@
"devDependencies": {
"@types/express": "4.17.6",
"@types/faker": "4.1.11",
"@types/node": "13.11.1",
"@types/node": "13.13.5",
"typescript": "3.8.3"
},
"peerDependencies": {
......
......@@ -40,23 +40,23 @@
"@visualdl/i18n": "^2.0.0-beta.32",
"express": "4.17.1",
"http-proxy-middleware": "1.0.3",
"next": "9.3.4",
"pm2": "4.2.3"
"next": "9.3.6",
"pm2": "4.4.0"
},
"devDependencies": {
"@types/express": "4.17.6",
"@types/node": "13.11.1",
"@types/node": "13.13.5",
"@types/shelljs": "0.8.7",
"@types/webpack": "4.41.10",
"@types/webpack": "4.41.12",
"@types/webpack-dev-middleware": "3.7.0",
"@visualdl/mock": "^2.0.0-beta.32",
"cross-env": "7.0.2",
"nodemon": "2.0.3",
"shelljs": "0.8.3",
"ts-loader": "6.2.2",
"ts-node": "8.8.2",
"shelljs": "0.8.4",
"ts-loader": "7.0.3",
"ts-node": "8.10.1",
"typescript": "3.8.3",
"webpack": "4.42.1",
"webpack": "4.43.0",
"webpack-cli": "3.3.11",
"webpack-dev-middleware": "3.7.2"
},
......
......@@ -31,12 +31,12 @@
"test": "echo \"Error: no test specified\" && exit 0"
},
"devDependencies": {
"@types/node": "13.11.1",
"@types/node": "13.13.5",
"@types/rimraf": "3.0.0",
"@visualdl/core": "^2.0.0-beta.32",
"cross-env": "7.0.2",
"rimraf": "3.0.2",
"ts-node": "8.8.2",
"ts-node": "8.10.1",
"typescript": "3.8.3"
},
"engines": {
......
此差异已折叠。
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册