未验证 提交 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 = { ...@@ -33,6 +33,7 @@ module.exports = {
extends: [ extends: [
'plugin:@typescript-eslint/recommended', 'plugin:@typescript-eslint/recommended',
'plugin:react/recommended', 'plugin:react/recommended',
'plugin:react-hooks/recommended',
'prettier/@typescript-eslint', 'prettier/@typescript-eslint',
'plugin:prettier/recommended' 'plugin:prettier/recommended'
], ],
...@@ -44,7 +45,6 @@ module.exports = { ...@@ -44,7 +45,6 @@ module.exports = {
ecmaVersion: 2018, ecmaVersion: 2018,
sourceType: 'module' sourceType: 'module'
}, },
plugins: ['react-hooks'],
settings: { settings: {
react: { react: {
version: 'detect' version: 'detect'
...@@ -54,9 +54,7 @@ module.exports = { ...@@ -54,9 +54,7 @@ module.exports = {
'@typescript-eslint/explicit-function-return-type': 'off', '@typescript-eslint/explicit-function-return-type': 'off',
'@typescript-eslint/no-explicit-any': 'error', '@typescript-eslint/no-explicit-any': 'error',
'react/prop-types': 'off', 'react/prop-types': 'off',
'react/react-in-jsx-scope': 'off', 'react/react-in-jsx-scope': 'off'
'react-hooks/rules-of-hooks': 'error',
'react-hooks/exhaustive-deps': 'warn'
} }
} }
] ]
......
...@@ -3,19 +3,24 @@ const fs = require('fs'); ...@@ -3,19 +3,24 @@ const fs = require('fs');
module.exports = { module.exports = {
// lint all files when global package.json or eslint config changes. // 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. // check types when ts file or package.json changes.
'packages/**/(*.ts?(x)|package.json)': filenames => './(packages/*/package.json|packages/*/**/*.ts?(x))': filenames =>
[ [
...new Set( ...new Set(
filenames.map( filenames.map(filename => path.relative(path.join(__dirname, 'packages'), filename).split(path.sep)[0])
filename => path.relative(path.join(process.cwd(), 'packages'), filename).split(path.sep)[0]
)
) )
] ]
.map(p => path.join(process.cwd(), 'packages', p, 'tsconfig.json')) .map(p => path.join(__dirname, 'packages', p, 'tsconfig.json'))
.filter(p => fs.statSync(p).isFile()) .filter(p => {
try {
return fs.statSync(p).isFile();
} catch (e) {
return false;
}
})
.map(p => `tsc -p ${p} --noEmit`), .map(p => `tsc -p ${p} --noEmit`),
// lint changed files // lint changed files
......
...@@ -38,17 +38,17 @@ ...@@ -38,17 +38,17 @@
"version": "yarn format && git add -A" "version": "yarn format && git add -A"
}, },
"devDependencies": { "devDependencies": {
"@typescript-eslint/eslint-plugin": "2.27.0", "@typescript-eslint/eslint-plugin": "2.31.0",
"@typescript-eslint/parser": "2.27.0", "@typescript-eslint/parser": "2.31.0",
"eslint": "6.8.0", "eslint": "6.8.0",
"eslint-config-prettier": "6.10.1", "eslint-config-prettier": "6.11.0",
"eslint-plugin-prettier": "3.1.2", "eslint-plugin-prettier": "3.1.3",
"eslint-plugin-react": "7.19.0", "eslint-plugin-react": "7.19.0",
"eslint-plugin-react-hooks": "3.0.0", "eslint-plugin-react-hooks": "4.0.0",
"husky": "4.2.5", "husky": "4.2.5",
"lerna": "^3.20.2", "lerna": "3.20.2",
"lint-staged": "10.1.3", "lint-staged": "10.2.2",
"prettier": "2.0.4", "prettier": "2.0.5",
"rimraf": "3.0.2", "rimraf": "3.0.2",
"typescript": "3.8.3", "typescript": "3.8.3",
"yarn": "1.22.4" "yarn": "1.22.4"
......
...@@ -30,11 +30,11 @@ ...@@ -30,11 +30,11 @@
}, },
"dependencies": { "dependencies": {
"@visualdl/server": "^2.0.0-beta.32", "@visualdl/server": "^2.0.0-beta.32",
"pm2": "4.2.3" "pm2": "4.4.0"
}, },
"devDependencies": { "devDependencies": {
"electron": "8.2.1", "electron": "8.2.5",
"electron-builder": "22.4.1" "electron-builder": "22.6.0"
}, },
"engines": { "engines": {
"node": ">=10", "node": ">=10",
......
...@@ -36,15 +36,15 @@ ...@@ -36,15 +36,15 @@
"dependencies": { "dependencies": {
"@visualdl/server": "^2.0.0-beta.32", "@visualdl/server": "^2.0.0-beta.32",
"open": "7.0.3", "open": "7.0.3",
"ora": "4.0.3", "ora": "4.0.4",
"pm2": "4.2.3", "pm2": "4.4.0",
"yargs": "15.3.1" "yargs": "15.3.1"
}, },
"devDependencies": { "devDependencies": {
"@types/node": "13.11.1", "@types/node": "13.13.5",
"@types/yargs": "15.0.4", "@types/yargs": "15.0.4",
"cross-env": "7.0.2", "cross-env": "7.0.2",
"ts-node": "8.8.2", "ts-node": "8.10.1",
"typescript": "3.8.3" "typescript": "3.8.3"
}, },
"engines": { "engines": {
......
import React, {FunctionComponent} from 'react'; import React, {FunctionComponent} from 'react';
import { import {
WithStyled, WithStyled,
borderActiveColor,
borderColor,
borderFocusedColor,
borderRadius,
dangerActiveColor, dangerActiveColor,
dangerColor, dangerColor,
dangerFocusedColor, dangerFocusedColor,
...@@ -10,7 +14,10 @@ import { ...@@ -10,7 +14,10 @@ import {
primaryActiveColor, primaryActiveColor,
primaryColor, primaryColor,
primaryFocusedColor, primaryFocusedColor,
sameBorder,
textColor,
textInvertColor, textInvertColor,
textLighterColor,
transitionProps transitionProps
} from '~/utils/style'; } from '~/utils/style';
...@@ -31,26 +38,36 @@ const colors = { ...@@ -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; cursor: pointer;
height: ${height}; height: ${height};
line-height: ${height}; line-height: ${height};
border-radius: ${half(height)}; border-radius: ${props => (props.rounded ? half(height) : borderRadius)};
background-color: ${props => colors[props.type].default}; ${props => (props.type ? '' : sameBorder({color: borderColor}))}
color: ${textInvertColor}; background-color: ${props => (props.type ? colors[props.type].default : 'transparent')};
display: block; 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; text-align: center;
${transitionProps('background-color')} padding: 0 ${em(20)};
${transitionProps(['background-color', 'border-color'])}
${ellipsis()} ${ellipsis()}
&:hover, ${props =>
&:focus { props.disabled
background-color: ${props => colors[props.type].focused}; ? ''
} : `
&:hover,
&:focus {
${props.type ? '' : sameBorder({color: borderFocusedColor})}
background-color: ${props.type ? colors[props.type].focused : 'transparent'};
}
&:active { &: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)` const Icon = styled(RawIcon)`
...@@ -58,13 +75,23 @@ const Icon = styled(RawIcon)` ...@@ -58,13 +75,23 @@ const Icon = styled(RawIcon)`
`; `;
type ButtonProps = { type ButtonProps = {
rounded?: boolean;
icon?: string; icon?: string;
type?: keyof typeof colors; type?: keyof typeof colors;
disabled?: boolean;
onClick?: () => unknown; onClick?: () => unknown;
}; };
const Button: FunctionComponent<ButtonProps & WithStyled> = ({icon, type, children, className, onClick}) => ( const Button: FunctionComponent<ButtonProps & WithStyled> = ({
<Wrapper className={className} onClick={onClick} type={type || 'primary'}> disabled,
rounded,
icon,
type,
children,
className,
onClick
}) => (
<Wrapper className={className} onClick={onClick} type={type} rounded={rounded} disabled={disabled}>
{icon && <Icon type={icon}></Icon>} {icon && <Icon type={icon}></Icon>}
{children} {children}
</Wrapper> </Wrapper>
......
import React, {FunctionComponent} from 'react'; import React, {FunctionComponent, useCallback, useEffect, useState} from 'react';
import { import {
WithStyled, WithStyled,
backgroundColor, backgroundColor,
borderRadius, borderRadius,
headerHeight,
math, math,
primaryColor, primaryColor,
rem,
sameBorder, sameBorder,
size,
transitionProps transitionProps
} from '~/utils/style'; } from '~/utils/style';
import ee from '~/utils/event';
import styled from 'styled-components'; 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}; background-color: ${backgroundColor};
${sameBorder({radius: math(`${borderRadius} * 2`)})} ${sameBorder({radius: math(`${borderRadius} * 2`)})}
${transitionProps(['border-color', 'box-shadow'])} ${transitionProps(['border-color', 'box-shadow'])}
position: relative;
&:hover { &:hover {
border-color: ${primaryColor}; border-color: ${primaryColor};
...@@ -22,8 +32,34 @@ const Div = styled.div` ...@@ -22,8 +32,34 @@ const Div = styled.div`
} }
`; `;
const Chart: FunctionComponent<WithStyled> = ({className, children}) => { type ChartProps = {
return <Div className={className}>{children}</Div>; 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; 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 React, {FunctionComponent, PropsWithChildren, useCallback, useEffect, useMemo, useState} from 'react';
import {WithStyled, primaryColor, rem} from '~/utils/style'; import {Trans, useTranslation} from '~/utils/i18n';
import {WithStyled, backgroundColor, headerHeight, link, primaryColor, rem, textLighterColor} from '~/utils/style';
import BarLoader from 'react-spinners/BarLoader'; import BarLoader from 'react-spinners/BarLoader';
import Chart from '~/components/Chart'; import Chart from '~/components/Chart';
import ChartCollapse from '~/components/ChartCollapse';
import Pagination from '~/components/Pagination'; import Pagination from '~/components/Pagination';
import SearchInput from '~/components/SearchInput';
import groupBy from 'lodash/groupBy';
import styled from 'styled-components'; 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` const Wrapper = styled.div`
display: flex; display: flex;
...@@ -13,6 +21,7 @@ const Wrapper = styled.div` ...@@ -13,6 +21,7 @@ const Wrapper = styled.div`
justify-content: flex-start; justify-content: flex-start;
align-items: stretch; align-items: stretch;
align-content: flex-start; align-content: flex-start;
margin-bottom: -${rem(20)};
> * { > * {
margin: 0 ${rem(20)} ${rem(20)} 0; margin: 0 ${rem(20)} ${rem(20)} 0;
...@@ -21,6 +30,11 @@ const Wrapper = styled.div` ...@@ -21,6 +30,11 @@ const Wrapper = styled.div`
} }
`; `;
const Search = styled.div`
width: ${rem(280)};
margin-bottom: ${rem(16)};
`;
const Loading = styled.div` const Loading = styled.div`
display: flex; display: flex;
justify-content: center; justify-content: center;
...@@ -29,49 +43,168 @@ const Loading = styled.div` ...@@ -29,49 +43,168 @@ const Loading = styled.div`
padding: ${rem(40)} 0; padding: ${rem(40)} 0;
`; `;
const Empty = styled.div` const Empty = styled.div<{height?: string}>`
display: flex; width: 100%;
justify-content: center; text-align: center;
align-items: center; font-size: ${rem(16)};
font-size: ${rem(20)}; color: ${textLighterColor};
height: ${rem(150)}; line-height: ${rem(24)};
flex-grow: 1; 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 type Item = {
// eslint-disable-next-line @typescript-eslint/no-explicit-any id?: string;
type ChartPageProps<T = any> = { label: string;
};
export interface WithChart<T extends Item> {
(item: T & {cid: symbol}, index: number): React.ReactNode;
}
type ChartPageProps<T extends Item> = {
items?: T[]; items?: T[];
running?: boolean;
loading?: 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 {t} = useTranslation('common');
const [page, setPage] = useState(1);
const pageSize = 12; 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 ( const pageMatchedTags = useMemo(() => matchedTags?.slice((page - 1) * pageSize, page * pageSize) ?? [], [
<div className={className}> matchedTags,
{loading ? ( 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> <Loading>
<BarLoader color={primaryColor} width="20%" height="4px" /> <BarLoader color={primaryColor} width="20%" height="4px" />
</Loading> </Loading>
) : ( ) : (
<Wrapper> <Wrapper>
{pageItems.length ? ( {charts.length ? (
pageItems.map((item, index) => <Chart key={index}>{withChart?.(item)}</Chart>) 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> </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> </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}> ...@@ -47,7 +47,7 @@ const Inner = styled.div<{checked?: boolean; size?: string; disabled?: boolean}>
flex-shrink: 0; flex-shrink: 0;
${props => size(math(`${checkSize} * ${props.size === 'small' ? 0.875 : 1}`))} ${props => size(math(`${checkSize} * ${props.size === 'small' ? 0.875 : 1}`))}
margin: ${half(`${height} - ${checkSize}`)} 0; margin: ${half(`${height} - ${checkSize}`)} 0;
margin-right: ${em(4)}; margin-right: ${em(10)};
${props => sameBorder({color: props.disabled || !props.checked ? textLighterColor : primaryColor})}; ${props => sameBorder({color: props.disabled || !props.checked ? textLighterColor : primaryColor})};
background-color: ${props => background-color: ${props =>
props.disabled props.disabled
......
...@@ -5,7 +5,6 @@ import HashLoader from 'react-spinners/HashLoader'; ...@@ -5,7 +5,6 @@ import HashLoader from 'react-spinners/HashLoader';
import styled from 'styled-components'; import styled from 'styled-components';
const margin = rem(20); const margin = rem(20);
const padding = rem(20);
const Section = styled.section` const Section = styled.section`
/* trigger BFC */ /* trigger BFC */
...@@ -15,13 +14,10 @@ const Section = styled.section` ...@@ -15,13 +14,10 @@ const Section = styled.section`
const Article = styled.article<{aside?: boolean}>` const Article = styled.article<{aside?: boolean}>`
margin: ${margin}; margin: ${margin};
margin-right: ${props => (props.aside ? math(`${margin} + ${asideWidth}`) : margin)}; margin-right: ${props => (props.aside ? math(`${margin} + ${asideWidth}`) : margin)};
padding: ${padding};
background-color: ${backgroundColor};
min-height: calc(100vh - ${math(`${margin} * 2 + ${headerHeight}`)}); min-height: calc(100vh - ${math(`${margin} * 2 + ${headerHeight}`)});
`; `;
const Aside = styled.aside` const Aside = styled.aside`
padding: ${padding};
background-color: ${backgroundColor}; background-color: ${backgroundColor};
${size(`calc(100vh - ${headerHeight})`, asideWidth)} ${size(`calc(100vh - ${headerHeight})`, asideWidth)}
${position('fixed', headerHeight, 0, null, null)} ${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 GridLoader from 'react-spinners/GridLoader';
import {blobFetcher} from '~/utils/fetch'; import mime from 'mime-types';
import {primaryColor} from '~/utils/style'; import {primaryColor} from '~/utils/style';
import {saveAs} from 'file-saver';
import useRequest from '~/hooks/useRequest'; import useRequest from '~/hooks/useRequest';
import {useTranslation} from '~/utils/i18n'; import {useTranslation} from '~/utils/i18n';
export type ImageRef = {
save(filename: string): void;
};
type ImageProps = { type ImageProps = {
src?: string; src?: string;
cache?: number; cache?: number;
}; };
const Image: FunctionComponent<ImageProps> = ({src, cache}) => { const Image = React.forwardRef<ImageRef, ImageProps>(({src, cache}, ref) => {
const {t} = useTranslation('common'); const {t} = useTranslation('common');
const [url, setUrl] = useState(''); 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 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 // use useLayoutEffect hook to prevent image render after url revoked
useLayoutEffect(() => { useLayoutEffect(() => {
if (process.browser && data) { if (process.browser && data) {
let objectUrl: string | null = null; let objectUrl: string | null = null;
objectUrl = URL.createObjectURL(data); objectUrl = URL.createObjectURL(data.data);
setUrl(objectUrl); setUrl(objectUrl);
return () => { return () => {
objectUrl && URL.revokeObjectURL(objectUrl); objectUrl && URL.revokeObjectURL(objectUrl);
...@@ -41,6 +56,6 @@ const Image: FunctionComponent<ImageProps> = ({src, cache}) => { ...@@ -41,6 +56,6 @@ const Image: FunctionComponent<ImageProps> = ({src, cache}) => {
} }
return <img src={url} />; return <img src={url} />;
}; });
export default Image; export default Image;
...@@ -32,7 +32,9 @@ export type InputProps = { ...@@ -32,7 +32,9 @@ export type InputProps = {
onChange?: (value: string) => unknown; 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 <StyledInput
rounded={rounded} rounded={rounded}
placeholder={placeholder} placeholder={placeholder}
...@@ -40,7 +42,8 @@ const Input: FunctionComponent<InputProps & WithStyled> = ({rounded, placeholder ...@@ -40,7 +42,8 @@ const Input: FunctionComponent<InputProps & WithStyled> = ({rounded, placeholder
type="text" type="text"
className={className} className={className}
onChange={e => onChange?.(e.target.value)} onChange={e => onChange?.(e.target.value)}
></StyledInput> {...props}
/>
); );
export default Input; 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 * 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 {WithStyled, position, primaryColor, size} from '~/utils/style';
import {EChartOption} from 'echarts'; import {EChartOption} from 'echarts';
import GridLoader from 'react-spinners/GridLoader'; import GridLoader from 'react-spinners/GridLoader';
import {dataURL2Blob} from '~/utils/image';
import {formatTime} from '~/utils'; import {formatTime} from '~/utils';
import {saveAs} from 'file-saver';
import styled from 'styled-components'; import styled from 'styled-components';
import useECharts from '~/hooks/useECharts'; import useECharts from '~/hooks/useECharts';
import {useTranslation} from '~/utils/i18n'; import {useTranslation} from '~/utils/i18n';
...@@ -37,98 +39,125 @@ type LineChartProps = { ...@@ -37,98 +39,125 @@ type LineChartProps = {
data?: Partial<NonNullable<EChartOption<EChartOption.SeriesLine>['series']>>; data?: Partial<NonNullable<EChartOption<EChartOption.SeriesLine>['series']>>;
xAxis?: string; xAxis?: string;
yAxis?: string; yAxis?: string;
type?: EChartOption.BasicComponents.CartesianAxis.Type; xType?: EChartOption.BasicComponents.CartesianAxis.Type;
yType?: EChartOption.BasicComponents.CartesianAxis.Type;
xRange?: Range; xRange?: Range;
yRange?: Range; yRange?: Range;
tooltip?: string | EChartOption.Tooltip.Formatter; tooltip?: string | EChartOption.Tooltip.Formatter;
loading?: boolean; loading?: boolean;
}; };
const LineChart: FunctionComponent<LineChartProps & WithStyled> = ({ export type LineChartRef = {
title, restore(): void;
legend, saveAsImage(): void;
data,
xAxis,
yAxis,
type,
xRange,
yRange,
tooltip,
loading,
className
}) => {
const {i18n} = useTranslation();
const {ref, echart} = useECharts<HTMLDivElement>({
loading: !!loading,
zoom: true
});
const xAxisFormatter = useCallback(
(value: number) => (type === 'time' ? formatTime(value, i18n.language, 'LTS') : value),
[type, i18n.language]
);
useEffect(() => {
if (process.browser) {
echart?.current?.setOption(
{
color: chart.color,
title: {
...chart.title,
text: title ?? ''
},
tooltip: {
...chart.tooltip,
...(tooltip
? {
formatter: tooltip
}
: {})
},
toolbox: chart.toolbox,
legend: {
...chart.legend,
data: legend ?? []
},
grid: chart.grid,
xAxis: {
...chart.xAxis,
name: xAxis || '',
type: type || 'value',
axisLabel: {
...chart.xAxis.axisLabel,
formatter: xAxisFormatter
},
...(xRange || {})
},
yAxis: {
...chart.yAxis,
name: yAxis || '',
...(yRange || {})
},
series: data?.map(item => ({
...chart.series,
// show symbol if there is only one point
showSymbol: (item?.data?.length ?? 0) <= 1,
...item
}))
} as EChartOption,
{notMerge: true}
);
}
}, [data, title, legend, xAxis, yAxis, type, xAxisFormatter, xRange, yRange, tooltip, echart]);
return (
<Wrapper className={className}>
{!echart && (
<div className="loading">
<GridLoader color={primaryColor} size="10px" />
</div>
)}
<div className="echarts" ref={ref}></div>
</Wrapper>
);
}; };
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: 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) => (xType === 'time' ? formatTime(value, i18n.language, 'LTS') : value),
[xType, i18n.language]
);
useEffect(() => {
if (process.browser) {
echart?.current?.setOption(
{
color: chart.color,
title: {
...chart.title,
text: title ?? ''
},
tooltip: {
...chart.tooltip,
...(tooltip
? {
formatter: tooltip
}
: {})
},
toolbox: chart.toolbox,
legend: {
...chart.legend,
data: legend ?? []
},
grid: chart.grid,
xAxis: {
...chart.xAxis,
name: xAxis || '',
type: xType || 'value',
axisLabel: {
...chart.xAxis.axisLabel,
formatter: xAxisFormatter
},
...(xRange || {})
},
yAxis: {
...chart.yAxis,
name: yAxis || '',
type: yType || 'value',
...(yRange || {})
},
series: data?.map(item => ({
...chart.series,
// show symbol if there is only one point
showSymbol: (item?.data?.length ?? 0) <= 1,
...item
}))
} as EChartOption,
{notMerge: true}
);
}
}, [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 ref={wrapperRef} className={className}>
{!echart && (
<div className="loading">
<GridLoader color={primaryColor} size="10px" />
</div>
)}
<div className="echarts" ref={echartRef}></div>
</Wrapper>
);
}
);
export default LineChart; export default LineChart;
import {Link, useTranslation} from '~/utils/i18n'; import {Link, config, i18n, useTranslation} from '~/utils/i18n';
import React, {FunctionComponent} from 'react'; import React, {FunctionComponent} from 'react';
import { import {
border, border,
...@@ -11,6 +11,9 @@ import { ...@@ -11,6 +11,9 @@ import {
transitionProps transitionProps
} from '~/utils/style'; } from '~/utils/style';
import Icon from '~/components/Icon';
import Language from '~/components/Language';
import ee from '~/utils/event';
import intersection from 'lodash/intersection'; import intersection from 'lodash/intersection';
import styled from 'styled-components'; import styled from 'styled-components';
import {useRouter} from 'next/router'; import {useRouter} from 'next/router';
...@@ -30,8 +33,21 @@ const Nav = styled.nav` ...@@ -30,8 +33,21 @@ const Nav = styled.nav`
${size('100%')} ${size('100%')}
padding: 0 ${rem(20)}; padding: 0 ${rem(20)};
display: flex; display: flex;
justify-content: flex-start; justify-content: space-between;
align-items: center; 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` const Logo = styled.a`
...@@ -52,20 +68,21 @@ 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)}; padding: 0 ${rem(20)};
height: 100%; height: 100%;
display: inline-flex; display: inline-flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
background-color: ${navbarBackgroundColor}; background-color: ${navbarBackgroundColor};
cursor: pointer;
${transitionProps('background-color')} ${transitionProps('background-color')}
&:hover { &:hover {
background-color: ${navbarHoverBackgroundColor}; background-color: ${navbarHoverBackgroundColor};
} }
> span { > .nav-text {
padding: ${rem(10)} 0 ${rem(7)}; padding: ${rem(10)} 0 ${rem(7)};
${props => border('bottom', rem(3), 'solid', props.active ? navbarHighlightColor : 'transparent')} ${props => border('bottom', rem(3), 'solid', props.active ? navbarHighlightColor : 'transparent')}
${transitionProps('border-bottom')} ${transitionProps('border-bottom')}
...@@ -73,27 +90,45 @@ const NavItem = styled.a<{active: boolean}>` ...@@ -73,27 +90,45 @@ 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 Navbar: FunctionComponent = () => {
const {t} = useTranslation('common'); const {t} = useTranslation('common');
const {pathname} = useRouter(); const {pathname} = useRouter();
return ( return (
<Nav> <Nav>
<Logo href={process.env.PUBLIC_PATH || '/'}> <div className="left">
<img alt="PaddlePaddle" src={`${process.env.PUBLIC_PATH}/images/logo.svg`} /> <Logo href={process.env.PUBLIC_PATH || '/'}>
<span>VisualDL</span> <img alt="PaddlePaddle" src={`${process.env.PUBLIC_PATH}/images/logo.svg`} />
</Logo> <span>VisualDL</span>
{navItems.map(name => { </Logo>
const href = `/${name}`; {navItems.map(name => {
return ( const href = `/${name}`;
// https://nextjs.org/docs/api-reference/next/link#if-the-child-is-a-custom-component-that-wraps-an-a-tag return (
<Link href={href} key={name} passHref> // https://nextjs.org/docs/api-reference/next/link#if-the-child-is-a-custom-component-that-wraps-an-a-tag
<NavItem active={pathname === href}> <Link href={href} key={name} passHref>
<span>{t(name)}</span> <NavItem active={pathname === href}>
</NavItem> <span className="nav-text">{t(name)}</span>
</Link> </NavItem>
); </Link>
})} );
})}
</div>
<div className="right">
<NavItem onClick={changeLanguage}>
<Language />
</NavItem>
<NavItem onClick={() => ee.emit('refresh-running')}>
<Icon type="refresh" />
</NavItem>
</div>
</Nav> </Nav>
); );
}; };
......
// cSpell:words hellip // cSpell:words hellip
import React, {FunctionComponent, useCallback, useMemo} from 'react'; import React, {FunctionComponent, useCallback, useEffect, useState} from 'react';
import { import {WithStyled, em} from '~/utils/style';
WithStyled,
backgroundColor,
borderColor,
borderFocusedColor,
em,
primaryColor,
sameBorder,
size,
textColor,
textInvertColor,
transitionProps
} from '~/utils/style';
import Button from '~/components/Button';
import Input from '~/components/Input';
import styled from 'styled-components'; import styled from 'styled-components';
import {useTranslation} from '~/utils/i18n';
const height = em(36);
const Wrapper = styled.nav` const Wrapper = styled.nav`
display: flex; display: flex;
user-select: none; justify-content: space-between;
`; align-items: center;
const Ul = styled.ul` > div {
display: inline-flex; > a:not(:last-child),
list-style: none; > span {
margin: 0; margin-right: ${em(15)};
padding: 0; }
`; > input {
width: ${em(80)};
const Li = styled.li` margin-right: ${em(6)};
list-style: none; }
margin-left: ${em(10)};
&:first-child {
margin-left: 0;
}
`;
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 = { type PaginationProps = {
page: number; page?: number;
total: number; total: number;
onChange?: (page: number) => unknown; onChange?: (page: number) => unknown;
}; };
const Pagination: FunctionComponent<PaginationProps & WithStyled> = ({page, total, className, onChange}) => { const Pagination: FunctionComponent<PaginationProps & WithStyled> = ({page, total, className, onChange}) => {
const padding = 2; const {t} = useTranslation('common');
const around = 2;
const [currentPage, setCurrentPage] = useState(page ?? 1);
const startEllipsis = useMemo(() => page - padding - around - 1 > 0, [page]); const [jumpPage, setJumpPage] = useState('');
const endEllipsis = useMemo(() => page + padding + around < total, [page, total]);
const start = useMemo( useEffect(() => setCurrentPage(page ?? 1), [page]);
() =>
page - around - 1 <= 0 ? [] : Array.from(new Array(Math.min(padding, page - around - 1)), (_v, i) => i + 1), const setPage = useCallback(
[page] (value: unknown) => {
); const p = 'number' === typeof value ? value : Number.parseInt(value + '');
const end = useMemo( if (Number.isNaN(p) || p > total || p < 1 || p === currentPage) {
() => return;
page + around >= total }
? [] setCurrentPage(p);
: Array.from( setJumpPage('');
new Array(Math.min(padding, total - page - around)), onChange?.(p);
(_v, i) => total - padding + i + 1 + Math.max(padding - total + page + around, 0) },
), [currentPage, onChange, total]
[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 genLink = useCallback(
(arr: number[]) =>
arr.map(i => (
<Li key={i}>
<A onClick={() => onChange?.(i)}>{i}</A>
</Li>
)),
[onChange]
);
const hellip = (
<Li>
<Span>&hellip;</Span>
</Li>
); );
return ( return (
<Wrapper className={className}> <Wrapper className={className}>
<Ul> <div>
{genLink(start)} <Button disabled={currentPage <= 1} onClick={() => setPage(currentPage - 1)}>
{startEllipsis && hellip} {t('previous-page')}
{genLink(before)} </Button>
<Li> <Button disabled={currentPage >= total} onClick={() => setPage(currentPage + 1)}>
<A current>{page}</A> {t('next-page')}
</Li> </Button>
{genLink(after)} </div>
{endEllipsis && hellip} <div>
{genLink(end)} <span>{t('total-page', {count: total})}</span>
</Ul> <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> </Wrapper>
); );
}; };
......
import {EventContext, ValueContext} from '~/components/RadioGroup'; import {EventContext, ValueContext} from '~/components/RadioGroup';
import React, {FunctionComponent, useCallback, useContext} from 'react'; import React, {FunctionComponent, PropsWithChildren, useCallback, useContext} from 'react';
import { import {
WithStyled, WithStyled,
backgroundColor, backgroundColor,
...@@ -29,6 +29,7 @@ const Button = styled.a<{selected?: boolean}>` ...@@ -29,6 +29,7 @@ const Button = styled.a<{selected?: boolean}>`
height: ${height}; height: ${height};
line-height: calc(${height} - 2px); line-height: calc(${height} - 2px);
min-width: ${minWidth}; min-width: ${minWidth};
padding: 0 ${em(8)};
text-align: center; text-align: center;
${ellipsis(maxWidth)} ${ellipsis(maxWidth)}
${props => sameBorder({color: props.selected ? primaryColor : borderColor})}; ${props => sameBorder({color: props.selected ? primaryColor : borderColor})};
...@@ -54,19 +55,19 @@ const Button = styled.a<{selected?: boolean}>` ...@@ -54,19 +55,19 @@ const Button = styled.a<{selected?: boolean}>`
} }
`; `;
type RadioButtonProps = { type RadioButtonProps<T> = {
selected?: boolean; selected?: boolean;
title?: string; title?: string;
value?: string | number | symbol; value?: T;
}; };
const RadioButton: FunctionComponent<RadioButtonProps & WithStyled> = ({ const RadioButton = <T extends unknown>({
className, className,
value, value,
selected, selected,
title, title,
children children
}) => { }: PropsWithChildren<RadioButtonProps<T>> & WithStyled): ReturnType<FunctionComponent> => {
const groupValue = useContext(ValueContext); const groupValue = useContext(ValueContext);
const onChange = useContext(EventContext); 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 {WithStyled} from '~/utils/style';
import styled from 'styled-components'; import styled from 'styled-components';
...@@ -12,19 +12,24 @@ const Wrapper = styled.div` ...@@ -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 // 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 = { type RadioGroupProps<T> = {
value?: string | number | symbol; value?: T;
onChange?: (value: string | number | symbol) => unknown; 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 [selected, setSelected] = useState(value);
const onSelectedChange = useCallback( const onSelectedChange = useCallback(
(value: string | number | symbol) => { (value: T) => {
setSelected(value); setSelected(value);
onChange?.(value); onChange?.(value);
}, },
...@@ -32,7 +37,7 @@ const RadioGroup: FunctionComponent<RadioGroupProps & WithStyled> = ({value, onC ...@@ -32,7 +37,7 @@ const RadioGroup: FunctionComponent<RadioGroupProps & WithStyled> = ({value, onC
); );
return ( return (
<EventContext.Provider value={onSelectedChange}> <EventContext.Provider value={v => onSelectedChange(v as T)}>
<ValueContext.Provider value={selected}> <ValueContext.Provider value={selected}>
<Wrapper className={className}>{children}</Wrapper> <Wrapper className={className}>{children}</Wrapper>
</ValueContext.Provider> </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 React, {FunctionComponent, useEffect, useState} from 'react';
import {WithStyled, rem} from '~/utils/style';
import Button from '~/components/Button'; 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 styled from 'styled-components';
import {useTranslation} from '~/utils/i18n'; 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)` const StyledButton = styled(Button)`
margin-top: ${rem(40)};
width: 100%;
text-transform: uppercase; text-transform: uppercase;
width: 100%;
`; `;
type RunningToggleProps = { type RunningToggleProps = {
...@@ -16,7 +32,7 @@ type RunningToggleProps = { ...@@ -16,7 +32,7 @@ type RunningToggleProps = {
onToggle?: (running: boolean) => unknown; onToggle?: (running: boolean) => unknown;
}; };
const RunningToggle: FunctionComponent<RunningToggleProps> = ({running, onToggle}) => { const RunningToggle: FunctionComponent<RunningToggleProps & WithStyled> = ({running, onToggle, className}) => {
const {t} = useTranslation('common'); const {t} = useTranslation('common');
const [state, setState] = useState(!!running); const [state, setState] = useState(!!running);
...@@ -25,10 +41,24 @@ const RunningToggle: FunctionComponent<RunningToggleProps> = ({running, onToggle ...@@ -25,10 +41,24 @@ const RunningToggle: FunctionComponent<RunningToggleProps> = ({running, onToggle
onToggle?.(state); onToggle?.(state);
}, [onToggle, state]); }, [onToggle, state]);
const [id] = useState(`running-toggle-tooltip-${nanoid()}`);
return ( return (
<StyledButton onClick={() => setState(s => !s)} type={state ? 'primary' : 'danger'}> <Wrapper className={className}>
{t(state ? 'running' : 'stopped')} <span>{t(state ? 'running' : 'stopped')}</span>
</StyledButton> <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 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 GridLoader from 'react-spinners/GridLoader';
import Image from '~/components/Image';
import StepSlider from '~/components/SamplesPage/StepSlider'; import StepSlider from '~/components/SamplesPage/StepSlider';
import {formatTime} from '~/utils';
import isEmpty from 'lodash/isEmpty'; import isEmpty from 'lodash/isEmpty';
import queryString from 'query-string'; import queryString from 'query-string';
import styled from 'styled-components'; import styled from 'styled-components';
import {useRunningRequest} from '~/hooks/useRequest'; import {useRunningRequest} from '~/hooks/useRequest';
import {useTranslation} from '~/utils/i18n'; import {useTranslation} from '~/utils/i18n';
const width = em(430);
const height = em(384);
const Wrapper = styled.div` const Wrapper = styled.div`
${size(height, width)} height: 100%;
padding: ${em(20)}; padding: ${em(20)};
padding-bottom: 0;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
justify-content: flex-start; justify-content: flex-start;
...@@ -51,10 +51,10 @@ const Title = styled.div` ...@@ -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-grow: 1;
flex-shrink: 1; flex-shrink: 1;
margin-top: ${em(20)}; margin: ${em(20)} 0;
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
...@@ -62,6 +62,8 @@ const Container = styled.div<{fit?: boolean}>` ...@@ -62,6 +62,8 @@ const Container = styled.div<{fit?: boolean}>`
> img { > img {
${size('100%')} ${size('100%')}
filter: brightness(${props => props.brightness ?? 1}) contrast(${props => props.contrast ?? 1});
${transitionProps('filter')}
object-fit: ${props => (props.fit ? 'contain' : 'scale-down')}; object-fit: ${props => (props.fit ? 'contain' : 'scale-down')};
flex-shrink: 1; flex-shrink: 1;
} }
...@@ -75,6 +77,8 @@ type ImageData = { ...@@ -75,6 +77,8 @@ type ImageData = {
type SampleChartProps = { type SampleChartProps = {
run: string; run: string;
tag: string; tag: string;
brightness?: number;
contrast?: number;
fit?: boolean; fit?: boolean;
running?: boolean; running?: boolean;
}; };
...@@ -84,14 +88,18 @@ const getImageUrl = (index: number, run: string, tag: string, wallTime: number): ...@@ -84,14 +88,18 @@ const getImageUrl = (index: number, run: string, tag: string, wallTime: number):
const cacheValidity = 5 * 60 * 1000; const cacheValidity = 5 * 60 * 1000;
const SampleChart: FunctionComponent<SampleChartProps> = ({run, tag, fit, running}) => { const SampleChart: FunctionComponent<SampleChartProps> = ({run, tag, brightness, contrast, fit, running}) => {
const {t} = useTranslation('common'); const {t, i18n} = useTranslation(['samples', 'common']);
const image = useRef<ImageRef>(null);
const {data, error, loading} = useRunningRequest<ImageData[]>( const {data, error, loading} = useRunningRequest<ImageData[]>(
`/images/list?${queryString.stringify({run, tag})}`, `/images/list?${queryString.stringify({run, tag})}`,
!!running !!running
); );
const steps = useMemo(() => data?.map(item => item.step) ?? [], [data]);
const [step, setStep] = useState(0); const [step, setStep] = useState(0);
const [src, setSrc] = useState<string>(); const [src, setSrc] = useState<string>();
...@@ -104,11 +112,13 @@ const SampleChart: FunctionComponent<SampleChartProps> = ({run, tag, fit, runnin ...@@ -104,11 +112,13 @@ const SampleChart: FunctionComponent<SampleChartProps> = ({run, tag, fit, runnin
cached.current = {}; cached.current = {};
}, [tag, run]); }, [tag, run]);
const wallTime = useMemo(() => data?.[step].wallTime ?? 0, [data, step]);
const cacheImageSrc = useCallback(() => { const cacheImageSrc = useCallback(() => {
if (!data) { if (!data) {
return; return;
} }
const imageUrl = getImageUrl(step, run, tag, data[step].wallTime); const imageUrl = getImageUrl(step, run, tag, wallTime);
cached.current[step] = { cached.current[step] = {
src: imageUrl, src: imageUrl,
timer: setTimeout(() => { timer: setTimeout(() => {
...@@ -116,7 +126,11 @@ const SampleChart: FunctionComponent<SampleChartProps> = ({run, tag, fit, runnin ...@@ -116,7 +126,11 @@ const SampleChart: FunctionComponent<SampleChartProps> = ({run, tag, fit, runnin
}, cacheValidity) }, cacheValidity)
}; };
setSrc(imageUrl); 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(() => { useEffect(() => {
if (cached.current[step]) { if (cached.current[step]) {
...@@ -139,12 +153,12 @@ const SampleChart: FunctionComponent<SampleChartProps> = ({run, tag, fit, runnin ...@@ -139,12 +153,12 @@ const SampleChart: FunctionComponent<SampleChartProps> = ({run, tag, fit, runnin
return <GridLoader color={primaryColor} size="10px" />; return <GridLoader color={primaryColor} size="10px" />;
} }
if (!data && error) { if (!data && error) {
return <span>{t('error')}</span>; return <span>{t('common:error')}</span>;
} }
if (isEmpty(data)) { 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]); }, [loading, error, data, step, src, t]);
return ( return (
...@@ -153,13 +167,21 @@ const SampleChart: FunctionComponent<SampleChartProps> = ({run, tag, fit, runnin ...@@ -153,13 +167,21 @@ const SampleChart: FunctionComponent<SampleChartProps> = ({run, tag, fit, runnin
<h4>{tag}</h4> <h4>{tag}</h4>
<span>{run}</span> <span>{run}</span>
</Title> </Title>
<StepSlider <StepSlider value={step} steps={steps} onChange={setStep} onChangeComplete={cacheImageSrc}>
value={step} {formatTime(wallTime, i18n.language)}
steps={data?.map(item => item.step) ?? []} </StepSlider>
onChange={setStep} <Container brightness={brightness} contrast={contrast} fit={fit}>
onChangeComplete={cacheImageSrc} {Content}
</Container>
<ChartToolbox
items={[
{
icon: 'download',
tooltip: t('download-image'),
onClick: saveImage
}
]}
/> />
<Container fit={fit}>{Content}</Container>
</Wrapper> </Wrapper>
); );
}; };
......
...@@ -6,9 +6,15 @@ import styled from 'styled-components'; ...@@ -6,9 +6,15 @@ import styled from 'styled-components';
import {useTranslation} from '~/utils/i18n'; import {useTranslation} from '~/utils/i18n';
const Label = styled.div` const Label = styled.div`
display: flex;
justify-content: space-between;
color: ${textLightColor}; color: ${textLightColor};
font-size: ${em(12)}; font-size: ${em(12)};
margin-bottom: ${em(5)}; margin-bottom: ${em(5)};
> :not(:first-child) {
flex-grow: 0;
}
`; `;
const FullWidthRangeSlider = styled(RangeSlider)` const FullWidthRangeSlider = styled(RangeSlider)`
...@@ -22,7 +28,7 @@ type StepSliderProps = { ...@@ -22,7 +28,7 @@ type StepSliderProps = {
onChangeComplete?: () => unknown; onChangeComplete?: () => unknown;
}; };
const StepSlider: FunctionComponent<StepSliderProps> = ({onChange, onChangeComplete, value, steps}) => { const StepSlider: FunctionComponent<StepSliderProps> = ({onChange, onChangeComplete, value, steps, children}) => {
const {t} = useTranslation('samples'); const {t} = useTranslation('samples');
const [step, setStep] = useState(value); const [step, setStep] = useState(value);
...@@ -38,7 +44,10 @@ const StepSlider: FunctionComponent<StepSliderProps> = ({onChange, onChangeCompl ...@@ -38,7 +44,10 @@ const StepSlider: FunctionComponent<StepSliderProps> = ({onChange, onChangeCompl
return ( return (
<> <>
<Label>{`${t('step')}: ${steps[step] ?? '...'}`}</Label> <Label>
<span>{`${t('step')}: ${steps[step] ?? '...'}`}</span>
{children && <span>{children}</span>}
</Label>
<FullWidthRangeSlider <FullWidthRangeSlider
min={0} min={0}
max={steps.length ? steps.length - 1 : 0} max={steps.length ? steps.length - 1 : 0}
......
...@@ -4,6 +4,7 @@ import { ...@@ -4,6 +4,7 @@ import {
RangeParams, RangeParams,
TransformParams, TransformParams,
chartData, chartData,
nearestPoint,
range, range,
singlePointRange, singlePointRange,
sortingMethodMap, sortingMethodMap,
...@@ -11,21 +12,21 @@ import { ...@@ -11,21 +12,21 @@ import {
transform, transform,
xAxisMap xAxisMap
} from '~/resource/scalars'; } from '~/resource/scalars';
import React, {FunctionComponent, useCallback, useMemo} from 'react'; import LineChart, {LineChartRef} from '~/components/LineChart';
import {em, size} from '~/utils/style'; 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 {EChartOption} from 'echarts';
import LineChart from '~/components/LineChart'; import {Run} from '~/types';
import {cycleFetcher} from '~/utils/fetch'; import {cycleFetcher} from '~/utils/fetch';
import ee from '~/utils/event';
import queryString from 'query-string'; import queryString from 'query-string';
import styled from 'styled-components'; import styled from 'styled-components';
import useHeavyWork from '~/hooks/useHeavyWork'; import useHeavyWork from '~/hooks/useHeavyWork';
import {useRunningRequest} from '~/hooks/useRequest'; import {useRunningRequest} from '~/hooks/useRequest';
import {useTranslation} from '~/utils/i18n'; import {useTranslation} from '~/utils/i18n';
const width = em(430);
const height = em(320);
const smoothWasm = () => const smoothWasm = () =>
import('@visualdl/wasm').then(({transform}) => (params: TransformParams) => import('@visualdl/wasm').then(({transform}) => (params: TransformParams) =>
(transform(params.datasets, params.smoothing) as unknown) as Dataset[] (transform(params.datasets, params.smoothing) as unknown) as Dataset[]
...@@ -38,28 +39,63 @@ const rangeWasm = () => ...@@ -38,28 +39,63 @@ const rangeWasm = () =>
const smoothWorker = () => new Worker('~/worker/scalars/smooth.worker.ts', {type: 'module'}); const smoothWorker = () => new Worker('~/worker/scalars/smooth.worker.ts', {type: 'module'});
const rangeWorker = () => new Worker('~/worker/scalars/range.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)` 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` const Error = styled.div`
${size(height, width)} ${size('100%', '100%')}
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
`; `;
enum XAxisType {
value = 'value',
log = 'log',
time = 'time'
}
enum YAxisType {
value = 'value',
log = 'log'
}
type ScalarChartProps = { type ScalarChartProps = {
runs: string[]; cid: symbol;
runs: Run[];
tag: string; tag: string;
smoothing: number; smoothing: number;
xAxis: keyof typeof xAxisMap; xAxis: keyof typeof xAxisMap;
sortingMethod: keyof typeof sortingMethodMap; sortingMethod: keyof typeof sortingMethodMap;
outlier?: boolean; outlier?: boolean;
running?: boolean; running?: boolean;
onToggleMaximized?: (maximized: boolean) => void;
}; };
const ScalarChart: FunctionComponent<ScalarChartProps> = ({ const ScalarChart: FunctionComponent<ScalarChartProps> = ({
cid,
runs, runs,
tag, tag,
smoothing, smoothing,
...@@ -70,15 +106,27 @@ const ScalarChart: FunctionComponent<ScalarChartProps> = ({ ...@@ -70,15 +106,27 @@ const ScalarChart: FunctionComponent<ScalarChartProps> = ({
}) => { }) => {
const {t, i18n} = useTranslation(['scalars', 'common']); const {t, i18n} = useTranslation(['scalars', 'common']);
const echart = useRef<LineChartRef>(null);
const {data: datasets, error, loading} = useRunningRequest<(Dataset | 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, !!running,
(...urls) => cycleFetcher(urls) (...urls) => cycleFetcher(urls)
); );
const smooth = false; const smooth = false;
const type = useMemo(() => (xAxis === 'wall' ? 'time' : 'value'), [xAxis]); const [maximized, setMaximized] = useState<boolean>(false);
const xAxisLabel = useMemo(() => (xAxis === 'step' ? '' : t(`x-axis-value.${xAxis}`)), [xAxis, t]); 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( const transformParams = useMemo(
() => ({ () => ({
...@@ -104,18 +152,18 @@ const ScalarChart: FunctionComponent<ScalarChartProps> = ({ ...@@ -104,18 +152,18 @@ const ScalarChart: FunctionComponent<ScalarChartProps> = ({
// if there is only one point, place it in the middle // if there is only one point, place it in the middle
if (smoothedDatasets.length === 1 && smoothedDatasets[0].length === 1) { 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]]); x = singlePointRange(smoothedDatasets[0][0][xAxisMap[xAxis]]);
} }
y = singlePointRange(smoothedDatasets[0][0][2]); y = singlePointRange(smoothedDatasets[0][0][2]);
} }
return {x, y}; return {x, y};
}, [smoothedDatasets, yRange, type, xAxis]); }, [smoothedDatasets, yRange, xAxisType, xAxis]);
const data = useMemo( const data = useMemo(
() => () =>
chartData({ chartData({
data: smoothedDatasets, data: smoothedDatasets.slice(0, runs.length),
runs, runs,
smooth, smooth,
xAxis xAxis
...@@ -127,32 +175,7 @@ const ScalarChart: FunctionComponent<ScalarChartProps> = ({ ...@@ -127,32 +175,7 @@ const ScalarChart: FunctionComponent<ScalarChartProps> = ({
(params: EChartOption.Tooltip.Format | EChartOption.Tooltip.Format[]) => { (params: EChartOption.Tooltip.Format | EChartOption.Tooltip.Format[]) => {
const data = Array.isArray(params) ? params[0].data : params.data; const data = Array.isArray(params) ? params[0].data : params.data;
const step = data[1]; const step = data[1];
const points = const points = nearestPoint(smoothedDatasets ?? [], runs, step);
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 sort = sortingMethodMap[sortingMethod]; const sort = sortingMethodMap[sortingMethod];
return tooltip(sort ? sort(points, data) : points, i18n); return tooltip(sort ? sort(points, data) : points, i18n);
}, },
...@@ -165,16 +188,47 @@ const ScalarChart: FunctionComponent<ScalarChartProps> = ({ ...@@ -165,16 +188,47 @@ const ScalarChart: FunctionComponent<ScalarChartProps> = ({
} }
return ( return (
<StyledLineChart <Wrapper>
title={tag} <StyledLineChart
xAxis={xAxisLabel} ref={echart}
xRange={ranges.x} title={tag}
yRange={ranges.y} xRange={ranges.x}
type={type} yRange={ranges.y}
tooltip={formatter} xType={xAxisType}
data={data} yType={yAxisType}
loading={loading} 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 Input, {InputProps, padding} from '~/components/Input';
import React, {FunctionComponent} from 'react'; 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 Icon from '~/components/Icon';
import styled from 'styled-components'; import styled from 'styled-components';
...@@ -8,27 +8,26 @@ import styled from 'styled-components'; ...@@ -8,27 +8,26 @@ import styled from 'styled-components';
const iconSize = em(16); const iconSize = em(16);
const StyledInput = styled(Input)` const StyledInput = styled(Input)`
padding-right: ${math(`${iconSize} + ${padding} * 2`)}; padding-left: ${math(`${iconSize} + ${padding} * 2`)};
width: 100%; width: 100%;
`; `;
const Control = styled.div` const Control = styled.div`
background-color: ${backgroundColor};
position: relative; position: relative;
`; `;
const SearchIcon = styled(Icon)` const SearchIcon = styled(Icon)`
font-size: ${iconSize}; font-size: ${iconSize};
display: block; display: block;
${position('absolute', padding, padding, null, null)} ${position('absolute', padding, null, null, padding)}
pointer-events: none; pointer-events: none;
color: ${textLighterColor}; color: ${textLighterColor};
`; `;
const SearchInput: FunctionComponent<InputProps & WithStyled> = ({className, ...props}) => ( const SearchInput: FunctionComponent<InputProps & WithStyled> = ({className, ...props}) => (
<Control className={className}> <Control className={className}>
<StyledInput {...props} />
<SearchIcon type="search" /> <SearchIcon type="search" />
<StyledInput {...props} />
</Control> </Control>
); );
......
...@@ -27,12 +27,10 @@ import without from 'lodash/without'; ...@@ -27,12 +27,10 @@ import without from 'lodash/without';
export const padding = em(10); export const padding = em(10);
export const height = em(36); export const height = em(36);
const minWidth = em(160);
const Wrapper = styled.div<{opened?: boolean}>` const Wrapper = styled.div<{opened?: boolean}>`
height: ${height}; height: ${height};
line-height: calc(${height} - 2px); line-height: calc(${height} - 2px);
min-width: ${minWidth};
max-width: 100%; max-width: 100%;
display: inline-block; display: inline-block;
position: relative; position: relative;
...@@ -122,29 +120,38 @@ const MultipleListItem = styled(Checkbox)<{selected?: boolean}>` ...@@ -122,29 +120,38 @@ const MultipleListItem = styled(Checkbox)<{selected?: boolean}>`
align-items: center; align-items: center;
`; `;
export type SelectValueType = string | number | symbol;
type SelectListItem<T> = { type SelectListItem<T> = {
value: T; value: T;
label: string; label: string;
}; };
type SelectProps<T> = { type OnSingleChange<T> = (value: T) => unknown;
list?: (SelectListItem<T> | string)[]; type OnMultipleChange<T> = (value: T[]) => unknown;
value?: T | T[];
onChange?: (value: T | T[]) => unknown; export type SelectProps<T> = {
multiple?: boolean; list?: (SelectListItem<T> | T)[];
placeholder?: string; 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, list: propList,
value: propValue, value: propValue,
placeholder, placeholder,
multiple, multiple,
className, className,
onChange onChange
}) => { }: SelectProps<T> & WithStyled): ReturnType<FunctionComponent> => {
const {t} = useTranslation('common'); const {t} = useTranslation('common');
const [isOpened, setIsOpened] = useState(false); const [isOpened, setIsOpened] = useState(false);
...@@ -158,53 +165,56 @@ const Select: FunctionComponent<SelectProps<SelectValueType> & WithStyled> = ({ ...@@ -158,53 +165,56 @@ const Select: FunctionComponent<SelectProps<SelectValueType> & WithStyled> = ({
setValue setValue
]); ]);
const isSelected = useMemo( const isSelected = useMemo(() => !!(multiple ? (value as T[]) && (value as T[]).length !== 0 : (value as T)), [
() => !!(multiple ? value && (value as SelectValueType[]).length !== 0 : (value as SelectValueType)), multiple,
[multiple, value] value
); ]);
const changeValue = useCallback( const changeValue = useCallback(
(mutateValue: SelectValueType, checked?: boolean) => { (mutateValue: T) => {
let newValue; setValue(mutateValue);
if (multiple) { (onChange as OnSingleChange<T>)?.(mutateValue);
newValue = value as SelectValueType[]; setIsOpenedFalse();
if (checked) { },
if (!newValue.includes(mutateValue)) { [setIsOpenedFalse, onChange]
newValue = [...newValue, mutateValue]; );
} const changeMultipleValue = useCallback(
} else { (mutateValue: T, checked: boolean) => {
if (newValue.includes(mutateValue)) { let newValue = value as T[];
newValue = without(newValue, mutateValue); if (checked) {
} if (!newValue.includes(mutateValue)) {
newValue = [...newValue, mutateValue];
} }
} else { } else {
newValue = mutateValue; if (newValue.includes(mutateValue)) {
newValue = without(newValue, mutateValue);
}
} }
setValue(newValue); setValue(newValue);
onChange?.(newValue); (onChange as OnMultipleChange<T>)?.(newValue);
if (!multiple) {
setIsOpenedFalse();
}
}, },
[multiple, value, setIsOpenedFalse, onChange] [value, onChange]
); );
const ref = useClickOutside(setIsOpenedFalse); const ref = useClickOutside(setIsOpenedFalse);
const list = useMemo( const list = useMemo<SelectListItem<T>[]>(
() => propList?.map(item => ('string' === typeof item ? {value: item, label: item} : item)) ?? [], () =>
propList?.map(item =>
['string', 'number'].includes(typeof item)
? {value: item as T, label: item + ''}
: (item as SelectListItem<T>)
) ?? [],
[propList] [propList]
); );
const isListEmpty = useMemo(() => list.length === 0, [list]); const isListEmpty = useMemo(() => list.length === 0, [list]);
const findLabelByValue = useCallback((v: SelectValueType) => list.find(item => item.value === v)?.label ?? '', [ const findLabelByValue = useCallback((v: T) => list.find(item => item.value === v)?.label ?? '', [list]);
list
]);
const label = useMemo( const label = useMemo(
() => () =>
isSelected isSelected
? multiple ? multiple
? (value as SelectValueType[]).map(findLabelByValue).join(' / ') ? (value as T[]).map(findLabelByValue).join(' / ')
: findLabelByValue(value as SelectValueType) : findLabelByValue(value as T)
: placeholder || t('select'), : placeholder || t('select'),
[multiple, value, findLabelByValue, isSelected, placeholder, t] [multiple, value, findLabelByValue, isSelected, placeholder, t]
); );
...@@ -222,11 +232,11 @@ const Select: FunctionComponent<SelectProps<SelectValueType> & WithStyled> = ({ ...@@ -222,11 +232,11 @@ const Select: FunctionComponent<SelectProps<SelectValueType> & WithStyled> = ({
if (multiple) { if (multiple) {
return ( return (
<MultipleListItem <MultipleListItem
value={(value as SelectValueType[]).includes(item.value)} value={(value as T[]).includes(item.value)}
key={index} key={index}
title={item.label} title={item.label}
size="small" size="small"
onChange={checked => changeValue(item.value, checked)} onChange={checked => changeMultipleValue(item.value, checked)}
> >
{item.label} {item.label}
</MultipleListItem> </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: { ...@@ -21,6 +21,9 @@ const useECharts = <T extends HTMLElement>(options: {
if (options.gl) { if (options.gl) {
await import('echarts-gl'); await import('echarts-gl');
} }
if (!ref.current) {
return;
}
echartInstance.current = echarts.init((ref.current as unknown) as HTMLDivElement); echartInstance.current = echarts.init((ref.current as unknown) as HTMLDivElement);
if (options.zoom) { if (options.zoom) {
setTimeout(() => { setTimeout(() => {
......
import {useEffect, useMemo} from 'react'; import {useEffect, useMemo} from 'react';
import useSWR, {ConfigInterface, keyInterface, responseInterface} from 'swr'; import useSWR, {ConfigInterface, keyInterface, responseInterface} from 'swr';
import ee from '~/utils/event';
import {fetcherFn} from 'swr/dist/types'; import {fetcherFn} from 'swr/dist/types';
type Response<D, E> = responseInterface<D, E> & { type Response<D, E> = responseInterface<D, E> & {
...@@ -61,6 +62,13 @@ function useRunningRequest<D = unknown, E = unknown>( ...@@ -61,6 +62,13 @@ function useRunningRequest<D = unknown, E = unknown>(
} }
}, [running, mutate]); }, [running, mutate]);
useEffect(() => {
ee.on('refresh-running', mutate);
return () => {
ee.off('refresh-running', mutate);
};
}, [mutate]);
return {mutate, ...others}; return {mutate, ...others};
} }
......
import {Run, Tag} from '~/types';
import {color, colorAlt} from '~/utils/chart';
import {useCallback, useEffect, useMemo, useReducer} from 'react'; import {useCallback, useEffect, useMemo, useReducer} from 'react';
import {Tag} from '~/types';
import groupBy from 'lodash/groupBy'; import groupBy from 'lodash/groupBy';
import intersection from 'lodash/intersection'; import intersectionBy from 'lodash/intersectionBy';
import uniq from 'lodash/uniq'; import uniq from 'lodash/uniq';
import {useRouter} from 'next/router'; import {useRouter} from 'next/router';
import {useRunningRequest} from '~/hooks/useRequest'; import {useRunningRequest} from '~/hooks/useRequest';
type Runs = string[];
type Tags = Record<string, string[]>; type Tags = Record<string, string[]>;
type State = { type State = {
runs: Runs; initRuns: string[];
runs: Run[];
selectedRuns: Run[];
initTags: Tags; initTags: Tags;
tags: Tag[]; tags: Tag[];
filteredTags: Tag[]; selectedTags: Tag[];
}; };
enum ActionType { enum ActionType {
initRuns,
setRuns, setRuns,
setSelectedRuns,
initTags, initTags,
setTags, setTags,
setFilteredTags setSelectedTags
} }
type ActionInitRuns = {
type: ActionType.initRuns;
payload: string[];
};
type ActionSetRuns = { type ActionSetRuns = {
type: ActionType.setRuns; type: ActionType.setRuns | ActionType.setSelectedRuns;
payload: Runs; payload: Run[];
}; };
type ActionInitTags = { type ActionInitTags = {
...@@ -35,31 +44,26 @@ type ActionInitTags = { ...@@ -35,31 +44,26 @@ type ActionInitTags = {
}; };
type ActionSetTags = { type ActionSetTags = {
type: ActionType.setTags; type: ActionType.setTags | ActionType.setSelectedTags;
payload: Tag[]; payload: Tag[];
}; };
type ActionSetFilteredTags = { type Action = ActionInitRuns | ActionSetRuns | ActionInitTags | ActionSetTags;
type: ActionType.setFilteredTags;
payload: Tag[];
};
type Action = ActionSetRuns | ActionInitTags | ActionSetTags | ActionSetFilteredTags;
type SingleTag = {label: Tag['label']; run: Tag['runs'][number]}; 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( Object.entries(
groupBy<SingleTag>( groupBy<SingleTag>(
runs runs
// get tags of selected runs // get tags of selected runs
.filter(run => runs.includes(run)) .filter(run => !!runs.find(r => r.label === run.label))
// group by runs // group by runs
.reduce<SingleTag[]>((prev, run) => { .reduce<SingleTag[]>((prev, run) => {
if (tags && tags[run]) { if (tags && tags[run.label]) {
Array.prototype.push.apply( Array.prototype.push.apply(
prev, prev,
tags[run].map(label => ({label, run})) tags[run.label].map(label => ({label, run}))
); );
} }
return prev; return prev;
...@@ -68,34 +72,66 @@ const groupTags = (runs: Runs, tags?: Tags): Tag[] => ...@@ -68,34 +72,66 @@ const groupTags = (runs: Runs, tags?: Tags): Tag[] =>
) )
).map(([label, tags]) => ({label, runs: tags.map(tag => tag.run)})); ).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 => { const reducer = (state: State, action: Action): State => {
switch (action.type) { 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: 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 { return {
...state, ...state,
runs: action.payload, runs: action.payload,
tags: runTags, selectedRuns: setRunsSelectedRuns,
filteredTags: runTags 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: case ActionType.initTags:
const newTags = groupTags(state.runs, action.payload); const initTagsTags = groupTags(state.selectedRuns, action.payload);
return { return {
...state, ...state,
initTags: action.payload, initTags: action.payload,
tags: newTags, tags: initTagsTags,
filteredTags: newTags selectedTags: initTagsTags
}; };
case ActionType.setTags: case ActionType.setTags:
return { return {
...state, ...state,
tags: action.payload, tags: action.payload,
filteredTags: action.payload selectedTags: action.payload
}; };
case ActionType.setFilteredTags: case ActionType.setSelectedTags:
return { return {
...state, ...state,
filteredTags: action.payload selectedTags: action.payload
}; };
default: default:
throw new Error(); throw new Error();
...@@ -105,48 +141,42 @@ const reducer = (state: State, action: Action): State => { ...@@ -105,48 +141,42 @@ const reducer = (state: State, action: Action): State => {
const useTagFilter = (type: string, running: boolean) => { const useTagFilter = (type: string, running: boolean) => {
const router = useRouter(); 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 {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
? router.query.runs ? uniq(Array.isArray(router.query.runs) ? router.query.runs : router.query.runs.split(','))
? intersection(
uniq(Array.isArray(router.query.runs) ? router.query.runs : router.query.runs.split(',')),
runs
)
: runs
: [], : [],
[router, runs] [router]
); );
const [state, dispatch] = useReducer( const runsFromQuery = useMemo(
reducer, () => (queryRuns.length ? state.runs.filter(run => queryRuns.includes(run.label)) : state.runs),
{ [state.runs, queryRuns]
runs: selectedRuns,
initTags: {},
tags: groupTags(selectedRuns, tags)
},
initArgs => ({...initArgs, filteredTags: initArgs.tags})
); );
const onChangeRuns = useCallback((runs: Runs) => dispatch({type: ActionType.setRuns, payload: runs}), [dispatch]); const onChangeRuns = useCallback((runs: Run[]) => dispatch({type: ActionType.setSelectedRuns, payload: runs}), []);
const onInitTags = useCallback((tags: Tags) => dispatch({type: ActionType.initTags, payload: tags}), [dispatch]); const onChangeTags = useCallback((tags: Tag[]) => dispatch({type: ActionType.setSelectedTags, payload: tags}), []);
const onFilterTags = useCallback((tags: Tag[]) => dispatch({type: ActionType.setFilteredTags, payload: tags}), [
dispatch
]);
useEffect(() => onInitTags(tags || {}), [onInitTags, tags]); useEffect(() => dispatch({type: ActionType.initRuns, payload: runs || []}), [runs]);
useEffect(() => onChangeRuns(selectedRuns), [onChangeRuns, selectedRuns]); useEffect(() => dispatch({type: ActionType.setSelectedRuns, payload: runsFromQuery}), [runsFromQuery]);
useEffect(() => dispatch({type: ActionType.initTags, payload: tags || {}}), [tags]);
return { return {
runs, ...state,
tags: state.tags,
selectedRuns: state.runs,
selectedTags: state.filteredTags,
onChangeRuns, onChangeRuns,
onFilterTags, onChangeTags,
loadingRuns, loadingRuns,
loadingTags loadingTags
}; };
......
...@@ -37,45 +37,52 @@ ...@@ -37,45 +37,52 @@
"dagre-d3": "0.6.4", "dagre-d3": "0.6.4",
"echarts": "4.7.0", "echarts": "4.7.0",
"echarts-gl": "1.1.1", "echarts-gl": "1.1.1",
"eventemitter3": "4.0.0",
"file-saver": "2.0.2",
"isomorphic-unfetch": "3.0.0", "isomorphic-unfetch": "3.0.0",
"lodash": "4.17.15", "lodash": "4.17.15",
"moment": "2.24.0", "mime-types": "2.1.27",
"next": "9.3.4", "moment": "2.25.3",
"nanoid": "3.1.5",
"next": "9.3.6",
"nprogress": "0.2.0", "nprogress": "0.2.0",
"polished": "3.5.1", "polished": "3.6.2",
"prop-types": "15.7.2", "prop-types": "15.7.2",
"query-string": "6.12.0", "query-string": "6.12.1",
"react": "16.13.1", "react": "16.13.1",
"react-dom": "16.13.1", "react-dom": "16.13.1",
"react-hooks-worker": "0.9.0", "react-hooks-worker": "0.9.0",
"react-input-range": "1.3.0", "react-input-range": "1.3.0",
"react-is": "16.13.1", "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", "save-svg-as-png": "1.4.17",
"styled-components": "5.1.0", "styled-components": "5.1.0",
"swr": "0.2.0" "swr": "0.2.0"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "7.9.0", "@babel/core": "7.9.6",
"@types/d3": "5.7.2", "@types/d3": "5.7.2",
"@types/dagre-d3": "0.4.39", "@types/dagre-d3": "0.4.39",
"@types/echarts": "4.4.5", "@types/echarts": "4.6.0",
"@types/lodash": "4.14.149", "@types/file-saver": "2.0.1",
"@types/node": "13.11.1", "@types/lodash": "4.14.150",
"@types/mime-types": "2.1.0",
"@types/node": "13.13.5",
"@types/nprogress": "0.2.0", "@types/nprogress": "0.2.0",
"@types/react": "16.9.34", "@types/react": "16.9.34",
"@types/react-dom": "16.9.6", "@types/react-dom": "16.9.7",
"@types/styled-components": "5.0.1", "@types/styled-components": "5.1.0",
"@visualdl/mock": "^2.0.0-beta.32", "@visualdl/mock": "^2.0.0-beta.32",
"babel-plugin-emotion": "10.0.33", "babel-plugin-emotion": "10.0.33",
"babel-plugin-styled-components": "1.10.7", "babel-plugin-styled-components": "1.10.7",
"babel-plugin-typescript-to-proptypes": "1.3.2", "babel-plugin-typescript-to-proptypes": "1.3.2",
"core-js": "3.6.5", "core-js": "3.6.5",
"cross-env": "7.0.2", "cross-env": "7.0.2",
"css-loader": "3.5.2", "css-loader": "3.5.3",
"ora": "4.0.3", "ora": "4.0.4",
"typescript": "3.8.3", "typescript": "3.8.3",
"worker-plugin": "4.0.2" "worker-plugin": "4.0.3"
}, },
"engines": { "engines": {
"node": ">=10", "node": ">=10",
......
...@@ -18,6 +18,10 @@ import useRequest from '~/hooks/useRequest'; ...@@ -18,6 +18,10 @@ import useRequest from '~/hooks/useRequest';
// eslint-disable-next-line @typescript-eslint/no-empty-function // eslint-disable-next-line @typescript-eslint/no-empty-function
const dumbFn = () => {}; const dumbFn = () => {};
const AsideSection = styled.section`
padding: ${rem(20)};
`;
const SubSection = styled.div` const SubSection = styled.div`
margin-bottom: ${rem(30)}; margin-bottom: ${rem(30)};
`; `;
...@@ -257,12 +261,12 @@ const Graphs: NextI18NextPage = () => { ...@@ -257,12 +261,12 @@ const Graphs: NextI18NextPage = () => {
const {currentNode, downloadImage, fitScreen, scale, setScale} = useDagreD3(graph); const {currentNode, downloadImage, fitScreen, scale, setScale} = useDagreD3(graph);
const aside = ( const aside = (
<section> <AsideSection>
<SubSection> <SubSection>
<Button icon="download" onClick={downloadImage}> <Button rounded type="primary" icon="download" onClick={downloadImage}>
{t('download-image')} {t('download-image')}
</Button> </Button>
<Button icon="revert" onClick={fitScreen}> <Button rounded type="primary" icon="revert" onClick={fitScreen}>
{t('restore-image')} {t('restore-image')}
</Button> </Button>
</SubSection> </SubSection>
...@@ -277,7 +281,7 @@ const Graphs: NextI18NextPage = () => { ...@@ -277,7 +281,7 @@ const Graphs: NextI18NextPage = () => {
<Field label={`${t('node-info')}:`} /> <Field label={`${t('node-info')}:`} />
<NodeInfo node={currentNode} /> <NodeInfo node={currentNode} />
</SubSection> </SubSection>
</section> </AsideSection>
); );
const ContentInner = useMemo(() => { const ContentInner = useMemo(() => {
......
import {Dimension, Reduction} from '~/resource/high-dimensional'; import {Dimension, Reduction} from '~/resource/high-dimensional';
import {NextI18NextPage, useTranslation} from '~/utils/i18n'; import {NextI18NextPage, useTranslation} from '~/utils/i18n';
import React, {useEffect, useState} from 'react'; 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 {em, rem} from '~/utils/style';
import AsideDivider from '~/components/AsideDivider'; import AsideDivider from '~/components/AsideDivider';
...@@ -24,6 +24,14 @@ import useSearchValue from '~/hooks/useSearchValue'; ...@@ -24,6 +24,14 @@ import useSearchValue from '~/hooks/useSearchValue';
const dimensions = ['2d', '3d']; const dimensions = ['2d', '3d'];
const reductions = ['pca', 'tsne']; 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)` const StyledIcon = styled(Icon)`
margin-right: ${em(4)}; margin-right: ${em(4)};
vertical-align: middle; vertical-align: middle;
...@@ -44,7 +52,7 @@ const HighDimensional: NextI18NextPage = () => { ...@@ -44,7 +52,7 @@ const HighDimensional: NextI18NextPage = () => {
const {query} = useRouter(); const {query} = useRouter();
const queryRun = Array.isArray(query.run) ? query.run[0] : query.run; const queryRun = Array.isArray(query.run) ? query.run[0] : query.run;
const {data: runs, error, loading} = useRunningRequest<string[]>('/runs', running); 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); const [run, setRun] = useState(selectedRun);
useEffect(() => setRun(selectedRun), [setRun, selectedRun]); useEffect(() => setRun(selectedRun), [setRun, selectedRun]);
...@@ -56,12 +64,12 @@ const HighDimensional: NextI18NextPage = () => { ...@@ -56,12 +64,12 @@ const HighDimensional: NextI18NextPage = () => {
const [labelVisibility, setLabelVisibility] = useState(true); const [labelVisibility, setLabelVisibility] = useState(true);
const aside = ( const aside = (
<section> <AsideSection>
<AsideTitle>{t('common:select-runs')}</AsideTitle> <AsideTitle>{t('common:select-runs')}</AsideTitle>
<Select <StyledSelect
list={runs} list={runs}
value={run} value={run}
onChange={(value: SelectValueType | SelectValueType[]) => setRun(value as string)} onChange={(value: NonNullable<typeof runs>[number]) => setRun(value)}
/> />
<AsideDivider /> <AsideDivider />
<Field> <Field>
...@@ -78,7 +86,7 @@ const HighDimensional: NextI18NextPage = () => { ...@@ -78,7 +86,7 @@ const HighDimensional: NextI18NextPage = () => {
{t('dimension')} {t('dimension')}
</AsideTitle> </AsideTitle>
<Field> <Field>
<RadioGroup value={dimension} onChange={value => setDimension(value as Dimension)}> <RadioGroup value={dimension} onChange={value => setDimension(value)}>
{dimensions.map(item => ( {dimensions.map(item => (
<RadioButton key={item} value={item}> <RadioButton key={item} value={item}>
{t(item)} {t(item)}
...@@ -92,7 +100,7 @@ const HighDimensional: NextI18NextPage = () => { ...@@ -92,7 +100,7 @@ const HighDimensional: NextI18NextPage = () => {
{t('reduction-method')} {t('reduction-method')}
</AsideTitle> </AsideTitle>
<Field> <Field>
<RadioGroup value={reduction} onChange={value => setReduction(value as Reduction)}> <RadioGroup value={reduction} onChange={value => setReduction(value)}>
{reductions.map(item => ( {reductions.map(item => (
<RadioButton key={item} value={item}> <RadioButton key={item} value={item}>
{t(item)} {t(item)}
...@@ -101,7 +109,7 @@ const HighDimensional: NextI18NextPage = () => { ...@@ -101,7 +109,7 @@ const HighDimensional: NextI18NextPage = () => {
</RadioGroup> </RadioGroup>
</Field> </Field>
<RunningToggle running={running} onToggle={setRunning} /> <RunningToggle running={running} onToggle={setRunning} />
</section> </AsideSection>
); );
return ( return (
......
// cSpell:words ungrouped // cSpell:words ungrouped
import ChartPage, {WithChart} from '~/components/ChartPage';
import {NextI18NextPage, useTranslation} from '~/utils/i18n'; import {NextI18NextPage, useTranslation} from '~/utils/i18n';
import React, {useCallback, useMemo, useState} from 'react'; 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 Checkbox from '~/components/Checkbox';
import Content from '~/components/Content'; import Content from '~/components/Content';
// import Field from '~/components/Field'; import Field from '~/components/Field';
// import Icon from '~/components/Icon';
import Preloader from '~/components/Preloader'; import Preloader from '~/components/Preloader';
import RunSelect from '~/components/RunSelect'; import RunAside from '~/components/RunAside';
import RunningToggle from '~/components/RunningToggle';
import SampleChart from '~/components/SamplesPage/SampleChart'; import SampleChart from '~/components/SamplesPage/SampleChart';
import TagFilter from '~/components/TagFilter'; import Slider from '~/components/Slider';
import Title from '~/components/Title'; import Title from '~/components/Title';
// import {rem} from '~/utils/style'; import {rem} from '~/utils/style';
// import styled from 'styled-components';
import useTagFilter from '~/hooks/useTagFilter'; import useTagFilter from '~/hooks/useTagFilter';
// const StyledIcon = styled(Icon)` const chartSize = {
// font-size: ${rem(16)}; height: rem(406)
// 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;
// `;
type Item = { type Item = {
run: string; run: string;
...@@ -43,65 +29,62 @@ const Samples: NextI18NextPage = () => { ...@@ -43,65 +29,62 @@ const Samples: NextI18NextPage = () => {
const [running, setRunning] = useState(true); const [running, setRunning] = useState(true);
const {runs, tags, selectedRuns, selectedTags, onChangeRuns, onFilterTags, loadingRuns, loadingTags} = useTagFilter( const {runs, tags, selectedRuns, onChangeRuns, loadingRuns, loadingTags} = useTagFilter('images', running);
'images',
running
);
const ungroupedSelectedTags = useMemo( const ungroupedSelectedTags = useMemo(
() => () =>
selectedTags.reduce<Item[]>((prev, {runs, ...item}) => { tags.reduce<Item[]>((prev, {runs, ...item}) => {
Array.prototype.push.apply( Array.prototype.push.apply(
prev, prev,
runs.map(run => ({...item, run})) runs.map(run => ({...item, run: run.label, id: `${item.label}-${run.label}`}))
); );
return prev; 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 [showActualSize, setShowActualSize] = useState(false);
const [brightness, setBrightness] = useState(1);
const [contrast, setContrast] = useState(1);
const aside = ( const aside = (
<section> <RunAside
<RunSelect runs={runs} value={selectedRuns} onChange={onChangeRuns} /> runs={runs}
<AsideDivider /> selectedRuns={selectedRuns}
{/* <Field> onChangeRuns={onChangeRuns}
<Checkbox value={showImage} onChange={setShowImage} disabled> running={running}
<StyledIcon type="image" /> onToggleRunning={setRunning}
<CheckboxTitle>{t('image')}</CheckboxTitle> >
</Checkbox> <section>
</Field> */}
{showImage && (
<Checkbox value={showActualSize} onChange={setShowActualSize}> <Checkbox value={showActualSize} onChange={setShowActualSize}>
{t('show-actual-size')} {t('show-actual-size')}
</Checkbox> </Checkbox>
)} </section>
{/* <AsideDivider /> <section>
<Field> <Field label={t('brightness')}>
<Checkbox value={showAudio} onChange={setShowAudio}> <Slider min={0} max={2} step={0.01} value={brightness} onChange={setBrightness} />
<StyledIcon type="audio" /> </Field>
<CheckboxTitle>{t('audio')}</CheckboxTitle> </section>
</Checkbox> <section>
</Field> <Field label={t('contrast')}>
<AsideDivider /> <Slider min={0} max={2} step={0.01} value={contrast} onChange={setContrast} />
<Field> </Field>
<Checkbox value={showText} onChange={setShowText}> </section>
<StyledIcon type="text" /> </RunAside>
<CheckboxTitle>{t('text')}</CheckboxTitle>
</Checkbox>
</Field> */}
<RunningToggle running={running} onToggle={setRunning} />
</section>
); );
const withChart = useCallback( const withChart = useCallback<WithChart<Item>>(
({run, label}: Item) => <SampleChart run={run} tag={label} fit={!showActualSize} running={running} />, ({run, label}) => (
[showActualSize, running] <SampleChart
run={run}
tag={label}
fit={!showActualSize}
running={running}
brightness={brightness}
contrast={contrast}
/>
),
[showActualSize, running, brightness, contrast]
); );
return ( return (
...@@ -110,8 +93,12 @@ const Samples: NextI18NextPage = () => { ...@@ -110,8 +93,12 @@ const Samples: NextI18NextPage = () => {
<Preloader url="/images/tags" /> <Preloader url="/images/tags" />
<Title>{t('common:samples')}</Title> <Title>{t('common:samples')}</Title>
<Content aside={aside} loading={loadingRuns}> <Content aside={aside} loading={loadingRuns}>
<TagFilter tags={tags} onChange={onFilterTags} /> <ChartPage
<ChartPage items={ungroupedSelectedTags} withChart={withChart} loading={loadingRuns || loadingTags} /> items={ungroupedSelectedTags}
chartSize={chartSize}
withChart={withChart}
loading={loadingRuns || loadingTags}
/>
</Content> </Content>
</> </>
); );
......
import ChartPage, {WithChart} from '~/components/ChartPage';
import {NextI18NextPage, useTranslation} from '~/utils/i18n'; import {NextI18NextPage, useTranslation} from '~/utils/i18n';
import React, {useCallback, useState} from 'react'; import React, {useCallback, useState} from 'react';
import Select, {SelectValueType} from '~/components/Select';
import {sortingMethodMap, xAxisMap} from '~/resource/scalars'; import {sortingMethodMap, xAxisMap} from '~/resource/scalars';
import AsideDivider from '~/components/AsideDivider';
import ChartPage from '~/components/ChartPage';
import Checkbox from '~/components/Checkbox'; import Checkbox from '~/components/Checkbox';
import Content from '~/components/Content'; import Content from '~/components/Content';
import Field from '~/components/Field'; import Field from '~/components/Field';
import Preloader from '~/components/Preloader'; import Preloader from '~/components/Preloader';
import RunSelect from '~/components/RunSelect'; import RadioButton from '~/components/RadioButton';
import RunningToggle from '~/components/RunningToggle'; import RadioGroup from '~/components/RadioGroup';
import RunAside from '~/components/RunAside';
import ScalarChart from '~/components/ScalarsPage/ScalarChart'; 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 {Tag} from '~/types';
import TagFilter from '~/components/TagFilter';
import Title from '~/components/Title'; 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'; import useTagFilter from '~/hooks/useTagFilter';
type XAxis = keyof typeof xAxisMap; type XAxis = keyof typeof xAxisMap;
const xAxisValues = ['step', 'relative', 'wall']; const xAxisValues = ['step', 'relative', 'wall'] as const;
type TooltipSorting = keyof typeof sortingMethodMap; 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 Scalars: NextI18NextPage = () => {
const {t} = useTranslation(['scalars', 'common']); const {t} = useTranslation(['scalars', 'common']);
const [running, setRunning] = useState(true); const [running, setRunning] = useState(true);
const {runs, tags, selectedRuns, selectedTags, onChangeRuns, onFilterTags, loadingRuns, loadingTags} = useTagFilter( const {runs, tags, selectedRuns, onChangeRuns, loadingRuns, loadingTags} = useTagFilter('scalars', running);
'scalars',
running
);
const debounceTags = useSearchValue(selectedTags);
const [smoothing, setSmoothing] = useState(0.6); const [smoothing, setSmoothing] = useState(0.6);
const [xAxis, setXAxis] = useState(xAxisValues[0] as XAxis); const [xAxis, setXAxis] = useState<XAxis>(xAxisValues[0]);
const onChangeXAxis = (value: SelectValueType | SelectValueType[]) => setXAxis(value as XAxis);
const [tooltipSorting, setTooltipSorting] = useState(toolTipSortingValues[0] as TooltipSorting); const [tooltipSorting, setTooltipSorting] = useState<TooltipSorting>(toolTipSortingValues[0]);
const onChangeTooltipSorting = (value: SelectValueType | SelectValueType[]) => const onChangeTooltipSorting = (value: TooltipSorting) => setTooltipSorting(value);
setTooltipSorting(value as TooltipSorting);
const [ignoreOutliers, setIgnoreOutliers] = useState(false); const [ignoreOutliers, setIgnoreOutliers] = useState(false);
const aside = ( const aside = (
<section> <RunAside
<RunSelect runs={runs} value={selectedRuns} onChange={onChangeRuns} /> runs={runs}
<AsideDivider /> selectedRuns={selectedRuns}
<SmoothingSlider value={smoothing} onChange={setSmoothing} /> onChangeRuns={onChangeRuns}
<Field label={t('x-axis')}> running={running}
<Select onToggleRunning={setRunning}
list={xAxisValues.map(value => ({label: t(`x-axis-value.${value}`), value}))} >
value={xAxis} <section>
onChange={onChangeXAxis}
/>
</Field>
<Field label={t('tooltip-sorting')}>
<Select
list={toolTipSortingValues.map(value => ({label: t(`tooltip-sorting-value.${value}`), value}))}
value={tooltipSorting}
onChange={onChangeTooltipSorting}
/>
</Field>
<Field>
<Checkbox value={ignoreOutliers} onChange={setIgnoreOutliers}> <Checkbox value={ignoreOutliers} onChange={setIgnoreOutliers}>
{t('ignore-outliers')} {t('ignore-outliers')}
</Checkbox> </Checkbox>
</Field> <TooltipSortingDiv>
<RunningToggle running={running} onToggle={setRunning} /> <span>{t('tooltip-sorting')}</span>
</section> <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>
</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>
</section>
</RunAside>
); );
const withChart = useCallback( const withChart = useCallback<WithChart<Tag>>(
(item: Tag) => ( ({label, runs, ...args}) => (
<ScalarChart <ScalarChart
runs={item.runs} runs={runs}
tag={item.label} tag={label}
{...args}
smoothing={smoothing} smoothing={smoothing}
xAxis={xAxis} xAxis={xAxis}
sortingMethod={tooltipSorting} sortingMethod={tooltipSorting}
...@@ -96,8 +114,7 @@ const Scalars: NextI18NextPage = () => { ...@@ -96,8 +114,7 @@ const Scalars: NextI18NextPage = () => {
<Preloader url="/scalars/tags" /> <Preloader url="/scalars/tags" />
<Title>{t('common:scalars')}</Title> <Title>{t('common:scalars')}</Title>
<Content aside={aside} loading={loadingRuns}> <Content aside={aside} loading={loadingRuns}>
<TagFilter tags={tags} onChange={onFilterTags} /> <ChartPage items={tags} withChart={withChart} loading={loadingRuns || loadingTags} />
<ChartPage items={debounceTags} withChart={withChart} loading={loadingRuns || loadingTags} />
</Content> </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 @@ ...@@ -4,14 +4,27 @@
"graphs": "Graphs", "graphs": "Graphs",
"high-dimensional": "High Dimensional", "high-dimensional": "High Dimensional",
"search": "Search", "search": "Search",
"searchTagPlaceholder": "Search tags in RegExp", "search-tags": "Search tags in RegExp",
"all": "ALL", "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", "empty": "Empty",
"select": "Please Select", "select": "Please Select",
"select-all": "Select All",
"runs": "Runs", "runs": "Runs",
"select-runs": "Select Runs", "select-runs": "Select Runs",
"search-runs": "Search runs",
"running": "Running", "running": "Running",
"stopped": "Stopped", "stopped": "Stopped",
"run": "Run",
"stop": "Stop",
"start-realtime-refresh": "Start realtime refresh",
"stop-realtime-refresh": "Stop realtime refresh",
"loading": "Loading", "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", "image": "image",
"audio": "Audio", "audio": "audio",
"text": "Text", "text": "text",
"show-actual-size": "Show Actual Image Size", "show-actual-size": "Show Actual Image Size",
"step": "Step" "step": "Step",
"download-image": "Download $t(image)",
"brightness": "Brightness",
"contrast": "Contrast"
} }
...@@ -15,5 +15,10 @@ ...@@ -15,5 +15,10 @@
"ascending": "Ascending", "ascending": "Ascending",
"nearest": "Nearest" "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 @@ ...@@ -4,14 +4,28 @@
"graphs": "网络结构", "graphs": "网络结构",
"high-dimensional": "高维数据映射", "high-dimensional": "高维数据映射",
"search": "搜索", "search": "搜索",
"searchTagPlaceholder": "搜索标签(支持正则)", "search-tags": "搜索标签(支持正则)",
"search-result": "搜索结果",
"search-empty": "没有找到您期望的内容,你可以尝试其他搜索词<1/>或者点击<3>查看全部图表</3>",
"unselected-empty": "未选中任何数据<1/>请在右侧操作栏选择要展示的数据",
"all": "全部", "all": "全部",
"empty": "空空如也", "empty": "空空如也",
"select": "请选择", "select": "请选择",
"select-all": "全选",
"runs": "数据流", "runs": "数据流",
"select-runs": "选择数据流", "select-runs": "选择数据流",
"search-runs": "搜索数据流",
"running": "运行中", "running": "运行中",
"stopped": "已停止", "stopped": "已停止",
"run": "运行",
"stop": "停止",
"start-realtime-refresh": "运行实时数据刷新",
"stop-realtime-refresh": "停止实时数据刷新",
"loading": "载入中", "loading": "载入中",
"error": "发生错误" "error": "发生错误",
"previous-page": "上一页",
"next-page": "下一页",
"total-page": "共 {{count}} 页,跳转至",
"total-page_plural": "共 {{count}} 页,跳转至",
"confirm": "确定"
} }
...@@ -3,5 +3,8 @@ ...@@ -3,5 +3,8 @@
"audio": "音频", "audio": "音频",
"text": "文本", "text": "文本",
"show-actual-size": "按真实大小展示", "show-actual-size": "按真实大小展示",
"step": "Step" "step": "Step",
"download-image": "下载$t(image)",
"brightness": "亮度",
"contrast": "对比度"
} }
...@@ -15,5 +15,10 @@ ...@@ -15,5 +15,10 @@
"ascending": "升序", "ascending": "升序",
"nearest": "最近" "nearest": "最近"
}, },
"ignore-outliers": "图表缩放时忽略极端值" "ignore-outliers": "图表缩放时忽略极端值",
"maximize": "最大化",
"minimize": "最小化",
"restore": "还原",
"axis": "坐标轴",
"download-image": "下载图片"
} }
...@@ -13,6 +13,21 @@ ...@@ -13,6 +13,21 @@
-moz-osx-font-smoothing: grayscale; -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 { .icon-chevron-down:before {
content: '\e90a'; content: '\e90a';
} }
......
import * as chart from '~/utils/chart'; 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 {formatTime, quantile} from '~/utils';
import BigNumber from 'bignumber.js'; import BigNumber from 'bignumber.js';
import {I18n} from '@visualdl/i18n'; import {I18n} from '@visualdl/i18n';
import {Run} from '~/types';
import cloneDeep from 'lodash/cloneDeep'; import cloneDeep from 'lodash/cloneDeep';
import compact from 'lodash/compact'; import compact from 'lodash/compact';
import maxBy from 'lodash/maxBy'; import maxBy from 'lodash/maxBy';
...@@ -15,6 +16,12 @@ BigNumber.config({EXPONENTIAL_AT: [-6, 7]}); ...@@ -15,6 +16,12 @@ BigNumber.config({EXPONENTIAL_AT: [-6, 7]});
export * from './types'; export * from './types';
export const xAxisMap = {
step: 1,
relative: 4,
wall: 0
};
export const sortingMethodMap = { export const sortingMethodMap = {
default: null, default: null,
descending: (points: TooltipData[]) => sortBy(points, point => point.item[3]).reverse(), descending: (points: TooltipData[]) => sortBy(points, point => point.item[3]).reverse(),
...@@ -38,7 +45,7 @@ export const transform = ({datasets, smoothing}: TransformParams) => ...@@ -38,7 +45,7 @@ export const transform = ({datasets, smoothing}: TransformParams) =>
if (i === 0) { if (i === 0) {
startValue = millisecond; startValue = millisecond;
} }
// Relative time, millisecond to hours. // relative time, millisecond to hours.
d[4] = Math.floor(millisecond - startValue) / (60 * 60 * 1000); d[4] = Math.floor(millisecond - startValue) / (60 * 60 * 1000);
if (!nextVal.isFinite()) { if (!nextVal.isFinite()) {
d[3] = nextVal.toNumber(); d[3] = nextVal.toNumber();
...@@ -67,9 +74,9 @@ export const chartData = ({data, runs, smooth, xAxis}: ChartDataParams) => ...@@ -67,9 +74,9 @@ export const chartData = ({data, runs, smooth, xAxis}: ChartDataParams) =>
// [2] orginal value // [2] orginal value
// [3] smoothed value // [3] smoothed value
// [4] relative // [4] relative
const name = runs[i]; const name = runs[i].label;
const color = chart.color[i % chart.color.length]; const color = runs[i].colors[0];
const colorAlt = chart.colorAlt[i % chart.colorAlt.length]; const colorAlt = runs[i].colors[1];
return [ return [
{ {
name, name,
...@@ -140,52 +147,87 @@ export const range = ({datasets, outlier}: RangeParams) => { ...@@ -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 // TODO: make it better, don't concat html
export const tooltip = (data: TooltipData[], i18n: I18n) => { export const tooltip = (data: TooltipData[], i18n: I18n) => {
const indexPropMap = { const indexPropMap = {
Time: 0, time: 0,
Step: 1, step: 1,
Value: 2, value: 2,
Smoothed: 3, smoothed: 3,
Relative: 4 relative: 4
}; } as const;
const widthPropMap = { const widthPropMap = {
Run: 60, run: [60, 180] as [number, number],
Time: 120, time: 150,
Step: 40, step: 40,
Value: 60, value: 60,
Smoothed: 60, smoothed: 70,
Relative: 60 relative: 60
}; } as const;
const translatePropMap = { const translatePropMap = {
Run: 'common:runs', run: 'common:runs',
Time: 'scalars:x-axis-value.wall', time: 'scalars:x-axis-value.wall',
Step: 'scalars:x-axis-value.step', step: 'scalars:x-axis-value.step',
Value: 'scalars:value', value: 'scalars:value',
Smoothed: 'scalars:smoothed', smoothed: 'scalars:smoothed',
Relative: 'scalars:x-axis-value.relative' relative: 'scalars:x-axis-value.relative'
}; } as const;
const transformedData = data.map(item => { const transformedData = data.map(item => {
const data = item.item; const data = item.item;
return { return {
Run: item.run, run: item.run,
// use precision then toString to remove trailling 0 // use precision then toString to remove trailling 0
Smoothed: new BigNumber(data[indexPropMap.Smoothed] ?? Number.NaN).precision(5).toString(), smoothed: new BigNumber(data[indexPropMap.smoothed] ?? Number.NaN).precision(5).toString(),
Value: 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], step: data[indexPropMap.step],
Time: formatTime(data[indexPropMap.Time], i18n.language), time: formatTime(data[indexPropMap.time], i18n.language),
// Relative display value should take easy-read into consideration. // Relative display value should take easy-read into consideration.
// Better to tranform data to 'day:hour', 'hour:minutes', 'minute: seconds' and second only. // 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;">'; let headerHtml = '<tr style="font-size:14px;">';
headerHtml += (Object.keys(transformedData[0]) as (keyof typeof transformedData[0])[]) headerHtml += (Object.keys(transformedData[0]) as (keyof typeof transformedData[0])[])
.map(key => { .map(key => {
return `<td style="padding: 0 4px; font-weight: bold; width: ${widthPropMap[key]}px;">${i18n.t( return `<th style="padding: 0 4px; font-weight: bold;" class="${key}">${renderContent(
translatePropMap[key] i18n.t(translatePropMap[key]),
)}</td>`; widthPropMap[key]
)}</th>`;
}) })
.join(''); .join('');
headerHtml += '</tr>'; headerHtml += '</tr>';
...@@ -194,8 +236,20 @@ export const tooltip = (data: TooltipData[], i18n: I18n) => { ...@@ -194,8 +236,20 @@ export const tooltip = (data: TooltipData[], i18n: I18n) => {
.map(item => { .map(item => {
let str = '<tr style="font-size:12px;">'; let str = '<tr style="font-size:12px;">';
str += Object.keys(item) str += Object.keys(item)
.map(val => { .map(key => {
return `<td style="padding: 0 4px; overflow: hidden;">${item[val as keyof typeof item]}</td>`; 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(''); .join('');
str += '</tr>'; str += '</tr>';
...@@ -203,6 +257,5 @@ export const tooltip = (data: TooltipData[], i18n: I18n) => { ...@@ -203,6 +257,5 @@ export const tooltip = (data: TooltipData[], i18n: I18n) => {
}) })
.join(''); .join('');
// eslint-disable-next-line return `<table style="text-align: left;table-layout: fixed;"><thead>${headerHtml}</thead><tbody>${content}</tbody><table>`;
return `<table style="text-align: left;table-layout: fixed;width: 500px;"><thead>${headerHtml}</thead><tbody>${content}</tbody><table>`;
}; };
import {Run} from '~/types';
import {xAxisMap} from './index';
export type Dataset = number[][]; export type Dataset = number[][];
export type Range = { export type Range = {
...@@ -5,14 +8,8 @@ export type Range = { ...@@ -5,14 +8,8 @@ export type Range = {
max: number; max: number;
}; };
export const xAxisMap = {
step: 1,
relative: 4,
wall: 0
};
export type TooltipData = { export type TooltipData = {
run: string; run: Run;
item: number[]; item: number[];
}; };
...@@ -23,7 +20,7 @@ export type TransformParams = { ...@@ -23,7 +20,7 @@ export type TransformParams = {
export type ChartDataParams = { export type ChartDataParams = {
data: Dataset[]; data: Dataset[];
runs: string[]; runs: Run[];
smooth: boolean; smooth: boolean;
xAxis: keyof typeof xAxisMap; 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 { export interface Tag {
runs: string[]; runs: Run[];
label: string; label: string;
} }
...@@ -50,7 +50,10 @@ export const title = { ...@@ -50,7 +50,10 @@ export const title = {
export const tooltip = { export const tooltip = {
trigger: 'axis', trigger: 'axis',
position: ['10%', '95%'], position: ['10%', '100%'],
backgroundColor: 'rgba(0, 0, 0, 0.75)',
hideDelay: 100,
enterable: false,
axisPointer: { axisPointer: {
type: 'cross', type: 'cross',
label: { label: {
...@@ -69,13 +72,13 @@ export const tooltip = { ...@@ -69,13 +72,13 @@ export const tooltip = {
export const toolbox = { export const toolbox = {
show: true, show: true,
orient: 'vertical',
showTitle: false, showTitle: false,
top: 50, itemSize: 0,
right: 8,
feature: { feature: {
saveAsImage: { saveAsImage: {
show: true show: true,
type: 'png',
pixelRatio: 2
}, },
dataZoom: { dataZoom: {
show: true show: true
...@@ -83,9 +86,6 @@ export const toolbox = { ...@@ -83,9 +86,6 @@ export const toolbox = {
restore: { restore: {
show: true show: true
} }
},
tooltip: {
show: true
} }
}; };
...@@ -103,8 +103,8 @@ export const legend = { ...@@ -103,8 +103,8 @@ export const legend = {
export const grid = { export const grid = {
left: 50, left: 50,
top: 60, top: 60,
right: 50, right: 30,
bottom: 50 bottom: 30
}; };
export const xAxis = { 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> = ...@@ -12,9 +12,15 @@ export const fetcher = async <T = any>(url: string, options?: any): Promise<T> =
return response && 'data' in response ? response.data : response; 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); 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[]> => { export const cycleFetcher = async <T = any>(urls: string[], options?: any): Promise<T[]> => {
......
...@@ -29,7 +29,7 @@ const nextI18Next = new NextI18Next({ ...@@ -29,7 +29,7 @@ const nextI18Next = new NextI18Next({
export default 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 // from ~/node_modules/next/types/index.d.ts
// https://gitlab.com/kachkaev/website-frontend/-/blob/master/src/i18n.ts#L64-68 // 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 * as polished from 'polished';
import {createGlobalStyle, keyframes} from 'styled-components'; import {createGlobalStyle, keyframes} from 'styled-components';
import {css} from 'styled-components';
import vdlIcon from '!!css-loader!~/public/style/vdl-icon.css'; import vdlIcon from '!!css-loader!~/public/style/vdl-icon.css';
export {default as styled} from 'styled-components'; export {default as styled} from 'styled-components';
...@@ -41,11 +43,14 @@ export const backgroundColor = '#FFF'; ...@@ -41,11 +43,14 @@ export const backgroundColor = '#FFF';
export const backgroundFocusedColor = '#F6F6F6'; export const backgroundFocusedColor = '#F6F6F6';
export const borderColor = '#DDD'; export const borderColor = '#DDD';
export const borderFocusedColor = darken(0.15, borderColor); export const borderFocusedColor = darken(0.15, borderColor);
export const borderActiveColor = darken(0.3, borderColor);
export const navbarBackgroundColor = '#1527C2'; export const navbarBackgroundColor = '#1527C2';
export const navbarHoverBackgroundColor = lighten(0.05, navbarBackgroundColor); export const navbarHoverBackgroundColor = lighten(0.05, navbarBackgroundColor);
export const navbarHighlightColor = '#596cd6'; export const navbarHighlightColor = '#596cd6';
export const progressBarColor = '#FFF'; export const progressBarColor = '#FFF';
export const maskColor = 'rgba(255, 255, 255, 0.8)'; export const maskColor = 'rgba(255, 255, 255, 0.8)';
export const tooltipBackgroundColor = 'rgba(0, 0, 0, 0.6)';
export const tooltipTextColor = '#FFF';
// transitions // transitions
export const duration = '75ms'; export const duration = '75ms';
...@@ -82,6 +87,21 @@ export const transitionProps = (props: string | string[], args?: string) => { ...@@ -82,6 +87,21 @@ export const transitionProps = (props: string | string[], args?: string) => {
} }
return transitions(props, args); return transitions(props, args);
}; };
export const link = css`
a {
color: ${primaryColor};
cursor: pointer;
${transitionProps('color')};
&:hover {
color: ${primaryFocusedColor};
}
&:active {
color: ${primaryActiveColor};
}
}
`;
const spinner = keyframes` const spinner = keyframes`
0% { 0% {
......
export const enabled = () => process.env.NODE_ENV !== 'development' || !!process.env.WITH_WASM;
...@@ -37,22 +37,22 @@ ...@@ -37,22 +37,22 @@
"dependencies": { "dependencies": {
"detect-node": "2.0.4", "detect-node": "2.0.4",
"hoist-non-react-statics": "3.3.2", "hoist-non-react-statics": "3.3.2",
"i18next": "19.4.1", "i18next": "19.4.4",
"i18next-browser-languagedetector": "4.0.2", "i18next-browser-languagedetector": "4.1.1",
"i18next-express-middleware": "1.9.1", "i18next-fs-backend": "1.0.2",
"i18next-node-fs-backend": "2.1.3", "i18next-http-backend": "1.0.8",
"i18next-xhr-backend": "3.2.2", "i18next-http-middleware": "1.0.4",
"path-match": "1.2.4", "path-match": "1.2.4",
"prop-types": "15.7.2", "prop-types": "15.7.2",
"react-i18next": "11.3.4", "react-i18next": "11.4.0",
"url": "0.11.0" "url": "0.11.0"
}, },
"devDependencies": { "devDependencies": {
"@types/express": "4.17.6", "@types/express": "4.17.6",
"@types/hoist-non-react-statics": "3.3.1", "@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": "16.9.34",
"@types/react-dom": "16.9.6", "@types/react-dom": "16.9.7",
"typescript": "3.8.3" "typescript": "3.8.3"
}, },
"peerDependencies": { "peerDependencies": {
......
...@@ -2,7 +2,7 @@ import {Config, InitPromise} from '../types'; ...@@ -2,7 +2,7 @@ import {Config, InitPromise} from '../types';
import I18nextBrowserLanguageDetector from 'i18next-browser-languagedetector'; import I18nextBrowserLanguageDetector from 'i18next-browser-languagedetector';
import i18n from 'i18next'; import i18n from 'i18next';
import i18nextXHRBackend from 'i18next-xhr-backend'; import i18nextHttpBackend from 'i18next-http-backend';
import isNode from 'detect-node'; import isNode from 'detect-node';
export default (config: Config) => { export default (config: Config) => {
...@@ -10,8 +10,8 @@ export default (config: Config) => { ...@@ -10,8 +10,8 @@ export default (config: Config) => {
if (!i18n.isInitialized) { if (!i18n.isInitialized) {
if (isNode) { if (isNode) {
const i18nextNodeBackend = eval("require('i18next-node-fs-backend')"); const i18nextNodeBackend = eval('require("i18next-fs-backend")');
const i18nextMiddleware = eval("require('i18next-express-middleware')"); const i18nextMiddleware = eval('require("i18next-http-middleware")');
i18n.use(i18nextNodeBackend); i18n.use(i18nextNodeBackend);
if (config.serverLanguageDetection) { if (config.serverLanguageDetection) {
const serverDetectors = new i18nextMiddleware.LanguageDetector(); const serverDetectors = new i18nextMiddleware.LanguageDetector();
...@@ -19,7 +19,7 @@ export default (config: Config) => { ...@@ -19,7 +19,7 @@ export default (config: Config) => {
i18n.use(serverDetectors); i18n.use(serverDetectors);
} }
} else { } else {
i18n.use(i18nextXHRBackend); i18n.use(i18nextHttpBackend);
if (config.browserLanguageDetection) { if (config.browserLanguageDetection) {
const browserDetectors = new I18nextBrowserLanguageDetector(); const browserDetectors = new I18nextBrowserLanguageDetector();
config.customDetectors?.forEach(detector => browserDetectors.addDetector(detector)); config.customDetectors?.forEach(detector => browserDetectors.addDetector(detector));
......
...@@ -101,7 +101,7 @@ export const appWithTranslation = function (this: NextI18Next, WrappedComponent: ...@@ -101,7 +101,7 @@ export const appWithTranslation = function (this: NextI18Next, WrappedComponent:
if (req && !req.i18n) { if (req && !req.i18n) {
const {router} = ctx; const {router} = ctx;
const result = router.asPath.match(/^\/(.*?)\//); 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}); req.i18n = i18n.cloneInstance({initImmediate: false, lng});
const res = ctx.ctx.res as (NextPageContext['res'] & I18nRes) | undefined; const res = ctx.ctx.res as (NextPageContext['res'] & I18nRes) | undefined;
const setContextLocale = (lng?: string) => { const setContextLocale = (lng?: string) => {
......
...@@ -10,7 +10,7 @@ import { ...@@ -10,7 +10,7 @@ import {
} from '../utils'; } from '../utils';
import NextI18Next from '../index'; import NextI18Next from '../index';
import i18nextMiddleware from 'i18next-express-middleware'; import i18nextMiddleware from 'i18next-http-middleware';
import pathMatch from 'path-match'; import pathMatch from 'path-match';
const route = pathMatch(); const route = pathMatch();
......
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
declare module 'detect-node'; declare module 'detect-node';
declare module 'path-match'; declare module 'path-match';
declare module 'i18next-http-middleware';
import * as React from 'react'; import * as React from 'react';
...@@ -73,6 +74,7 @@ declare global { ...@@ -73,6 +74,7 @@ declare global {
// eslint-disable-next-line @typescript-eslint/no-namespace // eslint-disable-next-line @typescript-eslint/no-namespace
namespace Express { namespace Express {
interface Request { interface Request {
i18n: I18n;
lng?: string; lng?: string;
} }
} }
......
...@@ -14,7 +14,8 @@ export default async (req: Request, res: Response) => { ...@@ -14,7 +14,8 @@ export default async (req: Request, res: Response) => {
const index = (+req.query.index ?? 0) % images.length; const index = (+req.query.index ?? 0) % images.length;
const result = await fetch(images[index]); const result = await fetch(images[index]);
if (result.headers.has('Content-Type')) { 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(); return result.arrayBuffer();
}; };
...@@ -3,23 +3,23 @@ export default { ...@@ -3,23 +3,23 @@ export default {
'input_reshape/input/image/7', 'input_reshape/input/image/7',
'input_reshape/input/image/4', 'input_reshape/input/image/4',
'input_reshape/input/image/5', 'input_reshape/input/image/5',
'input_reshape/input/image/2', 'hahaha/input/image/2',
'input_reshape/input/image/3', 'hahaha/input/image/3',
'input_reshape/input/image/0', 'hahaha/input/image/0',
'input_reshape/input/image/1', 'ohehe/input/image/1',
'input_reshape/input/image/8', '😼/input/image/8',
'input_reshape/input/image/9' '😼/input/image/9'
], ],
train: [ train: [
'input_reshape/input/image/6', 'input_reshape/input/image/6',
'input_reshape/input/image/7', 'input_reshape/input/image/7',
'input_reshape/input/image/4', 'input_reshape/input/image/4',
'input_reshape/input/image/5', 'input_reshape/input/image/5',
'input_reshape/input/image/2', 'hahaha/input/image/2',
'input_reshape/input/image/3', 'hahaha/input/image/3',
'input_reshape/input/image/0', 'oheihei/input/image/0',
'input_reshape/input/image/1', 'oheihei/input/image/1',
'input_reshape/input/image/8', '😼/input/image/8',
'input_reshape/input/image/9' '😼/input/image/9'
] ]
}; };
export default { export default {
test: ['layer2/biases/summaries/mean'], test: ['layer2/biases/summaries/mean', 'test/1234', 'another'],
train: ['layer2/biases/summaries/mean', 'layer2/biases/summaries/accuracy', 'layer2/biases/summaries/cost'] train: [
'layer2/biases/summaries/mean',
'layer2/biases/summaries/accuracy',
'layer2/biases/summaries/cost',
'test/431',
'others'
]
}; };
...@@ -38,7 +38,7 @@ ...@@ -38,7 +38,7 @@
"devDependencies": { "devDependencies": {
"@types/express": "4.17.6", "@types/express": "4.17.6",
"@types/faker": "4.1.11", "@types/faker": "4.1.11",
"@types/node": "13.11.1", "@types/node": "13.13.5",
"typescript": "3.8.3" "typescript": "3.8.3"
}, },
"peerDependencies": { "peerDependencies": {
......
...@@ -40,23 +40,23 @@ ...@@ -40,23 +40,23 @@
"@visualdl/i18n": "^2.0.0-beta.32", "@visualdl/i18n": "^2.0.0-beta.32",
"express": "4.17.1", "express": "4.17.1",
"http-proxy-middleware": "1.0.3", "http-proxy-middleware": "1.0.3",
"next": "9.3.4", "next": "9.3.6",
"pm2": "4.2.3" "pm2": "4.4.0"
}, },
"devDependencies": { "devDependencies": {
"@types/express": "4.17.6", "@types/express": "4.17.6",
"@types/node": "13.11.1", "@types/node": "13.13.5",
"@types/shelljs": "0.8.7", "@types/shelljs": "0.8.7",
"@types/webpack": "4.41.10", "@types/webpack": "4.41.12",
"@types/webpack-dev-middleware": "3.7.0", "@types/webpack-dev-middleware": "3.7.0",
"@visualdl/mock": "^2.0.0-beta.32", "@visualdl/mock": "^2.0.0-beta.32",
"cross-env": "7.0.2", "cross-env": "7.0.2",
"nodemon": "2.0.3", "nodemon": "2.0.3",
"shelljs": "0.8.3", "shelljs": "0.8.4",
"ts-loader": "6.2.2", "ts-loader": "7.0.3",
"ts-node": "8.8.2", "ts-node": "8.10.1",
"typescript": "3.8.3", "typescript": "3.8.3",
"webpack": "4.42.1", "webpack": "4.43.0",
"webpack-cli": "3.3.11", "webpack-cli": "3.3.11",
"webpack-dev-middleware": "3.7.2" "webpack-dev-middleware": "3.7.2"
}, },
......
...@@ -31,12 +31,12 @@ ...@@ -31,12 +31,12 @@
"test": "echo \"Error: no test specified\" && exit 0" "test": "echo \"Error: no test specified\" && exit 0"
}, },
"devDependencies": { "devDependencies": {
"@types/node": "13.11.1", "@types/node": "13.13.5",
"@types/rimraf": "3.0.0", "@types/rimraf": "3.0.0",
"@visualdl/core": "^2.0.0-beta.32", "@visualdl/core": "^2.0.0-beta.32",
"cross-env": "7.0.2", "cross-env": "7.0.2",
"rimraf": "3.0.2", "rimraf": "3.0.2",
"ts-node": "8.8.2", "ts-node": "8.10.1",
"typescript": "3.8.3" "typescript": "3.8.3"
}, },
"engines": { "engines": {
......
此差异已折叠。
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册