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

VisualDL 2.0 development in progress (#571)

* refactor: initialize VisualDL 2.0

* refactor: fix dev server problem

* refactor: add i18n

* refactor: fix i18n

* infra i18n支持

* styled-components for debug

* infra. essential hack for styled comopnent

* use yaml for translation

* fix: navbar url problem

* feat: add nuxt module to build locales

* fix: index route redirect error

* feat: add page title

* refactor: move i18n to module

* feat: add html lang attribute

* R.I.P

* Hello React

* refactor: initialize VisualDL 2.0

* fix: layout rerender

* add favicon

* add page title

* add meta tags

* feat: finish tag filter

* refactor: hook tastes good

* add single select

* finish components

* add api server

* scalars segregate metrics

* json-server sucks

* echarts

* add eslint

* bug fix

* finish scalars page

* change layout, fix aside

* add commit hook

* use tag filter hook

* add chart loading

* encapsulate run select

* samples page under construction

* finish images

* feat: graph page, still need some polishment

* finish high-dimensional

* fix mock data problem

* update readme

* fix build

* fix: use Buffer.from instead of constractor

* update Readme
Co-authored-by: NNiandalu <Niandalu@users.noreply.github.com>
上级 52e8a2c4
language: cpp
matrix:
include:
- language: cpp
compiler: clang
cache:
- pip
- ccache
sudo: required
dist: trusty
compiler: clang
os:
- linux
# much bug with osx environment
# TODO(ChunweiYan) support osx in the future
#- osx
cache:
- pip
- ccache
- yarn
- npm
sudo: required
dist: trusty
env:
- MATRIX_EVAL="CC=clang-5.0 && CXX=clang++-5.0 && JOB=check_style"
- MATRIX_EVAL="CC=clang-5.0 && CXX=clang++-5.0 && JOB=test"
- MATRIX_EVAL="CC=clang-5.0 && CXX=clang++-5.0 && JOB=test_python3"
- MATRIX_EVAL="CC=clang-5.0 && CXX=clang++-5.0 && JOB=build_doc"
os:
- linux
# much bug with osx environment
# TODO(ChunweiYan) support osx in the future
#- osx
addons:
apt:
sources:
- llvm-toolchain-trusty-5.0
packages:
- clang-5.0
- gcc-4.8
- g++-4.8
- git
- python
- python-pip
- python2.7-dev
- python-wheel
- python3-pip
- python3-dev
- python3-wheel
- clang-format-3.8
- ccache
ssh_known_hosts: 13.229.163.131
env:
- MATRIX_EVAL="CC=clang-5.0 && CXX=clang++-5.0 && JOB=check_style"
- MATRIX_EVAL="CC=clang-5.0 && CXX=clang++-5.0 && JOB=test"
- MATRIX_EVAL="CC=clang-5.0 && CXX=clang++-5.0 && JOB=test_python3"
- MATRIX_EVAL="CC=clang-5.0 && CXX=clang++-5.0 && JOB=build_doc"
before_install:
- eval "${MATRIX_EVAL}"
- if [[ "$JOB" == "check_style" ]]; then sudo ln -s /usr/bin/clang-format-3.8 /usr/bin/clang-format; sudo pip install pre-commit flake8; fi
- if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then brew update; fi
- if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then brew upgrade python; fi
- if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then brew install brew-pip; fi
addons:
apt:
sources:
- llvm-toolchain-trusty-5.0
packages:
- clang-5.0
- gcc-4.8
- g++-4.8
- git
- python
- python-pip
- python2.7-dev
- python-wheel
- python3-pip
- python3-dev
- python3-wheel
- clang-format-3.8
- ccache
- npm
- nodejs
ssh_known_hosts: 13.229.163.131
before_install:
- eval "${MATRIX_EVAL}"
- if [[ "$JOB" == "check_style" ]]; then sudo ln -s /usr/bin/clang-format-3.8 /usr/bin/clang-format; sudo pip install pre-commit flake8; fi
- if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then brew update; fi
- if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then brew upgrade python; fi
- if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then brew install brew-pip; fi
script:
- if [[ "$JOB" == "check_style" ]]; then ./scripts/check_style.sh; fi
- if [[ "$JOB" == "test" ]]; then ./scripts/tests.sh all; fi
- if [[ "$JOB" == "test_python3" ]]; then WITH_PYTHON3=ON ./scripts/tests.sh all; fi
- if [[ "$JOB" == "build_doc" ]]; then ./scripts/deploy_docs_on_travis.sh; fi;
script:
- if [[ "$JOB" == "check_style" ]]; then ./scripts/check_style.sh; fi
- if [[ "$JOB" == "test" ]]; then ./scripts/tests.sh all; fi
- if [[ "$JOB" == "test_python3" ]]; then WITH_PYTHON3=ON ./scripts/tests.sh all; fi
- if [[ "$JOB" == "build_doc" ]]; then ./scripts/deploy_docs_on_travis.sh; fi;
- language: node_js
dist: trusty
node_js:
- 12
sudo: false
cache:
- npm
- yarn
env:
- NODE_ENV=production
before_script:
- cd frontend
script:
- if [[ "$JOB" == "check_style" ]]; then yarn lint; fi
- if [[ "$JOB" == "test" ]]; then yarn test; fi
notifications:
email:
......
{
"presets": [
["es2015", {"modules": false}],
"stage-0"
],
"plugins": ["transform-class-properties"]
}
# editorconfig.org
root = true
[*]
indent_style = space
indent_size = 4
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
[*.md]
trim_trailing_whitespace = false
module.exports = {
extends: [
'google',
'plugin:vue/base',
'plugin:vue/essential',
'plugin:vue/strongly-recommended',
],
parserOptions: {
"sourceType": "module",
},
rules: {
// override/add rules settings here, such as:
'vue/no-unused-vars': 'warn',
'max-len': ["warn", 120],
"vue/prop-name-casing": ["error"],
'vue/script-indent': 'error',
// The following rules should apply eventually. Turn them off for now
// so we can have pre-commit running
'no-invalid-this': 'off',
'require-jsdoc': 'off',
}
}
env: {
browser: true,
es6: true,
node: true
},
extends: [
'plugin:@typescript-eslint/recommended',
'plugin:react/recommended',
'prettier/@typescript-eslint',
'plugin:prettier/recommended'
],
parser: '@typescript-eslint/parser',
parserOptions: {
ecmaFeatures: {
jsx: true
},
ecmaVersion: 2018,
sourceType: 'module'
},
plugins: ['react', 'react-hooks', '@typescript-eslint'],
settings: {
react: {
version: 'detect'
}
},
rules: {
'@typescript-eslint/explicit-function-return-type': 'off',
'react/prop-types': 'off',
'react/react-in-jsx-scope': 'off',
'react-hooks/rules-of-hooks': 'error',
'react-hooks/exhaustive-deps': 'warn'
}
};
node_modules/**/*
dep/**/*
test/**/*
mock/**/*
example/**/*
output/**/*
{
"files": [
"./src/**/*.san",
"./src/**/*.js",
"./template/**/*.html"
],
"eslint": {
"rules": {
"fecs-esnext-ext": [
"2",
[
"js",
"san"
]
],
"fecs-valid-jsdoc": [
"0"
]
}
},
"csshint": {},
"htmlcs": {},
"jformatter": {},
"esformatter": {},
"csscomb": {}
}
# Always validate the PR title AND all the commits
titleAndCommits: true
# Allows use of Merge commits (eg on github: "Merge branch 'master' into feature/ride-unicorns")
# this is only relevant when using commitsOnly: true (or titleAndCommits: true)
allowMergeCommits: true
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
/dist
# misc
.DS_Store
.env*
.vscode
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# FrontEnd of VisualDL
A platform to visualize the deep learning process and result.
[![code style: prettier](https://img.shields.io/badge/code_style-prettier-ff69b4.svg?style=flat-square)](https://github.com/prettier/prettier) [![License](https://img.shields.io/github/license/PaddlePaddle/VisualDL?style=flat-square)](https://github.com/PaddlePaddle/VisualDL/blob/develop/LICENSE) [![GitHub top language](https://img.shields.io/github/languages/top/PaddlePaddle/VisualDL?style=flat-square)](https://github.com/PaddlePaddle/VisualDL) [![GitHub code size in bytes](https://img.shields.io/github/languages/code-size/PaddlePaddle/VisualDL?style=flat-square)](https://github.com/PaddlePaddle/VisualDL) [![GitHub issues](https://img.shields.io/github/issues/PaddlePaddle/VisualDL?style=flat-square)](https://github.com/PaddlePaddle/VisualDL/issues) [![GitHub All Releases](https://img.shields.io/github/downloads/PaddlePaddle/VisualDL/total?style=flat-square)](https://github.com/PaddlePaddle/VisualDL/releases) [![GitHub stars](https://img.shields.io/github/stars/PaddlePaddle/VisualDL?style=social)](https://github.com/PaddlePaddle/VisualDL/stargazers)
**🚧UNDER CONSTRUCTION🚧**
**🚧SOME FEATURE MAY NOT WORK PROPERLY🚧**
**🚧PULL REQUESTS WELCOMED🚧**
## Development
> nodejs ≥ 10 and npm ≥ 6 is required.
First, install all dependencies:
```bash
npm install
# or
yarn
```
Then you can start the development server:
```bash
yarn dev
```
Now open [http://localhost:8999](http://localhost:8999) with your browser.
You can change the port with `PORT` environment variable.
```bash
PORT=3000 yarn dev
```
## Learn More
This project is based on following projects:
- [Next.js](https://nextjs.org/)
- [React](https://reactjs.org/)
- [ECharts](https://echarts.apache.org/)
## License
Apache-2.0
module.exports = {
presets: ['next/babel'],
plugins: [
[
'styled-components',
{
ssr: true,
displayName: true,
preprocess: false
}
],
...(process.env.NODE_ENV !== 'production' ? ['babel-plugin-typescript-to-proptypes'] : [])
]
};
import React, {FunctionComponent} from 'react';
import styled from 'styled-components';
import {rem} from '~/utils/style';
const Divider = styled.hr<{height?: string | number}>`
background-color: transparent;
margin: 0;
border: none;
height: ${({height}) => (height ? ('number' === height ? rem(height) : height) : rem(30))};
`;
type AsideDividerProps = {
height?: string | number;
};
const AsideDivider: FunctionComponent<AsideDividerProps> = ({height}) => <Divider height={height} />;
export default AsideDivider;
import React, {FunctionComponent} from 'react';
import styled from 'styled-components';
import {
WithStyled,
em,
half,
textInvertColor,
primaryColor,
primaryFocusedColor,
primaryActiveColor,
duration,
easing,
ellipsis,
transitions
} from '~/utils/style';
import RawIcon from '~/components/Icon';
const height = em(36);
const Wrapper = styled.a`
cursor: pointer;
height: ${height};
line-height: ${height};
border-radius: ${half(height)};
background-color: ${primaryColor};
color: ${textInvertColor};
display: block;
text-align: center;
${transitions('background-color', `${duration} ${easing}`)}
${ellipsis()}
&:hover,
&:focus {
background-color: ${primaryFocusedColor};
}
&:active {
background-color: ${primaryActiveColor};
}
`;
const Icon = styled(RawIcon)`
margin-right: 4px;
`;
type ButtonProps = {
icon?: string;
onClick?: () => unknown;
};
const Button: FunctionComponent<ButtonProps & WithStyled> = ({icon, children, className, onClick}) => (
<Wrapper className={className} onClick={onClick}>
{icon && <Icon type={icon}></Icon>}
{children}
</Wrapper>
);
export default Button;
import React, {FunctionComponent} from 'react';
import styled from 'styled-components';
import {
WithStyled,
primaryColor,
backgroundColor,
borderColor,
borderRadius,
duration,
easing,
transitions,
math
} from '~/utils/style';
const Div = styled.div`
background-color: ${backgroundColor};
border: 1px solid ${borderColor};
border-radius: ${math(`${borderRadius} * 2`)};
${transitions(['border-color', 'box-shadow'], `${duration} ${easing}`)}
&:hover {
border-color: ${primaryColor};
box-shadow: 0 5px 6px 0 rgba(0, 0, 0, 0.05);
}
`;
const Chart: FunctionComponent<WithStyled> = ({className, children}) => {
return <Div className={className}>{children}</Div>;
};
export default Chart;
import React, {FunctionComponent, useState} from 'react';
import styled from 'styled-components';
import {WithStyled, rem} from '~/utils/style';
import Chart from '~/components/Chart';
import Pagination from '~/components/Pagination';
const Wrapper = styled.div`
display: flex;
flex-wrap: wrap;
justify-content: flex-start;
align-items: stretch;
align-content: flex-start;
> * {
margin: 0 ${rem(20)} ${rem(20)} 0;
flex-shrink: 0;
flex-grow: 0;
}
`;
// TODO: add types
// eslint-disable-next-line
type ChartPageProps<T = any> = {
items?: T[];
withChart?: (item: T) => React.ReactNode;
};
const ChartPage: FunctionComponent<ChartPageProps & WithStyled> = ({items, withChart, className}) => {
const pageSize = 12;
const total = Math.ceil((items?.length ?? 0) / pageSize);
const [page, setPage] = useState(1);
const pageItems = items?.slice((page - 1) * pageSize, page * pageSize) ?? [];
return (
<div className={className}>
<Wrapper>
{pageItems.map((item, index) => (
<Chart key={index}>{withChart?.(item)}</Chart>
))}
</Wrapper>
<Pagination page={page} total={total} onChange={setPage} />
</div>
);
};
export default ChartPage;
import React, {FunctionComponent, useState} from 'react';
import styled from 'styled-components';
import {
WithStyled,
em,
textLighterColor,
textInvertColor,
backgroundColor,
primaryColor,
duration,
size,
easing,
ellipsis,
transitions,
math,
darken,
lighten
} from '~/utils/style';
const height = em(20);
const checkSize = em(16);
const checkMark =
// eslint-disable-next-line
'data:image/svg+xml;base64,PHN2ZyBoZWlnaHQ9IjgiIHZpZXdCb3g9IjAgMCAxMSA4IiB3aWR0aD0iMTEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHBhdGggZD0ibTkuNDc5NDI3MDggMTAuMTg3NWgtNS4yNXYtMS4zMTI1aDMuOTM3bC4wMDA1LTcuODc1aDEuMzEyNXoiIGZpbGw9IiNmNWY1ZjUiIGZpbGwtcnVsZT0iZXZlbm9kZCIgdHJhbnNmb3JtPSJtYXRyaXgoLjcwNzEwNjc4IC43MDcxMDY3OCAtLjcwNzEwNjc4IC43MDcxMDY3OCA0Ljk2Mjk5NCAtNi4yMDg0NCkiLz48L3N2Zz4=';
const Wrapper = styled.label<{disabled?: boolean}>`
position: relative;
display: inline-flex;
align-items: flex-start;
cursor: ${props => (props.disabled ? 'not-allowed' : 'pointer')};
`;
const Input = styled.input.attrs<{disabled?: boolean}>(props => ({
type: 'checkbox',
disabled: !!props.disabled
}))`
${size(0, 0)}
position: absolute;
left: 0;
top: 0;
opacity: 0;
pointer-events: none;
`;
const Inner = styled.div<{checked?: boolean; size?: string; disabled?: boolean}>`
color: ${props => (props.checked ? textInvertColor : 'transparent')};
flex-shrink: 0;
${props => size(math(`${checkSize} * ${props.size === 'small' ? 0.875 : 1}`))}
margin: ${math(`(${height} - ${checkSize}) / 2`)} 0;
margin-right: ${em(4)};
border: 1px solid ${props => (props.disabled || !props.checked ? textLighterColor : primaryColor)};
background-color: ${props =>
props.disabled
? props.checked
? textLighterColor
: lighten(0.333, textLighterColor)
: props.checked
? primaryColor
: backgroundColor};
background-image: ${props => (props.checked ? `url("${checkMark}")` : 'none')};
background-repeat: no-repeat;
background-position: center center;
background-size: ${em(10)} ${em(8)};
position: relative;
${transitions(['border-color', 'background-color', 'color'], `${duration} ${easing}`)}
${Wrapper}:hover > & {
border-color: ${props =>
props.disabled ? textLighterColor : props.checked ? primaryColor : darken(0.1, textLighterColor)};
}
`;
const Content = styled.div<{disabled?: boolean}>`
line-height: ${height};
flex-grow: 1;
${props => (props.disabled ? `color: ${textLighterColor};` : '')}
${ellipsis()}
`;
type CheckboxProps = {
value?: boolean;
onChange?: (value: boolean) => unknown;
size?: 'small';
disabled?: boolean;
};
const Checkbox: FunctionComponent<CheckboxProps & WithStyled> = ({
value,
children,
size,
disabled,
className,
onChange
}) => {
const [checked, setChecked] = useState(!!value);
const onChangeInput = (e: React.ChangeEvent<HTMLInputElement>) => {
if (disabled) {
return;
}
setChecked(e.target.checked);
onChange?.(e.target.checked);
};
return (
<Wrapper disabled={disabled} className={className}>
<Input onChange={onChangeInput} checked={checked} disabled={disabled} />
<Inner checked={checked} size={size} disabled={disabled} />
<Content disabled={disabled}>{children}</Content>
</Wrapper>
);
};
export default Checkbox;
import React, {FunctionComponent} from 'react';
import styled from 'styled-components';
import {rem, math, headerHeight, asideWidth, backgroundColor} from '~/utils/style';
const margin = rem(20);
const padding = rem(20);
const Section = styled.section`
/* trigger BFC */
overflow: hidden;
`;
const Article = styled.article<{aside?: boolean}>`
margin: ${margin};
margin-right: ${props => (props.aside ? math(`${margin} + ${asideWidth}`) : margin)};
padding: ${padding};
background-color: ${backgroundColor};
min-height: calc(100vh - ${math(`${margin} * 2 + ${headerHeight}`)});
`;
const Aside = styled.aside`
width: ${asideWidth};
padding: ${padding};
background-color: ${backgroundColor};
height: calc(100vh - ${headerHeight});
position: fixed;
top: ${headerHeight};
right: 0;
`;
type ContentProps = {
aside?: React.ReactNode;
};
const Content: FunctionComponent<ContentProps> = ({children, aside}) => (
<Section>
<Article aside={!!aside}>{children}</Article>
{aside && <Aside>{aside}</Aside>}
</Section>
);
export default Content;
import React, {FunctionComponent} from 'react';
import styled from 'styled-components';
import {WithStyled, rem} from '~/utils/style';
const Wrapper = styled.div`
& + & {
margin-top: ${rem(20)};
}
`;
const Label = styled.div`
margin-bottom: ${rem(10)};
`;
type FieldProps = {
label?: string;
};
const Field: FunctionComponent<FieldProps & WithStyled> = ({label, children, className}) => (
<Wrapper className={className}>
{label && <Label>{label}</Label>}
{children}
</Wrapper>
);
export default Field;
import React, {FunctionComponent} from 'react';
import {useTranslation, NextI18NextPage} from '~/utils/i18n';
import {NodeType, TypedNode} from '~/resource/graph';
const typeName: {[k in NodeType]: string} = {
[NodeType.Input]: 'input',
[NodeType.Output]: 'output',
[NodeType.Op]: 'operator'
};
export interface NodeInfoProps {
node?: TypedNode | {type: 'unknown'; guessType: NodeType; msg: string};
}
const NodeInfo: FunctionComponent<NodeInfoProps> = props => {
const {t} = useTranslation(['graphs']);
if (!props.node) {
return <p>{t('click-node')}</p>;
}
const node = props.node;
switch (node.type) {
case NodeType.Input:
case NodeType.Output:
return (
<ul>
<li>
{t('node-type')}: {typeName[node.type]}
</li>
<li>
{t('node-name')}: {node.name}
</li>
<li>
{t('node-data-shape')}: {node.shape}
</li>
<li>
{t('node-data-type')}: {node.data_type}
</li>
</ul>
);
case NodeType.Op:
return (
<ul>
<li>
{t('node-type')}: {typeName[node.type]}
</li>
<li>
{t('input')}: {node.input}
</li>
<li>
{t('op-type')}: {node.opType}
</li>
<li>
{t('output')}: {node.output}
</li>
</ul>
);
case 'unknown':
return (
<ul>
<li>
{t('node-type')}: {typeName[node.guessType]}
</li>
</ul>
);
default:
return <></>;
}
};
export default NodeInfo;
import React, {FunctionComponent} from 'react';
import {WithStyled} from '~/utils/style';
type IconProps = {
type: string;
};
const Icon: FunctionComponent<IconProps & WithStyled> = ({type, className}) => {
return <i className={`vdl-icon icon-${type} ${className}`} />;
};
export default Icon;
import React, {FunctionComponent, useEffect, useState, useRef} from 'react';
import {useTranslation} from '~/utils/i18n';
import fetch from 'isomorphic-unfetch';
type ImageProps = {
src?: string;
};
const Image: FunctionComponent<ImageProps> = ({src}) => {
const {t} = useTranslation('common');
const [url, setUrl] = useState('');
const [loading, setLoading] = useState(false);
const controller = useRef(null as AbortController | null);
useEffect(() => {
if (process.browser) {
let objectUrl: string | null = null;
(async () => {
setLoading(true);
controller.current?.abort();
controller.current = new AbortController();
try {
const result = await fetch(src ?? '', {signal: controller.current.signal});
const blob = await result.blob();
objectUrl = URL.createObjectURL(blob);
setUrl(objectUrl);
} catch {
// ignore abort error
} finally {
setLoading(false);
}
})();
return () => {
objectUrl && URL.revokeObjectURL(objectUrl);
};
}
}, [src]);
return loading ? <span>{t('loading')}</span> : <img src={url} />;
};
export default Image;
import React, {FunctionComponent} from 'react';
import styled from 'styled-components';
import {
WithStyled,
em,
textLighterColor,
borderColor,
borderFocusedColor,
borderRadius,
duration,
easing,
math
} from '~/utils/style';
export const padding = em(10);
export const height = em(36);
const StyledInput = styled.input<{rounded?: boolean}>`
padding: ${padding};
height: ${height};
line-height: ${height};
display: inline-block;
border: 1px solid ${borderColor};
border-radius: ${props => (props.rounded ? math(`${height} / 2`) : borderRadius)};
transition: border-color ${duration} ${easing};
outline: none;
&:hover,
&:focus {
border-color: ${borderFocusedColor};
}
&::placeholder {
color: ${textLighterColor};
}
`;
export type InputProps = {
rounded?: boolean;
placeholder?: string;
value?: string;
onChange?: (value: string) => unknown;
};
const Input: FunctionComponent<InputProps & WithStyled> = ({rounded, placeholder, value, onChange, className}) => (
<StyledInput
rounded={rounded}
placeholder={placeholder}
value={value}
type="text"
className={className}
onChange={e => onChange?.(e.target.value)}
></StyledInput>
);
export default Input;
import React, {FunctionComponent} from 'react';
import styled from 'styled-components';
import {headerHeight} from '~/utils/style';
import Navbar from '~/components/Navbar';
const Main = styled.main`
padding-top: ${headerHeight};
`;
const Header = styled.header`
position: fixed;
z-index: 10000;
width: 100%;
height: ${headerHeight};
top: 0;
left: 0;
right: 0;
`;
const Layout: FunctionComponent = ({children}) => (
<Main>
<Header>
<Navbar />
</Header>
{children}
</Main>
);
export default Layout;
import React, {FunctionComponent, useEffect, useCallback} from 'react';
import {EChartOption} from 'echarts';
import {WithStyled} from '~/utils/style';
import useECharts from '~/hooks/useECharts';
import * as chart from '~/utils/chart';
type LineChartProps = {
title?: string;
legend?: string[];
data?: Partial<NonNullable<EChartOption<EChartOption.SeriesLine>['series']>>;
xAxis?: string;
type?: EChartOption.BasicComponents.CartesianAxis.Type;
yRange?: {
min: number;
max: number;
};
tooltip?: string | EChartOption.Tooltip.Formatter;
loading?: boolean;
};
const LineChart: FunctionComponent<LineChartProps & WithStyled> = ({
title,
legend,
data,
xAxis,
type,
yRange,
tooltip,
loading,
className
}) => {
const [ref, echart] = useECharts<HTMLDivElement>(!!loading);
const xAxisFormatter = useCallback(
(value: number) => (type === 'time' ? new Date(value).toLocaleTimeString() : value),
[type]
);
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
}
},
yAxis: {
...chart.yAxis,
...(yRange || {})
},
series: data?.map(item => ({
...chart.series,
...item
}))
} as EChartOption,
{notMerge: true}
);
}
}, [data, title, legend, xAxis, type, xAxisFormatter, yRange, tooltip, echart]);
useEffect(() => {
if (process.browser) {
setTimeout(() => {
echart.current?.dispatchAction({
type: 'takeGlobalCursor',
key: 'dataZoomSelect',
dataZoomSelectActive: true
});
}, 0);
}
}, [echart]);
return <div className={className} ref={ref}></div>;
};
export default LineChart;
import React, {FunctionComponent} from 'react';
import styled from 'styled-components';
import {useRouter} from 'next/router';
import {useTranslation, Link} from '~/utils/i18n';
import {rem, headerColor, duration, easing, lighten, transitions} from '~/utils/style';
const navItems = ['scalars', 'samples', 'graphs', 'high-dimensional'];
const Nav = styled.nav`
background-color: ${headerColor};
color: #fff;
height: 100%;
width: 100%;
padding: 0 ${rem(20)};
display: flex;
justify-content: flex-start;
align-items: center;
`;
const Logo = styled.a`
font-size: ${rem(20)};
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif,
'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol';
font-weight: 600;
margin-right: ${rem(40)};
> img {
width: ${rem(98)};
height: ${rem(31)};
vertical-align: middle;
margin-right: ${rem(8)};
}
> span {
vertical-align: middle;
}
`;
const NavItem = styled.a<{active: boolean}>`
padding: 0 ${rem(20)};
height: 100%;
display: inline-flex;
justify-content: center;
align-items: center;
background-color: ${headerColor};
${transitions(['background-color'], `${duration} ${easing}`)}
&:hover {
background-color: ${lighten(0.05, headerColor)};
}
> span {
padding: ${rem(10)} 0 ${rem(7)};
border-bottom: ${rem(3)} solid ${props => (props.active ? '#596cd6' : 'transparent')};
${transitions(['border-bottom'], `${duration} ${easing}`)}
text-transform: uppercase;
}
`;
const Navbar: FunctionComponent = () => {
const {t} = useTranslation('common');
const {pathname} = useRouter();
return (
<Nav>
<Logo href="/">
<img alt="PaddlePaddle" src="/images/logo.svg" />
<span>VisualDL</span>
</Logo>
{navItems.map(name => {
const href = `/${name}`;
return (
// https://nextjs.org/docs/api-reference/next/link#if-the-child-is-a-custom-component-that-wraps-an-a-tag
<Link href={href} key={name} passHref>
<NavItem active={pathname === href}>
<span>{t(name)}</span>
</NavItem>
</Link>
);
})}
</Nav>
);
};
export default Navbar;
import React, {FunctionComponent} from 'react';
import styled from 'styled-components';
import {
WithStyled,
em,
primaryColor,
backgroundColor,
borderColor,
textInvertColor,
textColor,
borderRadius,
borderFocusedColor,
duration,
easing,
transitions,
size
} from '~/utils/style';
const height = em(36);
const Wrapper = styled.nav`
display: flex;
user-select: none;
`;
const Ul = styled.ul`
display: inline-flex;
list-style: none;
margin: 0;
padding: 0;
`;
const Li = styled.li`
list-style: none;
margin-left: ${em(10)};
&: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;
border: 1px solid ${props => (props.current ? primaryColor : borderColor)};
border-radius: ${borderRadius};
${transitions(['color', 'border-color', 'background-color'], `${duration} ${easing}`)}
&:hover {
border-color: ${props => (props.current ? primaryColor : borderFocusedColor)};
}
`;
const Span = styled.span`
display: block;
${size(height)}
line-height: ${height};
text-align: center;
`;
type PaginationProps = {
page: number;
total: number;
onChange?: (page: number) => unknown;
};
const Pagination: FunctionComponent<PaginationProps & WithStyled> = ({page, total, className, onChange}) => {
const padding = 2;
const around = 2;
const startEllipsis = page - padding - around - 1 > 0;
const endEllipsis = page + padding + around < total;
const start =
page - around - 1 <= 0 ? [] : Array.from(new Array(Math.min(padding, page - around - 1)), (_v, i) => i + 1);
const end =
page + around >= total
? []
: Array.from(
new Array(Math.min(padding, total - page - around)),
(_v, i) => total - padding + i + 1 + Math.max(padding - total + page + around, 0)
);
const before =
page - 1 <= 0
? []
: Array.from(
new Array(Math.min(around, page - 1)),
(_v, i) => page - around + i + Math.max(around - page + 1, 0)
);
const after = page >= total ? [] : Array.from(new Array(Math.min(around, total - page)), (_v, i) => page + i + 1);
const genLink = (arr: number[]) =>
arr.map(i => (
<Li key={i}>
<A onClick={() => onChange?.(i)}>{i}</A>
</Li>
));
const hellip = (
<Li>
<Span>&hellip;</Span>
</Li>
);
return (
<Wrapper className={className}>
<Ul>
{genLink(start)}
{startEllipsis && hellip}
{genLink(before)}
<Li>
<A current>{page}</A>
</Li>
{genLink(after)}
{endEllipsis && hellip}
{genLink(end)}
</Ul>
</Wrapper>
);
};
export default Pagination;
import React, {FunctionComponent, useContext} from 'react';
import styled from 'styled-components';
import {
WithStyled,
em,
textColor,
textInvertColor,
borderColor,
borderRadius,
backgroundColor,
primaryColor,
duration,
easing,
ellipsis,
transitions,
borderFocusedColor
} from '~/utils/style';
import {ValueContext, EventContext} from '~/components/RadioGroup';
const height = em(36);
const minWidth = em(72);
const maxWidth = em(144);
const Button = styled.a<{selected?: boolean}>`
cursor: pointer;
background-color: ${props => (props.selected ? primaryColor : backgroundColor)};
color: ${props => (props.selected ? textInvertColor : textColor)};
height: ${height};
line-height: calc(${height} - 2px);
min-width: ${minWidth};
${ellipsis(maxWidth)}
text-align: center;
border: 1px solid ${props => (props.selected ? primaryColor : borderColor)};
${transitions(['color', 'border-color', 'background-color'], `${duration} ${easing}`)}
/* bring selected one to top in order to cover the sibling's border */
${props => (props.selected ? 'position: relative;' : '')}
&:hover {
border-color: ${props => (props.selected ? primaryColor : borderFocusedColor)};
}
&:first-of-type {
border-top-left-radius: ${borderRadius};
border-bottom-left-radius: ${borderRadius};
}
&:last-of-type {
border-top-right-radius: ${borderRadius};
border-bottom-right-radius: ${borderRadius};
}
& + & {
margin-left: -1px;
}
`;
type RadioButtonProps = {
selected?: boolean;
title?: string;
value?: string | number | symbol;
};
const RadioButton: FunctionComponent<RadioButtonProps & WithStyled> = ({
className,
value,
selected,
title,
children
}) => {
const groupValue = useContext(ValueContext);
const onChange = useContext(EventContext);
const onClick = () => {
if (value && onChange && groupValue !== value) {
onChange(value);
}
};
return (
<Button className={className} title={title} selected={groupValue === value || selected} onClick={onClick}>
{children}
</Button>
);
};
export default RadioButton;
import React, {FunctionComponent, createContext, useState, useCallback} from 'react';
import styled from 'styled-components';
import {WithStyled} from '~/utils/style';
const Wrapper = styled.div`
display: inline-flex;
> * {
flex-shrink: 0;
align-items: flex-start;
}
`;
export const ValueContext = createContext(null as string | number | symbol | undefined | null);
// eslint-disable-next-line @typescript-eslint/no-empty-function
export const EventContext = createContext((() => {}) as ((value: string | number | symbol) => unknown) | undefined);
type RadioGroupProps = {
value?: string | number | symbol;
onChange?: (value: string | number | symbol) => unknown;
};
const RadioGroup: FunctionComponent<RadioGroupProps & WithStyled> = ({value, onChange, children, className}) => {
const [selected, setSelected] = useState(value);
const onSelectedChange = useCallback(
(value: string | number | symbol) => {
setSelected(value);
onChange?.(value);
},
[onChange]
);
return (
<EventContext.Provider value={onSelectedChange}>
<ValueContext.Provider value={selected}>
<Wrapper className={className}>{children}</Wrapper>
</ValueContext.Provider>
</EventContext.Provider>
);
};
export default RadioGroup;
import React, {FunctionComponent} from 'react';
import styled from 'styled-components';
import {WithStyled, em, size, half, math, primaryColor, textLighterColor, backgroundColor} from '~/utils/style';
import InputRange, {Range} from 'react-input-range';
const height = em(20);
const railHeight = em(4);
const thumbSize = em(12);
const railColor = '#DBDEEB';
const Wrapper = styled.div<{disabled?: boolean}>`
height: ${height};
.input-range {
height: 100%;
position: relative;
&__label {
display: none;
}
&__track {
cursor: ${props => (props.disabled ? 'not-allowed' : 'pointer')};
&--background {
height: ${railHeight};
width: 100%;
position: absolute;
top: 50%;
margin-top: -${half(railHeight)};
background-color: ${railColor};
border-radius: ${half(railHeight)};
}
&--active {
height: ${railHeight};
position: absolute;
background-color: ${props => (props.disabled ? textLighterColor : primaryColor)};
border-radius: ${half(railHeight)};
outline: none;
}
}
&__slider-container {
top: -${math(`(${thumbSize} - ${railHeight}) / 2`)};
margin-left: -${half(thumbSize)};
}
&__slider {
${size(thumbSize)}
border-radius: ${half(thumbSize)};
border: ${em(3)} solid ${props => (props.disabled ? textLighterColor : primaryColor)};
background-color: ${backgroundColor};
}
}
`;
type RangeSliderProps = {
min?: number;
max?: number;
step?: number;
value?: number;
disabled?: boolean;
onChange?: (value: number) => unknown;
onChangeComplete?: () => unknown;
};
const RangeSlider: FunctionComponent<RangeSliderProps & WithStyled> = ({
onChange,
onChangeComplete,
className,
min,
max,
step,
value,
disabled
}) => {
const onChangeRange = (range: number | Range) => onChange?.(range as number);
return (
<Wrapper className={className} disabled={disabled}>
<InputRange
minValue={min}
maxValue={max}
// there may be a warning when `minValue` equals `maxValue` though `allSameValue` is set to TRUE
// this is a bug of react-input-range
// ignore for now
allowSameValues
step={step}
disabled={disabled}
value={value as number}
onChange={onChangeRange}
onChangeComplete={() => onChangeComplete?.()}
/>
</Wrapper>
);
};
RangeSlider.defaultProps = {
min: 0,
max: 100,
step: 1,
value: 50
};
export default RangeSlider;
import React, {FunctionComponent} from 'react';
import styled from 'styled-components';
import {rem} from '~/utils/style';
import {useTranslation} from '~/utils/i18n';
import Select, {SelectValueType} from '~/components/Select';
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, useState, useCallback} from 'react';
import styled from 'styled-components';
import {rem} from '~/utils/style';
import {useTranslation} from '~/utils/i18n';
import Button from '~/components/Button';
const StyledButton = styled(Button)`
margin-top: ${rem(40)};
width: 100%;
text-transform: uppercase;
`;
type RunningToggleProps = {
running?: boolean;
onToggle?: (running: boolean) => unknown;
};
const RunningToggle: FunctionComponent<RunningToggleProps> = ({running, onToggle}) => {
const {t} = useTranslation('common');
const [state, setState] = useState(!!running);
const onClick = useCallback(() => {
setState(s => !s);
onToggle?.(state);
}, [state, onToggle]);
return <StyledButton onClick={onClick}>{t(state ? 'running' : 'stopped')}</StyledButton>;
};
export default RunningToggle;
import React, {FunctionComponent, useState} from 'react';
import styled from 'styled-components';
import useSWR from 'swr';
import {useTranslation} from '~/utils/i18n';
import {em, size, ellipsis, textLightColor} from '~/utils/style';
import StepSlider from '~/components/StepSlider';
import Image from '~/components/Image';
const width = em(430);
const height = em(384);
const Wrapper = styled.div`
${size(height, width)}
padding: ${em(20)};
display: flex;
flex-direction: column;
justify-content: flex-start;
align-items: stretch;
> * {
flex-grow: 0;
flex-shrink: 0;
}
`;
const Title = styled.div`
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: ${em(20)};
> h4 {
font-size: ${em(16)};
font-weight: 700;
flex-shrink: 1;
flex-grow: 1;
padding: 0;
margin: 0;
${ellipsis()}
}
> span {
font-size: ${em(14)};
flex-shrink: 0;
flex-grow: 0;
color: ${textLightColor};
}
`;
const Container = styled.div<{fit?: boolean}>`
flex-grow: 1;
flex-shrink: 1;
margin-top: ${em(20)};
display: flex;
justify-content: center;
align-items: center;
overflow: hidden;
> img {
width: 100%;
height: 100%;
object-fit: ${props => (props.fit ? 'contain' : 'scale-down')};
flex-shrink: 1;
}
`;
type ImageData = {
step: number;
wallTime: number;
};
type SampleChartProps = {
run: string;
tag: string;
fit?: boolean;
running?: boolean;
};
const getImageUrl = (index: number, run: string, tag: string, wallTime: number): string =>
`${process.env.API_URL}/images/individualImage?sample=0&index=${index}&ts=${wallTime}&run=${encodeURIComponent(
run
)}&tag=${encodeURIComponent(tag)}`;
const SampleChart: FunctionComponent<SampleChartProps> = ({run, tag, fit, running}) => {
const {t} = useTranslation('common');
const {data, error} = useSWR<ImageData[]>(
`/images/images?run=${encodeURIComponent(run)}&tag=${encodeURIComponent(tag)}`,
{
refreshInterval: running ? 15 * 1000 : 0
}
);
const [step, setStep] = useState(0);
return (
<Wrapper>
<Title>
<h4>{tag}</h4>
<span>{run}</span>
</Title>
<StepSlider value={step} steps={data?.map(item => item.step) ?? []} onChange={setStep} />
<Container fit={fit}>
{!data && !error && <span>{t('loading')}</span>}
{data && !error && <Image src={getImageUrl(step, run, tag, data[step].wallTime)} />}
</Container>
</Wrapper>
);
};
export default SampleChart;
import React, {FunctionComponent, useCallback, useMemo} from 'react';
import styled from 'styled-components';
import useSWR from 'swr';
import compact from 'lodash/compact';
import minBy from 'lodash/minBy';
import maxBy from 'lodash/maxBy';
import sortBy from 'lodash/sortBy';
import {EChartOption} from 'echarts';
import {em, size} from '~/utils/style';
import {useTranslation} from '~/utils/i18n';
import {cycleFetcher} from '~/utils/fetch';
import {transform, range, tooltip, TooltipData} from '~/utils/scalars';
import * as chart from '~/utils/chart';
import LineChart from '~/components/LineChart';
const width = em(430);
const height = em(320);
const StyledLineChart = styled(LineChart)`
${size(height, width)}
`;
export const xAxisMap = {
step: 1,
relative: 4,
wall: 0
};
export const sortingMethodMap = {
default: null,
descending: (points: TooltipData[]) => sortBy(points, point => point.item[3]).reverse(),
ascending: (points: TooltipData[]) => sortBy(points, point => point.item[3]),
// Compare other ponts width the trigger point, caculate the nearest sort.
nearest: (points: TooltipData[], data: number[]) => sortBy(points, point => point.item[3] - data[2])
};
type DataSet = number[][];
type ScalarChartProps = {
runs: string[];
tag: string;
smoothing: number;
xAxis: keyof typeof xAxisMap;
sortingMethod: keyof typeof sortingMethodMap;
outlier?: boolean;
running?: boolean;
};
const ScalarChart: FunctionComponent<ScalarChartProps> = ({
runs,
tag,
smoothing,
xAxis,
sortingMethod,
outlier,
running
}) => {
const {t} = useTranslation('scalars');
// TODO: maybe we can create a custom hook here
const {data: datasets, error} = useSWR<DataSet[]>(
runs.map(run => `/scalars/scalars?run=${encodeURIComponent(run)}&tag=${encodeURIComponent(tag)}`),
(...urls) => cycleFetcher(urls),
{
refreshInterval: running ? 15 * 1000 : 0
}
);
const type = xAxis === 'wall' ? 'time' : 'value';
const smooth = xAxis !== 'wall';
const smoothedDatasets = useMemo(() => datasets?.map(dataset => transform(dataset, smoothing)), [
datasets,
smoothing
]);
const data = useMemo(
() =>
smoothedDatasets
?.map((dataset, i) => {
// smoothed data:
// [0] wall time
// [1] step
// [2] orginal value
// [3] smoothed value
// [4] relative
const name = runs[i];
return [
{
name,
z: i,
lineStyle: {
width: chart.series.lineStyle.width,
opacity: 0.5
},
data: dataset,
encode: {
x: [xAxisMap[xAxis]],
y: [2]
},
smooth
},
{
name,
z: runs.length + i,
data: dataset,
encode: {
x: [xAxisMap[xAxis]],
y: [3]
},
smooth
}
];
})
.flat(),
[runs, smooth, smoothedDatasets, xAxis]
);
const yRange = useMemo(() => {
const ranges = compact(smoothedDatasets?.map(dataset => range(dataset, outlier)));
const min = minBy(ranges, range => range.min)?.min ?? 0;
const max = maxBy(ranges, range => range.max)?.max ?? 0;
if (!(min === 0 && max === 0)) {
return {
min: min > 0 ? min * 0.9 : min * 1.1,
max: max > 0 ? max * 1.1 : max * 0.9
};
}
}, [outlier, smoothedDatasets]);
const formatter = useCallback(
(params: EChartOption.Tooltip.Format | EChartOption.Tooltip.Format[]) => {
const data = Array.isArray(params) ? params[0].data : params.data;
const step = data[1];
const points =
smoothedDatasets?.map((series, index) => {
let nearestItem;
if (step === 0) {
nearestItem = series[0];
} else {
for (let i = 0; i < series.length; i++) {
const item = series[i];
if (item[1] === step) {
nearestItem = item;
break;
}
if (item[1] > step) {
nearestItem = series[i - 1 >= 0 ? i - 1 : 0];
break;
}
if (!nearestItem) {
nearestItem = series[series.length - 1];
}
}
}
return {
run: runs[index],
item: nearestItem || []
};
}) ?? [];
const sort = sortingMethodMap[sortingMethod];
return tooltip(sort ? sort(points, data) : points);
},
[smoothedDatasets, runs, sortingMethod]
);
return (
<StyledLineChart
title={tag}
legend={runs}
xAxis={t(`x-axis-value.${xAxis}`)}
yRange={yRange}
type={type}
tooltip={formatter}
data={data}
loading={!datasets && !error}
/>
);
};
export default ScalarChart;
import React, {FunctionComponent, useEffect} from 'react';
import {EChartOption} from 'echarts';
import {WithStyled, primaryColor} from '~/utils/style';
import useECharts from '~/hooks/useECharts';
import {Dimension} from '~/types';
type ScatterChartProps = {
data?: ([number, number] | [number, number, number])[];
labels?: string[];
loading?: boolean;
dimension?: Dimension;
};
const ScatterChart: FunctionComponent<ScatterChartProps & WithStyled> = ({
data,
labels,
loading,
dimension,
className
}) => {
const [ref, echart] = useECharts<HTMLDivElement>(!!loading);
useEffect(() => {
if (process.browser) {
(async () => {
const is3D = dimension === '3d';
if (is3D) {
await import('echarts-gl');
}
echart.current?.setOption(
{
...(is3D
? {
yAxis3D: {},
xAxis3D: {},
zAxis3D: {},
grid3D: {}
}
: {
xAxis: {},
yAxis: {}
}),
series: [
{
data,
label: {
show: true,
position: 'top',
formatter: (
params: EChartOption.Tooltip.Format | EChartOption.Tooltip.Format[]
) => {
if (!labels) {
return '';
}
const {dataIndex: index} = Array.isArray(params) ? params[0] : params;
if (index == null) {
return '';
}
return labels[index] ?? '';
}
},
symbolSize: 12,
itemStyle: {
color: primaryColor
},
type: is3D ? 'scatter3D' : 'scatter'
}
]
},
{notMerge: true}
);
})();
}
}, [data, labels, dimension, echart]);
return <div className={className} ref={ref}></div>;
};
export default ScatterChart;
import React, {FunctionComponent} from 'react';
import styled from 'styled-components';
import {WithStyled, em, math, textLighterColor} from '~/utils/style';
import Input, {padding, InputProps} from '~/components/Input';
import Icon from '~/components/Icon';
const iconSize = em(16);
const StyledInput = styled(Input)`
padding-right: ${math(`${iconSize} + ${padding} * 2`)};
width: 100%;
`;
const Control = styled.div`
background-color: #fff;
position: relative;
`;
const SearchIcon = styled(Icon)`
font-size: ${iconSize};
display: block;
position: absolute;
top: ${padding};
right: ${padding};
pointer-events: none;
color: ${textLighterColor};
`;
const SearchInput: FunctionComponent<InputProps & WithStyled> = ({className, ...props}) => (
<Control className={className}>
<StyledInput {...props} />
<SearchIcon type="search" />
</Control>
);
export default SearchInput;
import React, {FunctionComponent, useState, useCallback} from 'react';
import styled from 'styled-components';
import without from 'lodash/without';
import useClickOutside from '~/hooks/useClickOutside';
import {useTranslation} from '~/utils/i18n';
import {
WithStyled,
em,
backgroundColor,
backgroundFocusedColor,
textLighterColor,
selectedColor,
borderColor,
borderFocusedColor,
borderRadius,
duration,
easing,
ellipsis,
transitions,
css
} from '~/utils/style';
import Checkbox from '~/components/Checkbox';
import Icon from '~/components/Icon';
export const padding = em(10);
export const height = em(36);
const minWidth = em(160);
// prettier-ignore
const Wrapper = styled.div<{opened?: boolean}>`
height: ${height};
line-height: calc(${height} - 2px);
min-width: ${minWidth};
display: inline-block;
position: relative;
border: 1px solid ${borderColor};
/* eslint-disable-next-line */
border-radius: ${borderRadius} ${borderRadius} ${props => (props.opened ? '0 0' : `${borderRadius} ${borderRadius}`)};
transition: border-color ${duration} ${easing};
background-color: ${backgroundColor};
&:hover {
border-color: ${borderFocusedColor};
}
`;
const Trigger = styled.div<{selected?: boolean}>`
padding: ${padding};
display: inline-flex;
width: 100%;
height: 100%;
justify-content: space-between;
align-items: center;
cursor: pointer;
${props => (props.selected ? '' : `color: ${textLighterColor}`)}
`;
const TriggerIcon = styled(Icon)<{opened?: boolean}>`
width: ${em(14)};
height: ${em(14)};
text-align: center;
display: block;
flex-shrink: 0;
transform: rotate(${props => (props.opened ? '180' : '0')}deg) scale(${10 / 14});
transition: transform ${duration} ${easing};
`;
const Label = styled.span`
flex-grow: 1;
`;
const List = styled.div<{opened?: boolean; empty?: boolean}>`
position: absolute;
top: 100%;
width: calc(100% + 2px);
left: -1px;
padding: ${padding} 0;
border: inherit;
border-top-color: ${borderColor};
border-radius: 0 0 ${borderRadius} ${borderRadius};
display: ${props => (props.opened ? 'block' : 'none')};
z-index: 9999;
line-height: 1;
background-color: inherit;
box-shadow: 0 5px 6px 0 rgba(0, 0, 0, 0.05);
${props =>
props.empty
? `
color: ${textLighterColor};
text-align: center;
`
: ''}
`;
const listItem = css`
display: block;
cursor: pointer;
padding: 0 ${padding};
height: ${height};
line-height: ${height};
width: 100%;
${transitions(['color', 'background-color'], `${duration} ${easing}`)}
&:hover {
background-color: ${backgroundFocusedColor};
}
`;
const ListItem = styled.div<{selected?: boolean}>`
${ellipsis()}
${listItem}
${props => (props.selected ? `color: ${selectedColor};` : '')}
`;
const MultipleListItem = styled(Checkbox)<{selected?: boolean}>`
${listItem}
display: flex;
align-items: center;
`;
export type SelectValueType = string | number | symbol;
type SelectListItem<T> = {
value: T;
label: string;
};
type SelectProps<T> = {
list?: (SelectListItem<T> | string)[];
value?: T | T[];
onChange?: (value: T | T[]) => unknown;
multiple?: boolean;
placeholder?: string;
};
const Select: FunctionComponent<SelectProps<SelectValueType> & WithStyled> = ({
list: propList,
value: propValue,
placeholder,
multiple,
className,
onChange
}) => {
const {t} = useTranslation('common');
const [isOpened, setIsOpened] = useState(false);
const toggleOpened = useCallback(() => setIsOpened(!isOpened), [isOpened]);
const setIsOpenedFalse = useCallback(() => setIsOpened(false), []);
const [value, setValue] = useState(multiple ? (Array.isArray(propValue) ? propValue : []) : propValue);
const isSelected = !!(multiple ? value && (value as SelectValueType[]).length !== 0 : (value as SelectValueType));
const changeValue = (mutateValue: SelectValueType, checked?: boolean) => {
let newValue;
if (multiple) {
newValue = value as SelectValueType[];
if (checked) {
if (!newValue.includes(mutateValue)) {
newValue = [...newValue, mutateValue];
}
} else {
if (newValue.includes(mutateValue)) {
newValue = without(newValue, mutateValue);
}
}
} else {
newValue = mutateValue;
}
setValue(newValue);
onChange?.(newValue);
if (!multiple) {
setIsOpenedFalse();
}
};
const ref = useClickOutside(setIsOpenedFalse);
const list = propList?.map(item => ('string' === typeof item ? {value: item, label: item} : item)) ?? [];
const isListEmpty = list.length === 0;
const findLabelByValue = (v: SelectValueType) => list.find(item => item.value === v)?.label ?? '';
const label = isSelected
? multiple
? (value as SelectValueType[]).map(findLabelByValue).join(' / ')
: findLabelByValue(value as SelectValueType)
: placeholder || t('select');
return (
<Wrapper ref={ref} opened={isOpened} className={className}>
<Trigger onClick={toggleOpened} selected={isSelected} title={isSelected && label ? String(label) : ''}>
<Label>{label}</Label>
<TriggerIcon opened={isOpened} type="chevron-down" />
</Trigger>
<List opened={isOpened} empty={isListEmpty}>
{isListEmpty
? t('empty')
: list.map((item, index) => {
if (multiple) {
return (
<MultipleListItem
value={(value as SelectValueType[]).includes(item.value)}
key={index}
size="small"
onChange={checked => changeValue(item.value, checked)}
>
{item.label}
</MultipleListItem>
);
}
return (
<ListItem
selected={item.value === value}
key={index}
onClick={() => changeValue(item.value)}
>
{item.label}
</ListItem>
);
})}
</List>
</Wrapper>
);
};
export default Select;
import React, {FunctionComponent, useState} from 'react';
import styled from 'styled-components';
import {useTranslation} from '~/utils/i18n';
import Field from '~/components/Field';
import RangeSlider from '~/components/RangeSlider';
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 React, {FunctionComponent, useState} from 'react';
import styled from 'styled-components';
import {em, textLightColor} from '~/utils/style';
import {useTranslation} from '~/utils/i18n';
import RangeSlider from '~/components/RangeSlider';
const Label = styled.div`
color: ${textLightColor};
font-size: ${em(12)};
margin-bottom: ${em(5)};
`;
const FullWidthRangeSlider = styled(RangeSlider)`
width: 100%;
`;
type StepSliderProps = {
value: number;
steps: number[];
onChange?: (value: number) => unknown;
};
const StepSlider: FunctionComponent<StepSliderProps> = ({onChange, value, steps}) => {
const {t} = useTranslation('samples');
const [step, setStep] = useState(value);
return (
<>
<Label>{`${t('step')}: ${steps[step]}`}</Label>
<FullWidthRangeSlider
min={0}
max={steps.length ? steps.length - 1 : 0}
step={1}
value={step}
onChange={setStep}
onChangeComplete={() => onChange?.(step)}
/>
</>
);
};
export default StepSlider;
import React, {FunctionComponent} from 'react';
import styled from 'styled-components';
import {
WithStyled,
em,
primaryColor,
lightColor,
lightFocusedColor,
lightActiveColor,
duration,
easing,
math,
transitions
} from '~/utils/style';
const height = em(36);
const Span = styled.span<{active?: boolean}>`
padding: 0 ${em(16)};
height: ${height};
line-height: ${height};
display: inline-block;
border-radius: ${math(`${height} / 2`)};
${transitions(['color', 'background-color'], `${duration} ${easing}`)}
color: ${prop => (prop.active ? '#FFF' : primaryColor)};
background-color: ${prop => (prop.active ? primaryColor : lightColor)};
cursor: pointer;
&:hover {
background-color: ${prop => (prop.active ? primaryColor : lightFocusedColor)};
}
&:active {
background-color: ${prop => (prop.active ? primaryColor : lightActiveColor)};
}
`;
type TagProps = {
title?: string;
active?: boolean;
onClick?: () => void;
};
const Tag: FunctionComponent<TagProps & WithStyled> = ({children, ...props}) => <Span {...props}>{children}</Span>;
export default Tag;
import React, {FunctionComponent, useState, useCallback} from 'react';
import styled from 'styled-components';
import groupBy from 'lodash/groupBy';
import sortBy from 'lodash/sortBy';
import {useTranslation} from '~/utils/i18n';
import {rem, math, ellipsis} from '~/utils/style';
import SearchInput from '~/components/SearchInput';
import Tag from '~/components/Tag';
import {Tag as TagType} from '~/types';
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 = sortBy(
Object.entries(groupBy<TagType>(propTags || [], tag => tag.label.split('/')[0])).map(([label, tags]) => ({
label,
tags
})),
tag => tag.label
);
const [matchedCount, setMatchedCount] = useState(propTags?.length ?? 0);
const [inputValue, setInputValue] = useState(value || '');
const [selectedValue, setSelectedValue] = useState('');
const hasSelectedValue = selectedValue !== '';
const allText = inputValue || t('all');
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;
import React, {FunctionComponent} from 'react';
import Head from 'next/head';
const Title: FunctionComponent = ({children: title}) => (
<Head>
<title>
{'string' === typeof title ? `${title} - ` : ''}
{process.env.title}
</title>
</Head>
);
export default Title;
<template>
<div id="app">
<ui-app-menu
selected="{{route}}"
on-item-click="menuChange($event)"
></ui-app-menu>
<div id="content-container" class="visual-dl-content-container">
<div id="app-content" class="visual-dl-app-content">
<div id="content"></div>
</div>
</div>
</div>
</template>
<script>
import AppMenu from './common/component/AppMenu';
import {router} from 'san-router';
import {routeTo} from './common/util/routeTo';
export default {
components: {
'ui-app-menu': AppMenu
},
initData() {
return {
route: 'scalars'
};
},
attached() {
router.start();
let route;
if (location.hash) {
route = /(\#\/)(\w*)([?|&]{0,1})/.exec(location.hash)[2];
this.data.set('route', route);
}
else {
location.hash = '#/scalars';
}
},
menuChange(dataItem) {
routeTo(dataItem.url);
}
};
</script>
<style lang="stylus">
@import './style/variables';
// modify component style for custom theme
.sm-appbar
background $-theme-color
.sm-pagination .page-selector span.current
background $-theme-color
.sm-text-field.has-label .sm-text-field-label
color $-right-font-color
.sm-text-field-input
color $-right-font-color
.sm-text-field-hint.state-show
color $-right-font-color
.sm-text-field
.sm-text-field-line
background $-right-font-color
.sm-text-field-focus-line
background $-theme-color
.sm-form-item .label
color $-right-font-color
.sm-slider-bg
background $-right-font-color
.sm-slider-fill,
.sm-slider-thumb
background $-theme-color
.sm-radio-icon-checked,
.sm-checkbox-icon-checked
color $-theme-color
.sm-radio .sm-radio-wrapper .sm-radio-label,
.sm-checkbox .sm-radio-wrapper .sm-radio-label,
.sm-radio .sm-checkbox-wrapper .sm-radio-label,
.sm-checkbox .sm-checkbox-wrapper .sm-radio-label,
.sm-radio .sm-radio-wrapper .sm-checkbox-label,
.sm-checkbox .sm-radio-wrapper .sm-checkbox-label,
.sm-radio .sm-checkbox-wrapper .sm-checkbox-label,
.sm-checkbox .sm-checkbox-wrapper .sm-checkbox-label
color $-right-font-color
.sm-button.variant-secondery.variant-raised,
.sm-button.variant-secondery.variant-floating
background $-theme-color
+prefix-classes('visual-dl-page-')
.container
padding-right 250px
position relative
background $-left-background-color
.left
width 100%
overflow scroll
border solid 1px $-left-border-clor
background $-left-border-clor
min-height 300px
padding 2%
box-sizing border-box
.right
overflow scroll
width 250px
min-height 300px
position absolute
right 0
top 0
box-sizing border-box
.config-com
color $-right-font-color
</style>
<template>
<div class="visual-dl-app-menu">
<san-appbar title=" ">
<san-menu slot="right">
<san-menu-item
s-for="item in items"
class="{{selected === item.name ? 'sm-menu-item-selected' : ''}}"
on-click="handleItemClick(item)"
title="{{item.title}}" />
</san-menu>
</san-appbar>
</div>
</template>
<script>
import Appbar from 'san-mui/AppBar';
import {MenuItem, Menu} from 'san-mui/Menu';
export default {
components: {
'san-appbar': Appbar,
'san-menu': Menu,
'san-menu-item': MenuItem
},
initData() {
return {
selected: 'scalars',
items: [
{
url: '/scalars',
title: 'SCALARS',
name: 'scalars'
},
{
url: '/images',
title: 'IMAGES',
name: 'images'
},
{
url: '/histograms',
title: 'HISTOGRAMS',
name: 'histograms'
},
{
url: '/graphs',
title: 'GRAPHS',
name: 'graphs'
}
]
};
},
handleItemClick(item) {
this.data.set('selected', item.name);
this.fire('item-click', item);
}
};
</script>
<style lang="stylus">
@import '../../style/variables';
+prefix-classes(prefix)
.app-menu
width 100%
.visual-dl-app-menu
.sm-appbar-title
font-size 24px
flex none
margin-right 50px
.sm-appbar-title
background url('./visualdl-logo.png') no-repeat
background-size cover
width 120px
height 50px
.sm-appbar-right
width 100%
.sm-menu
width 100%
height 100%
display flex
flex-direction row
.sm-menu-item
padding 0 30px
.sm-menu-item-content
color #fff
opacity 0.6
.sm-menu-item:hover
background none
opacity 1
.sm-menu-item-selected
.sm-menu-item-content
color #fff
opacity 1
</style>
<template>
<div class="sm-form-item">
<label class="label">{{label}}</label>
<div class="group-box">
<san-checkbox
s-if="showAll"
class="checkbox-all"
label="全选"
value="all"
on-change="handleAllChange($event)"
checked="{{All}}"
></san-checkbox>
<div class="san-form-check-group">
<san-checkbox
s-for="item in items"
label="{{item.name}}"
value="{{item.value}}"
disabled="{{item.disabled}}"
checked="{{value}}"
on-change="handleChange($event, item)"
></san-checkbox>
</div>
</div>
</div>
</template>
<script>
import Checkbox from 'san-mui/Checkbox';
import {DataTypes} from 'san';
export default {
components: {
'san-checkbox': Checkbox
},
dataTypes: {
items: DataTypes.array,
value: DataTypes.array
},
initData() {
return {
allValue: [],
items: {},
value: [],
All: [],
showAll: false
};
},
computed: {
allValue() {
let items = this.data.get('items') || [];
return items.map(item => item.value);
}
},
attached() {
this.jugdeAll();
this.watch('value', val => {
this.dispatch('UI:form-item-change');
this.jugdeAll();
});
},
handleChange(e, item) {
let checked = e.target.checked;
let valueItem = item.value;
if (checked) {
this.addValue(valueItem);
}
else {
this.removeValue(valueItem);
}
this.dispatch('UI:form-item-change');
this.jugdeAll();
},
handleAllChange(e) {
let checked = e.target.checked;
let allValue = this.data.get('allValue') || [];
if (checked) {
this.data.set('value', allValue.slice());
this.fire('valueChange', allValue.slice());
}
else {
this.data.set('value', []);
this.fire('valueChange', []);
}
this.dispatch('UI:form-item-change');
},
addValue(itemValue) {
let value = this.data.get('value') || [];
if (!value.includes(itemValue)) {
value.push(itemValue);
this.data.set('value', value.slice());
this.fire('valueChange', value.slice());
}
},
removeValue(itemValue) {
let value = this.data.get('value') || [];
if (value.includes(itemValue)) {
let index = value.indexOf(itemValue);
value.splice(index, 1);
this.data.set('value', value.slice());
this.fire('valueChange', value.slice());
}
},
jugdeAll() {
let allValue = this.data.get('allValue') || [];
let value = this.data.get('value') || [];
let isAll = allValue.every(val => value.includes(val));
if (isAll) {
this.data.set('All', ['all']);
}
else {
this.data.set('All', []);
}
}
};
</script>
<template>
<div class="sm-form-item">
<label class="label">{{label}}</label>
<san-drop-down-menu
error="{{error}}"
disabled="{{disabled}}"
value="{=value=}"
maxHeight="{{200}}"
autoWidth="{{false}}">
<san-menu-item
s-for="item in items"
on-change="menuItemChange(item)"
value="{{item.value}}"
label="{{item.name}}" />
</sm-drop-down-menu>
</div>
</template>
<script>
import {DataTypes} from 'san';
import {MenuItem, DropDownMenu} from 'san-mui/Menu';
export default {
components: {
'san-menu-item': MenuItem,
'san-drop-down-menu': DropDownMenu
},
dataTypes: {
value: DataTypes.string,
items: DataTypes.array,
disabled: DataTypes.bool
},
menuItemChange(item) {
let value = item.value;
this.fire('valueChange', value);
this.dispatch('UI:form-item-change');
},
initData() {
return {
value: '',
items: []
};
}
};
</script>
<template>
<div class="visaul-dl-expand-panel">
<h3
class="visaul-dl-expand-head"
on-click="handleHeadClick()"
>
<span>{{title}}</span>
<span class="visaul-dl-expand-head-info">
<ui-icon class="visaul-dl-expand-head-arrow" size="20">{{iconName}}</ui-icon>
<span class="visaul-dl-expand-head-num">({{info}})</span>
</span>
</h3>
<div
s-if="{{isShow}}"
class="visaul-dl-expand-panel-content"
>
<slot></slot>
</div>
</div>
</template>
<script>
import Icon from 'san-mui/Icon';
export default {
components: {
'ui-icon': Icon
},
computed: {
iconName() {
let isShow = this.data.get('isShow');
return isShow ? 'expand_less' : 'expand_more';
}
},
initData() {
return {
isShow: false
};
},
handleHeadClick() {
this.toogleShow();
},
toogleShow() {
let isShow = this.data.get('isShow');
this.data.set('isShow', !isShow);
}
};
</script>
<style lang="stylus">
.visaul-dl-expand-panel
.visaul-dl-expand-head
border solid 1px #ccc
line-height 50px
height 50px
padding 0 20px
cursor pointer
position relative
.visaul-dl-expand-head-info
position absolute
left 90%
.visaul-dl-expand-head-arrow
vertical-align middle
.visaul-dl-expand-head-num
line-height 20px
font-size 12px
font-weight normal
.visaul-dl-expand-panel-content
padding 0 20px
.visaul-dl-expand-panel-content:after
content: "";
clear: both;
display: block;
</style>
<template>
<div class="sm-form-item">
<label class="label">{{label}}</label>
<div class="group-box">
<san-radio
on-change="handleChange($event)"
s-for="item in items"
label="{{item.name}}"
value="{{item.value}}"
disabled="{{item.disabled}}"
checked="{=value=}"
></san-radio>
</div>
</div>
</template>
<script>
import Radio from 'san-mui/Radio';
import {DataTypes} from 'san';
export default {
components: {
'san-radio': Radio
},
dataTypes: {
items: DataTypes.array,
value: DataTypes.string
},
initData() {
return {
items: {},
value: [],
label: ''
};
},
attached(value) {
this.watch('value', val => {
this.dispatch('UI:form-item-change', val);
});
},
handleChange(val) {
this.fire('valueChange', val);
}
};
</script>
\ No newline at end of file
<template>
<div class="sm-form-item">
<label class="label">{{label}}</label>
<div class="input-box">
<san-slider
on-change="handleSlideChange($event)"
value="{{value}}"
min="{{min}}"
max="{{max}}"
step="{{step}}"
/>
<span>{{value}}</span>
</div>
</div>
</template>
<script>
import Slider from 'san-mui/Slider';
import InputNumber from 'san-mui/InputNumber';
import {debounce} from 'lodash';
export default {
components: {
'san-slider': Slider,
'san-input-number': InputNumber
},
initData() {
return {
value: '0',
label: '',
max: 0,
min: 0,
step: 0,
debounce: 400
};
},
inited() {
let debounceTime = this.data.get('debounce');
this.handleSlideChange = debounce(val => {
this.data.set('value', val.toString());
}, debounceTime);
this.handlerChange = debounce(e => {
this.data.set('value', e.target.value);
}, debounceTime);
}
};
</script>
<style lang="stylus">
.sm-form-item
.input-box
margin-top 10px
.sm-slider
display inline-block
margin-right 20px
margin-top 4px
float left
width 160px
.sm-slider-thumb
transform translate(0, -50%)
.sm-inputNumber
width 130px
display inline-block
</style>
<template>
<div class="visual-dl-page-container">
<div class="visual-dl-page-left">
<ui-chart
filtScreen="{{filtScreen}}"
download="{{download}}"
scale="{{config.scale}}"
></ui-chart>
</div>
<div class="visual-dl-page-right">
<div class="visual-dl-page-config-container">
<ui-config
runsItems="{{runsItems}}"
config="{=config=}"
on-fitScreen="handleFitScreen"
on-download="handleDownload"
></ui-config>
</div>
</div>
</div>
</template>
<script>
import autoAdjustHeight from '../common/util/autoAdjustHeight';
import config from './ui/config';
import chart from './ui/chart';
export default {
components: {
'ui-config': config,
'ui-chart': chart
},
initData() {
return {
runsArray: [],
tags: [],
config: {
groupNameReg: '.*',
horizontal: 'step',
chartType: 'offset',
runs: [],
running: true,
scale: 0.5
}
};
},
attached() {
autoAdjustHeight();
},
handleFitScreen() {
this.data.set('filtScreen', {
filtScreen: true
});
this.data.set('config.scale', 0.5);
},
handleDownload() {
this.data.set('download', {
filtScreen: true
});
}
};
</script>
<style lang="stylus">
</style>
import {router} from 'san-router';
import Graph from './Graph';
router.add({
target: '#content',
rule: '/graphs',
Component: Graph,
});
<template>
<div class="visual-dl-graph-charts">
<div s-if="graphUrl" class="visual-dl-img-box">
<div class="small-img-box">
<img class="small-img" width="30" src="{{graphUrl}}" />
<div class="screen-handler"></div>
</div>
<img class="draggable" width="{{computedWidth}}" src="{{graphUrl}}" />
</div>
</div>
</template>
<script>
// libs
import echarts from 'echarts';
import {
dragMovelHandler,
tansformElement,
relativeMove
} from './dragHelper';
// service
import {getPluginGraphsGraph} from '../../service';
// https://github.com/taye/interact.js
import interact from 'interactjs';
export default {
computed: {
computedWidth() {
let scale = this.data.get('scale');
return Math.floor(scale * 2 * 700);
}
},
initData() {
return {
width: 800,
height: 600,
data: [
{
name: 'train',
value: []
}
]
};
},
inited() {
this.watch('filtScreen', filtScreen => {
this.clearDragedTransform(this.getBigImgEl());
this.clearDragedTransform(this.getSmallImgDragHandler());
});
this.watch('download', download => {
let aEl = document.createElement('a');
aEl.href = this.data.get('graphUrl');
aEl.download = 'graph.png';
aEl.click();
});
},
attached() {
this.getOriginChartsData();
},
createChart() {
let el = this.el.getElementsByClassName('visual-dl-chart-box')[0];
this.myChart = echarts.init(el);
},
initChartOption(data) {
this.setChartOptions(data);
},
setChartOptions(data) {
this.myChart.setOption(data);
},
getOriginChartsData() {
getPluginGraphsGraph().then(({status, data}) => {
if (+status === 0 && data.url) {
this.data.set('graphUrl', data.url);
this.addDragEventForImg();
}
});
},
clearDragedTransform(dragImgEl) {
dragImgEl.style.transform = 'none';
dragImgEl.setAttribute('data-x', 0);
dragImgEl.setAttribute('data-y', 0);
},
getBigImgEl() {
return this.el.getElementsByClassName('draggable')[0];
},
getSmallImgEl() {
return this.el.getElementsByClassName('small-img')[0];
},
getSmallImgDragHandler() {
return this.el.getElementsByClassName('screen-handler')[0];
},
addDragEventForImg() {
let that = this;
// target elements with the "draggable" class
interact('.draggable').draggable({
// enable inertial throwing
inertia: true,
autoScroll: true,
// call this function on every dragmove event
onmove(event) {
dragMovelHandler(event, (target, x, y) => {
tansformElement(target, x, y);
// compute the proportional value
let triggerEl = that.getBigImgEl();
let relativeEl = that.getSmallImgDragHandler();
relativeMove({triggerEl, x, y}, relativeEl);
});
}
});
interact('.screen-handler').draggable({
// enable inertial throwing
inertia: true,
autoScroll: true,
restrict: {
restriction: 'parent',
endOnly: false,
elementRect: {
top: 0,
left: 0,
bottom: 1,
right: 1
}
},
// call this function on every dragmove event
onmove(event) {
dragMovelHandler(event, (target, x, y) => {
tansformElement(target, x, y);
// compute the proportional value
let triggerEl = that.getSmallImgEl();
let relativeEl = that.getBigImgEl();
relativeMove({triggerEl, x, y}, relativeEl);
});
}
});
}
};
</script>
<style lang="stylus">
.visual-dl-graph-charts
width 90%
margin 0 auto
margin-bottom 20px
.visual-dl-chart-box
height 600px
.visual-dl-img-box
border solid 1px #e4e4e4
position relative
background #f0f0f0
overflow hidden
img
box-sizing border-box
margin 0 auto
display block
.small-img-box
width 30px
box-sizing border-box
position absolute
right 0
top 0
border-left solid 1px #e4e4e4
border-bottom solid 1px #e4e4e4
background #f0f0f0
z-index 1
.screen-handler
box-sizing border-box
position absolute
width 30px
height 20px
border solid 1px #666
top 0
left -1px
</style>
<template>
<div class="visual-dl-graph-config-com">
<ui-button
class="visual-dl-graph-config-button"
on-click="handleFitScreen"
variants="raised secondery">
<ui-icon style="margin-right: 6px">fullscreen</ui-icon>
Fit &nbsp; to &nbsp; screen
</ui-button>
<ui-button
class="visual-dl-graph-config-button"
on-click="handleDownload"
variants="raised secondery">
<ui-icon style="margin-right: 6px">file_download</ui-icon>
Download image
</ui-button>
<ui-slider
label="Scale"
value="{=config.scale=}"
min="{{0.1}}"
max="{{1}}"
step="{{0.1}}"
/>
</div>
</template>
<script>
import Button from 'san-mui/Button';
import Icon from 'san-mui/Icon';
import Slider from '../../common/component/Slider';
import DropDownMenu from '../../common/component/DropDownMenu';
export default {
components: {
'ui-dropdown-menu': DropDownMenu,
'ui-button': Button,
'ui-icon': Icon,
'ui-slider': Slider
},
initData() {
return {
config: {
groupName: 'aa',
isActualImageSize: [],
runs: [],
scale: 0.5
},
sessionItems: [
{
name: 'step1',
value: 'step1'
},
{
name: 'step2',
value: 'step2'
},
{
name: 'step3',
value: 'step3'
}
]
};
},
handleFitScreen() {
this.fire('fitScreen');
},
handleDownload() {
this.fire('download');
}
};
</script>
<style lang="stylus">
@import '../../style/variables';
+prefix-classes('visual-dl-graph-')
.config-com
width 90%
margin 20px auto
color $-right-font-color
.config-button
width 200px
margin-top 20px
.visual-dl-graph-config-com
.sm-icon
width 36px
height 26px
</style>
<template>
<div class="visual-dl-page-container">
<div class="visual-dl-page-left">
<ui-chart-page
expand="{{true}}"
config="{{config}}"
runsItems="{{runsItems}}"
tagList="{{filteredTagsList}}"
title="Tags matching {{config.groupNameReg}}"
></ui-chart-page>
<ui-chart-page
s-for="item in groupedTags"
config="{{config}}"
runsItems="{{runsItems}}"
tagList="{{item.tags}}"
title="{{item.group}}"
></ui-chart-page>
</div>
<div class="visual-dl-page-right">
<div class="visual-dl-page-config-container">
<ui-config
runsItems="{{runsItems}}"
config="{=config=}"
></ui-config>
</div>
</div>
</div>
</template>
<script>
import {getPluginHistogramsTags, getRuns} from '../service';
import config from './ui/config';
import chartPage from './ui/chartPage';
import {debounce, flatten, uniq} from 'lodash';
import autoAdjustHeight from '../common/util/autoAdjustHeight';
export default {
components: {
'ui-config': config,
'ui-chart-page': chartPage
},
computed: {
runsItems() {
let runsArray = this.data.get('runsArray') || [];
return runsArray.map(item => {
return {
name: item,
value: item
};
});
},
tagsList() {
let tags = this.data.get('tags');
let runs = Object.keys(tags);
let tagsArray = runs.map(run => Object.keys(tags[run]));
let allUniqTags = uniq(flatten(tagsArray));
// get the data for every chart
return allUniqTags.map(tag => {
let tagList = runs.map(run => {
return {
run,
tag: tags[run][tag]
};
}).filter(item => item.tag !== undefined);
return {
tagList,
tag,
group: tag.split('/')[0]
};
});
},
groupedTags() {
let tagsList = this.data.get('tagsList') || [];
// put data in group
let groupData = {};
tagsList.forEach(item => {
let group = item.group;
if (groupData[group] === undefined) {
groupData[group] = [];
groupData[group].push(item);
}
else {
groupData[group].push(item);
}
});
// to array
let groups = Object.keys(groupData);
return groups.map(group => {
return {
group,
tags: groupData[group]
};
});
}
},
initData() {
return {
runsArray: [],
tags: [],
config: {
groupNameReg: '.*',
horizontal: 'step',
chartType: 'offset',
runs: [],
running: true
}
};
},
inited() {
getPluginHistogramsTags().then(({errno, data}) => {
this.data.set('tags', data);
// filter when inited
let groupNameReg = this.data.get('config.groupNameReg');
this.filterTagsList(groupNameReg);
});
getRuns().then(({errno, data}) => {
this.data.set('runsArray', data);
this.data.set('config.runs', data);
});
// Need debounce, can't use computed
this.watch('config.groupNameReg', debounce(this.filterTagsList, 300));
},
attached() {
autoAdjustHeight();
},
filterTagsList(groupNameReg) {
if (!groupNameReg) {
this.data.set('filteredTagsList', []);
return;
}
let tagsList = this.data.get('tagsList') || [];
let regExp = new RegExp(groupNameReg);
let filtedTagsList = tagsList.filter(item => regExp.test(item.tag));
this.data.set('filteredTagsList', filtedTagsList);
}
};
</script>
<style lang="stylus">
</style>
import {router} from 'san-router';
import Histogram from './Histogram';
router.add({
target: '#content',
rule: '/histograms',
Component: Histogram,
});
<template>
<div class="visual-dl-histogram-charts">
<div class="visual-dl-chart-box">
</div>
<div class="visual-dl-chart-actions">
<san-button on-click="expandArea">
<san-icon size="20">settings_overscan</san-icon>
</san-button>
</div>
</div>
</template>
<script>
// components
import Button from 'san-mui/Button';
import Icon from 'san-mui/Icon';
// libs
import echarts from 'echarts';
import {originDataToChartData} from '../histogramHelper';
import {format, precisionRound} from 'd3-format';
// service
import {getPluginHistogramsHistograms} from '../../service';
let zrDrawElement = {};
zrDrawElement.hoverDots = [];
// the time to refresh chart data
const intervalTime = 15;
const p = Math.max(0, precisionRound(0.01, 1.01) - 1);
const yValueFormat = format('.' + p + 'e');
export default {
components: {
'san-button': Button,
'san-icon': Icon
},
initData() {
return {
data: [
{
name: 'train',
value: []
}
]
};
},
inited() {
this.watch('originData', data => {
this.initChartOption();
});
this.watch('chartType', chartType => {
this.initChartOption();
});
},
attached() {
let tagInfo = this.data.get('tagInfo');
this.initChart(tagInfo);
if (this.data.get('running')) {
this.startInterval();
}
this.watch('running', running => {
running ? this.startInterval() : this.stopInterval();
});
},
detached() {
this.stopInterval();
},
initChart(tagInfo) {
this.createChart();
this.getOriginChartData(tagInfo);
},
createChart() {
let el = this.el.getElementsByClassName('visual-dl-chart-box')[0];
this.myChart = echarts.init(el);
},
initChartOption() {
this.myChart.clear();
let zr = this.myChart.getZr();
let hoverDots = zrDrawElement.hoverDots;
if (hoverDots != null && hoverDots.length !== 0) {
hoverDots.forEach(dot => zr.remove(dot));
}
let chartType = this.data.get('chartType');
let data = this.data.get('originData');
let visData = originDataToChartData(data);
let tagInfo = this.data.get('tagInfo');
let title = tagInfo.tag.displayName + '(' + tagInfo.run + ')';
this.setChartOptions(visData, title, chartType);
},
setChartOptions(visData, tag, chartType) {
let grid = {
left: '15%',
top: '15%',
right: '10%',
bottom: '8%'
};
let title = {
text: tag,
textStyle: {
fontSize: '12'
}
};
if (chartType === 'overlay') {
this.setOverlayChartOption(visData, title, grid);
}
else if (chartType === 'offset') {
this.setOffsetChartOption(visData, title, grid);
}
},
setOverlayChartOption({chartData, min, max}, title, grid) {
let seriesOption = chartData.map(({time, step, items}) => ({
name: 'step' + step,
type: 'line',
showSymbol: false,
hoverAnimation: false,
z: 0,
data: items,
animationDuration: 100,
lineStyle: {
normal: {
width: 1,
color: '#008c99'
}
},
encode: {
x: [2],
y: [3]
}
})
);
let option = {
title: title,
axisPointer: {
link: {xAxisIndex: 'all'},
show: true,
snap: true,
triggerTooltip: true
},
grid: grid,
xAxis: {
type: 'value'
},
yAxis: {
type: 'value',
axisLine: {
onZero: false
},
axisLabel: {
formatter(value, index) {
return yValueFormat(value);
}
},
axisPointer: {
label: {
formatter({value}) {
return yValueFormat(value);
}
}
}
},
series: seriesOption
};
let zr1 = this.myChart.getZr();
zr1.on('mousemove', function (e) {
zr1.remove(zrDrawElement.hoverLine);
zr1.remove(zrDrawElement.tooltip);
zr1.remove(zrDrawElement.tooltipX);
zr1.remove(zrDrawElement.tooltipY);
zrDrawElement.hoverDots.forEach(dot => zr1.remove(dot));
zrDrawElement.hoverDots.length = 0;
});
this.myChart.setOption(option, {notMerge: true});
},
setOffsetChartOption({chartData, min, max}, title, grid) {
let rawData = [];
let minX = min;
let maxX = max;
let minZ = Infinity;
let maxZ = -Infinity;
let ecChart = this.myChart;
let maxStep = -Infinity;
let minStep = Infinity;
grid.top = '42%';
grid.left = '4%';
grid.right = '15%';
chartData.forEach(function (dataItem) {
let lineData = [];
maxStep = Math.max(dataItem.step, maxStep);
minStep = Math.min(dataItem.step, minStep);
dataItem.items.forEach(([time, step, x, y]) => {
minZ = Math.min(minZ, y);
maxZ = Math.max(maxZ, y);
lineData.push(x, step, y);
});
rawData.push(lineData);
});
let option = {
title,
color: ['#006069'],
visualMap: {
type: 'continuous',
show: false,
min: minStep,
max: maxStep,
dimension: 1,
inRange: {
colorLightness: [0.2, 0.4]
}
},
xAxis: {
min: minX,
max: maxX,
axisLine: {
onZero: false
},
axisLabel: {
formatter: function (value) {
return Math.round(value * 100) / 100;
}
},
splitLine: {
show: false
}
},
yAxis: {
position: 'right',
axisLine: {
onZero: false
},
inverse: true,
splitLine: {
show: false
}
},
grid,
series: [{
type: 'custom',
dimensions: ['x', 'y'],
renderItem: function (params, api) {
let points = makePolyPoints(
params.dataIndex,
api.value,
api.coord,
params.coordSys.y - 10
);
return {
type: 'polygon',
silent: true,
shape: {
points
},
style: api.style({
stroke: '#bbb',
lineWidth: 1
})
};
},
data: rawData
}]
};
function makePolyPoints(dataIndex, getValue, getCoord, yValueMapHeight) {
let points = [];
for (let i = 0; i < rawData[dataIndex].length;) {
let x = getValue(i++);
let y = getValue(i++);
let z = getValue(i++);
points.push(getPoint(x, y, z, getCoord, yValueMapHeight));
}
return points;
}
function getPoint(x, y, z, getCoord, yValueMapHeight) {
let pt = getCoord([x, y]);
// linear map in z axis
pt[1] -= (z - minZ) / (maxZ - minZ) * yValueMapHeight;
return pt;
}
let zr = ecChart.getZr();
function removeTooltip() {
if (zrDrawElement.hoverLine) {
zr.remove(zrDrawElement.hoverLine);
zr.remove(zrDrawElement.tooltip);
zrDrawElement.hoverDots.forEach(dot => zr.remove(dot));
zrDrawElement.hoverDots.length = 0;
zr.remove(zrDrawElement.tooltipX);
zr.remove(zrDrawElement.tooltipY);
}
}
zr.on('mouseout', e => {
removeTooltip();
});
zr.on('mousemove', e => {
removeTooltip();
let nearestIndex = findNearestValue(e.offsetX, e.offsetY);
if (nearestIndex) {
let getCoord = function (pt) {
return ecChart.convertToPixel('grid', pt);
};
let gridRect = ecChart.getModel().getComponent('grid', 0).coordinateSystem.getRect();
let linePoints = makePolyPoints(
nearestIndex.itemIndex,
function (i) {
return rawData[nearestIndex.itemIndex][i];
},
getCoord,
gridRect.y - 10
);
zr.add(zrDrawElement.hoverLine = new echarts.graphic.Polyline({
silent: true,
shape: {
points: linePoints
},
style: {
stroke: '#5c5c5c',
lineWidth: 2
},
z: 999
}));
let itemX;
rawData.forEach(dataItem => {
let binIndex = nearestIndex.binIndex;
let x = dataItem[binIndex * 3];
let y = dataItem[binIndex * 3 + 1];
let z = dataItem[binIndex * 3 + 2];
let pt = getPoint(x, y, z, getCoord, gridRect.y - 10);
itemX = pt[0];
let dot = new echarts.graphic.Circle({
shape: {
cx: pt[0],
cy: pt[1],
r: 3
},
style: {
fill: '#000',
stroke: '#ccc',
lineWidth: 1
},
z: 1000
});
zr.add(dot);
zrDrawElement.hoverDots.push(dot);
});
let hoveredItem = chartData[nearestIndex.itemIndex];
zrDrawElement.tooltip = new echarts.graphic.Text({
position: [e.offsetX + 30, e.offsetY - 50],
style: {
text: yValueFormat(hoveredItem.items[nearestIndex.binIndex][3]),
textFill: '#000',
fontSize: 14,
textBackgroundColor: '#eee',
textBorderColor: '#008c99',
textBorderWidth: 2,
textBorderRadius: 5,
textPadding: 10,
rich: {}
},
z: 2000
});
zr.add(zrDrawElement.tooltip);
zrDrawElement.tooltipX = new echarts.graphic.Text({
position: [
itemX,
gridRect.y + gridRect.height
],
style: {
text: Math.round(hoveredItem.items[nearestIndex.binIndex][2] * 1000) / 1000,
textFill: '#fff',
textAlign: 'center',
fontSize: 12,
textBackgroundColor: '#333',
textBorderWidth: 2,
textPadding: [5, 7],
rich: {}
},
z: 2000
});
zr.add(zrDrawElement.tooltipX);
zrDrawElement.tooltipY = new echarts.graphic.Text({
position: [
gridRect.x + gridRect.width,
linePoints[linePoints.length - 1][1]
],
style: {
text: hoveredItem.step,
textFill: '#fff',
textVerticalAlign: 'middle',
fontSize: 12,
textBackgroundColor: '#333',
textBorderWidth: 2,
textPadding: [5, 7],
rich: {}
},
z: 2000
});
zr.add(zrDrawElement.tooltipY);
}
});
function findNearestValue(px, py) {
let value = ecChart.convertFromPixel('grid', [px, py]);
let itemIndex;
let nearestY = Infinity;
let binIndex;
chartData.forEach((item, index) => {
let dist = Math.abs(value[1] - item.step);
if (dist < nearestY) {
nearestY = dist;
itemIndex = index;
}
});
if (itemIndex != null) {
let dataItem = chartData[itemIndex];
let nearestX = Infinity;
dataItem.items.forEach((item, index) => {
let dist = Math.abs(item[2] - value[0]);
if (dist < nearestX) {
nearestX = dist;
binIndex = index;
}
});
if (binIndex != null) {
return {
itemIndex: itemIndex,
binIndex: binIndex
};
}
}
}
ecChart.setOption(option, {notMerge: true});
},
// get origin data per 60 seconds
startInterval() {
this.getOringDataInterval = setInterval(() => {
let tagInfo = this.data.get('tagInfo');
this.getOriginChartData(tagInfo);
}, intervalTime * 1000);
},
stopInterval() {
clearInterval(this.getOringDataInterval);
},
getOriginChartData({run, tag}) {
let params = {
run,
tag: tag.displayName
};
getPluginHistogramsHistograms(params).then(({status, data}) => {
if (status === 0) {
this.data.set('originData', data);
}
});
},
expandArea() {
let isExpand = this.data.get('isExpand');
let pageBoxWidth = document.getElementsByClassName('visual-dl-chart-page')[0].offsetWidth;
if (!isExpand) {
let el = this.el.getElementsByClassName('visual-dl-chart-box')[0];
el.style.width = pageBoxWidth + 'px';
el.style.height = '600px';
this.data.set('isExpand', true);
this.myChart.resize({
width: pageBoxWidth,
height: 600
});
}
else {
let el = this.el.getElementsByClassName('visual-dl-chart-box')[0];
el.style.width = '400px';
el.style.height = '300px';
this.data.set('isExpand', false);
this.myChart.resize({
width: 400,
height: 300
});
}
}
};
</script>
<style lang="stylus">
.visual-dl-histogram-charts
float left
margin-bottom 20px
margin 20px 30px 10px 0
background #fff
padding 10px
.visual-dl-chart-box
width 400px
height 300px
.visual-dl-chart-actions
height 50px
margin-left 10%
.sm-form-item
float left
width 100px
margin-top 0px
display block
.sm-button
float left
display block
height 20px
line-height 20px
margin-top 10px
padding 0 10px
</style>
<template>
<!-- ClassName visual-dl-chart-page used in chart.san, change they all if you need!-->
<div class="visual-dl-chart-page">
<ui-expand-panel isShow="{{expand}}" info="{{total}}" title="{{title}}">
<ui-chart
s-for="tag in filteredPageList"
tagInfo="{{tag}}"
runs="{{config.runs}}"
chartType="{{config.chartType}}"
running="{{config.running}}"
runsItems="{{runsItems}}"
></ui-chart>
<ui-pagination
s-if="total > pageSize"
on-pageChange="handlePageChange($event)"
current="{{currentPage}}"
pageSize="{{pageSize}}"
total="{{total}}"
showSizeChanger="{{false}}"
/>
</ui-expand-panel>
</div>
</template>
<script>
import ExpandPanel from '../../common/component/ExpandPanel';
import Chart from './chart';
import Pagination from 'san-mui/Pagination';
import {cloneDeep, flatten} from 'lodash';
export default {
components: {
'ui-chart': Chart,
'ui-expand-panel': ExpandPanel,
'ui-pagination': Pagination
},
computed: {
filteredRunsList() {
let tagList = this.data.get('tagList') || [];
let runs = this.data.get('config.runs') || [];
let list = cloneDeep(tagList);
return flatten(list.slice().map(item => {
return item.tagList.filter(one => runs.includes(one.run));
}));
},
filteredPageList() {
let list = this.data.get('filteredRunsList');
let currentPage = this.data.get('currentPage');
let pageSize = this.data.get('pageSize');
return list.slice((currentPage - 1) * pageSize, currentPage * pageSize);
},
total() {
let list = this.data.get('filteredRunsList') || [];
return list.length;
}
},
initData() {
return {
// current page
currentPage: 1,
// item per page
pageSize: 4
};
},
handlePageChange({pageNum}) {
this.data.set('currentPage', pageNum);
}
};
</script>
<style lang="stylus">
@import '../../style/variables';
+prefix-classes('visual-dl-')
.chart-page
.chart-box
float left
.chart-page-box:after
content: "";
clear: both;
display: block;
</style>
<template>
<div class="visual-dl-histogram-config-com">
<san-text-field
hintText="input a tag group name to search"
label="Group name RegExp"
inputValue="{=config.groupNameReg=}"
/>
<ui-radio-group
label="Histogram mode"
value="{=config.chartType=}"
items="{{charTypeItems}}"
/>
<ui-checkbox-group
value="{=config.runs=}"
label="Runs"
items="{{runsItems}}"
/>
<san-button
class="visual-dl-histogram-run-toggle"
variants="raised {{config.running ? 'secondery' : 'primary'}}"
on-click="toggleAllRuns"
>
{{config.running ? 'Running' : 'Stopped'}}
</san-button>
</div>
</template>
<script>
import TextField from 'san-mui/TextField';
import Slider from '../../common/component/Slider';
import RadioGroup from '../../common/component/RadioGroup';
import DropDownMenu from '../../common/component/DropDownMenu';
import CheckBoxGroup from '../../common/component/CheckBoxGroup';
import Button from 'san-mui/Button';
export default {
components: {
'san-text-field': TextField,
'ui-slider': Slider,
'ui-radio-group': RadioGroup,
'ui-dropdown-menu': DropDownMenu,
'ui-checkbox-group': CheckBoxGroup,
'san-button': Button
},
initData() {
return {
config: {
groupNameReg: '.*',
smoothing: '0.6',
horizontal: 'step',
sortingMethod: 'default',
downloadLink: [],
outlier: [],
running: true
},
horizontalItems: [
{
name: 'Step',
value: 'step'
},
{
name: 'Relative',
value: 'relative'
},
{
name: 'Wall',
value: 'wall'
}
],
runsItems: [],
charTypeItems: [
{
name: 'Overlay',
value: 'overlay'
},
{
name: 'Offset',
value: 'offset'
}
]
};
},
toggleAllRuns() {
let running = this.data.get('config.running');
this.data.set('config.running', !running);
this.fire('runningChange', running);
}
};
</script>
<style lang="stylus">
@import '../../style/variables';
+prefix-classes('visual-dl-histogram-')
.config-com
width 90%
margin 20px auto
.run-toggle
width 100%
margin-top 20px
</style>
<template>
<article>
<h1>
{{text}}
</h1>
</article>
</template>
<script>
export default {
initData() {
return {
text: 'welcome'
};
}
};
</script>
import {router} from 'san-router';
import HomePage from './Home';
router.add({
target: '#content',
rule: '/welcome',
Component: HomePage
});
<template>
<div class="visual-dl-page-container">
<div class="visual-dl-page-left">
<ui-chart-page
expand="{{true}}"
config="{{filteredConfig}}"
runsItems="{{runsItems}}"
tagList="{{filteredTagsList}}"
title="Tags matching {{config.groupNameReg}}"
></ui-chart-page>
<ui-chart-page
s-for="item in groupedTags"
config="{{filteredConfig}}"
runsItems="{{runsItems}}"
tagList="{{item.tags}}"
title="{{item.group}}"
></ui-chart-page>
</div>
<div class="visual-dl-page-right">
<div class="visual-dl-page-config-container">
<ui-config
runsItems="{{runsItems}}"
config="{=config=}"
></ui-config>
</div>
</div>
</div>
</template>
<script>
import {getPluginImagesTags, getRuns} from '../service';
import config from './ui/config';
import chartPage from './ui/chartPage';
import {debounce, flatten, uniq, isArray} from 'lodash';
import autoAdjustHeight from '../common/util/autoAdjustHeight';
export default {
components: {
'ui-config': config,
'ui-chart-page': chartPage
},
computed: {
runsItems() {
let runsArray = this.data.get('runsArray') || [];
return runsArray.map(item => {
return {
name: item,
value: item
};
});
},
tagsList() {
let tags = this.data.get('tags');
let runs = Object.keys(tags);
let tagsArray = runs.map(run => Object.keys(tags[run]));
let allUniqTags = uniq(flatten(tagsArray));
// get the data for every chart
return allUniqTags.map(tag => {
let tagList = runs.map(run => {
return {
run,
tag: tags[run][tag]
};
}).filter(item => item.tag !== undefined);
return {
tagList,
tag,
group: tag.split('/')[0]
};
});
},
groupedTags() {
let tagsList = this.data.get('tagsList') || [];
// put data in group
let groupData = {};
tagsList.forEach(item => {
let group = item.group;
if (groupData[group] === undefined) {
groupData[group] = [];
groupData[group].push(item);
}
else {
groupData[group].push(item);
}
});
// to array
let groups = Object.keys(groupData);
return groups.map(group => {
return {
group,
tags: groupData[group]
};
});
},
filteredConfig() {
let tansformArr = ['isActualImageSize'];
let config = this.data.get('config') || {};
let filteredConfig = {};
Object.keys(config).forEach(key => {
let val = config[key];
if (tansformArr.indexOf(key) > -1) {
filteredConfig[key] = isArray(val) && val[0] === 'yes';
}
else {
filteredConfig[key] = val;
}
});
return filteredConfig;
}
},
initData() {
return {
runsArray: [],
tags: [],
config: {
groupNameReg: '.*',
isActualImageSize: [],
runs: [],
running: true
}
};
},
inited() {
getPluginImagesTags().then(({errno, data}) => {
this.data.set('tags', data);
// filter when inited
let groupNameReg = this.data.get('config.groupNameReg');
this.filterTagsList(groupNameReg);
});
getRuns().then(({errno, data}) => {
this.data.set('runsArray', data);
this.data.set('config.runs', data);
});
// need debounce, can't use computed
this.watch('config.groupNameReg', debounce(this.filterTagsList, 300));
},
attached() {
autoAdjustHeight();
},
filterTagsList(groupNameReg) {
if (!groupNameReg) {
this.data.set('filteredTagsList', []);
return;
}
let tagsList = this.data.get('tagsList') || [];
let regExp = new RegExp(groupNameReg);
let filtedTagsList = tagsList.filter(item => regExp.test(item.tag));
this.data.set('filteredTagsList', filtedTagsList);
}
};
</script>
<style lang="stylus">
</style>
import {router} from 'san-router';
import Images from './Images';
router.add({
target: '#content',
rule: '/images',
Component: Images,
});
<template>
<div class="visual-dl-chart-page">
<ui-expand-panel isShow="{{expand}}" info="{{total}}" title="{{title}}">
<ui-image
class="visual-dl-chart-image"
s-for="tag in filteredPageList"
tagInfo="{{tag}}"
isActualImageSize="{{config.isActualImageSize}}"
runs="{{config.runs}}"
running="{{config.running}}"
runsItems="{{runsItems}}"
></ui-image>
<ui-pagination
class="visual-dl-sm-pagination"
s-if="total > pageSize"
on-pageChange="handlePageChange($event)"
current="{{currentPage}}"
pageSize="{{pageSize}}"
total="{{total}}"
showSizeChanger="{{false}}"
/>
</ui-expand-panel>
</div>
</template>
<script>
import ExpandPanel from '../../common/component/ExpandPanel';
import image from './image';
import Pagination from 'san-mui/Pagination';
import {cloneDeep, flatten} from 'lodash';
export default {
components: {
'ui-image': image,
'ui-expand-panel': ExpandPanel,
'ui-pagination': Pagination
},
computed: {
filteredRunsList() {
let tagList = this.data.get('tagList') || [];
let runs = this.data.get('config.runs') || [];
let list = cloneDeep(tagList);
return flatten(list.slice().map(item => {
return item.tagList.filter(one => runs.includes(one.run));
}));
},
filteredPageList() {
let list = this.data.get('filteredRunsList');
let currentPage = this.data.get('currentPage');
let pageSize = this.data.get('pageSize');
return list.slice((currentPage - 1) * pageSize, currentPage * pageSize);
},
total() {
let list = this.data.get('filteredRunsList') || [];
return list.length;
}
},
initData() {
return {
// current page
currentPage: 1,
// item per page
pageSize: 8
};
},
handlePageChange({pageNum}) {
this.data.set('currentPage', pageNum);
}
};
</script>
<style lang="stylus">
@import '../../style/variables';
+prefix-classes('visual-dl-')
.chart-page
.image-chart-box
overflow hidden
float left
.visual-dl-chart-image
float left
.image-chart-box:after
content ""
clear both
display block
.sm-pagination
height 50px
float left
width 100%
</style>
<template>
<div class="visual-dl-image-config-com">
<san-text-field
hintText="input a tag group name to search"
label="Group name RegExp"
inputValue="{=config.groupNameReg=}"
/>
<ui-checkbox-group
value="{=config.isActualImageSize=}"
items="{{imageSizeItems}}"
/>
<ui-checkbox-group
value="{=config.runs=}"
label="Runs"
items="{{runsItems}}"
/>
<san-button
class="visual-dl-image-run-toggle"
variants="raised {{config.running ? 'secondery' : 'primary'}}"
on-click="toggleAllRuns"
>
{{config.running ? 'Running' : 'Stopped'}}
</san-button>
</div>
</template>
<script>
import TextField from 'san-mui/TextField';
import CheckBoxGroup from '../../common/component/CheckBoxGroup';
import Button from 'san-mui/Button';
export default {
components: {
'san-text-field': TextField,
'ui-checkbox-group': CheckBoxGroup,
'san-button': Button
},
initData() {
return {
config: {
groupName: 'aa',
isActualImageSize: [],
runs: []
},
runsItems: [],
imageSizeItems: [
{
value: 'yes',
name: 'Show actual image size'
}
]
};
},
toggleAllRuns() {
let running = this.data.get('config.running');
this.data.set('config.running', !running);
this.fire('runningChange', running);
}
};
</script>
<style lang="stylus">
@import '../../style/variables';
+prefix-classes('visual-dl-image-')
.config-com
width 90%
margin 20px auto
.run-toggle
width 100%
margin-top 20px
</style>
<template>
<div class="visual-dl-image">
<h3 class="visual-dl-image-title">{{tagInfo.tag.displayName}}
<span class="visual-dl-image-run-icon">{{tagInfo.run}}</span>
</h3>
<p>
<span>Step:</span>
<span>{{imgData.step}};</span>
<span>{{imgData.wall_time | formatTime}}</span>
</p>
<san-slider
on-change="handleSlideChange($event)"
value="{{currentIndex}}"
min="{{slider.min}}"
max="{{steps}}"
step="{{1}}"
/>
<img style="{{computedImgStyle}}" src="{{imgData.imgSrc}}" />
</div>
</template>
<script>
import Slider from 'san-mui/Slider';
import {getPluginImagesImages} from '../../service';
const defaultImgWidth = 400;
const defaultImgHight = 300;
// the time to refresh chart data
const intervalTime = 30;
export default {
components: {
'san-slider': Slider
},
computed: {
steps() {
let data = this.data.get('data') || [];
return data.length - 1;
},
computedImgStyle() {
let isActualImageSize = this.data.get('isActualImageSize');
let width;
let height;
if (isActualImageSize) {
width = this.data.get('imgData.width');
height = this.data.get('imgData.height');
}
else {
width = defaultImgWidth;
height = defaultImgHight;
}
return 'width:' + width + 'px;height:' + height + 'px';
}
},
filters: {
formatTime(value) {
if (!value) {
return;
}
let time = new Date();
time.setTime(value.toString().split('.')[0]);
return time;
}
},
initData() {
return {
currentIndex: 0,
slider: {
value: '0',
label: '',
min: 0,
step: 1
}
};
},
inited() {
this.getOriginChartsData();
// currentIndex change event
this.watch('currentIndex', index => {
/* eslint-disable fecs-camelcase */
let currentImgInfo = this.data.get('data') ? this.data.get('data')[index] : {};
let {height, width, query, step, wall_time} = currentImgInfo;
let url = '/data/plugin/images/individualImage?ts=' + wall_time;
let imgSrc = [url, query].join('&');
this.data.set('imgData', {
imgSrc,
height,
width,
step,
wall_time
});
/* eslint-enable fecs-camelcase */
});
},
attached() {
if (this.data.get('running')) {
this.startInterval();
}
this.watch('running', running => {
running ? this.startInterval() : this.stopInterval();
});
},
detached() {
this.stopInterval();
},
stopInterval() {
clearInterval(this.getOringDataInterval);
},
// get origin data per {{intervalTime}} seconds
startInterval() {
this.getOringDataInterval = setInterval(() => {
this.getOriginChartsData();
}, intervalTime * 1000);
},
handleSlideChange(val) {
this.data.set('currentIndex', val);
},
getOriginChartsData() {
let {run, tag} = this.data.get('tagInfo');
let {displayName, samples} = tag;
let params = {
run,
tag: displayName,
samples
};
getPluginImagesImages(params).then(({status, data}) => {
if (status === 0) {
this.data.set('data', data);
this.data.set('currentIndex', data.length - 1);
}
});
}
};
</script>
<style lang="stylus">
.visual-dl-image
font-size 12px
width 400px
float left
margin 20px 30px 10px 0
background #fff
padding 10px
.visual-dl-image-title
font-size 14px
line-height 30px
.visual-dl-image-run-icon
background #e4e4e4
float right
margin-right 10px
padding 0 10px
border solid 1px #e4e4e4
border-radius 6px
line-height 20px
margin-top 4px
.visual-dl-chart-actions
.sm-form-item
width 300px
display inline-block
</style>
<template>
<div class="visual-dl-page-container">
<div class="visual-dl-page-left">
<ui-chart-page
expand="{{true}}"
config="{{filteredConfig}}"
runsItems="{{runsItems}}"
tagList="{{filteredTagsList}}"
title="Tags matching {{config.groupNameReg}}"
></ui-chart-page>
<ui-chart-page
s-for="item in groupedTags"
config="{{filteredConfig}}"
runsItems="{{runsItems}}"
tagList="{{item.tags}}"
title="{{item.group}}"
></ui-chart-page>
</div>
<div class="visual-dl-page-right">
<div class="visual-dl-page-config-container">
<ui-config
runsItems="{{runsItems}}"
config="{=config=}"
></ui-config>
</div>
</div>
</div>
</template>
<script>
import {getPluginScalarsTags, getRuns} from '../service';
import config from './ui/config';
import chartPage from './ui/chartPage';
import {debounce, flatten, uniq, isArray} from 'lodash';
import autoAdjustHeight from '../common/util/autoAdjustHeight';
export default {
components: {
'ui-config': config,
'ui-chart-page': chartPage
},
computed: {
runsItems() {
let runsArray = this.data.get('runsArray') || [];
return runsArray.map(item => {
return {
name: item,
value: item
};
});
},
tagsList() {
let tags = this.data.get('tags');
let runs = Object.keys(tags);
let tagsArray = runs.map(run => Object.keys(tags[run]));
let allUniqTags = uniq(flatten(tagsArray));
// get the data for every chart
return allUniqTags.map(tag => {
let tagList = runs.map(run => {
return {
run,
tag: tags[run][tag]
};
}).filter(item => item.tag !== undefined);
return {
tagList,
tag,
group: tag.split('/')[0]
};
});
},
groupedTags() {
let tagsList = this.data.get('tagsList') || [];
// put data in group
let groupData = {};
tagsList.forEach(item => {
let group = item.group;
if (groupData[group] === undefined) {
groupData[group] = [];
groupData[group].push(item);
}
else {
groupData[group].push(item);
}
});
// to array
let groups = Object.keys(groupData);
return groups.map(group => {
return {
group,
tags: groupData[group]
};
});
},
filteredConfig() {
let tansformArr = ['downloadLink', 'outlier'];
let config = this.data.get('config') || {};
let filteredConfig = {};
Object.keys(config).forEach(key => {
let val = config[key];
if (tansformArr.indexOf(key) > -1) {
filteredConfig[key] = isArray(val) && val[0] === 'yes';
}
else {
filteredConfig[key] = val;
}
});
return filteredConfig;
}
},
initData() {
return {
runsArray: [],
tags: [],
config: {
groupNameReg: '.*',
smoothing: 0.6,
horizontal: 'step',
sortingMethod: 'default',
downloadLink: [],
outlier: [],
runs: [],
running: true
}
};
},
inited() {
getPluginScalarsTags().then(({errno, data}) => {
this.data.set('tags', data);
// filter when inited
let groupNameReg = this.data.get('config.groupNameReg');
this.filterTagsList(groupNameReg);
});
getRuns().then(({errno, data}) => {
this.data.set('runsArray', data);
this.data.set('config.runs', data);
});
// need debounce, can't use computed
this.watch('config.groupNameReg', debounce(this.filterTagsList, 300));
},
attached() {
autoAdjustHeight();
},
filterTagsList(groupNameReg) {
if (!groupNameReg) {
this.data.set('filteredTagsList', []);
return;
}
let tagsList = this.data.get('tagsList') || [];
let regExp = new RegExp(groupNameReg);
let filtedTagsList = tagsList.filter(item => regExp.test(item.tag));
this.data.set('filteredTagsList', filtedTagsList);
}
};
</script>
<style lang="stylus">
</style>
import {router} from 'san-router';
import Scalar from './Scalars';
router.add({
target: '#content',
rule: '/',
Component: Scalar,
});
router.add({
target: '#content',
rule: '/scalars',
Component: Scalar,
});
此差异已折叠。
<template>
<div class="visual-dl-chart-page">
<ui-expand-panel isShow="{{expand}}" info="{{tagList.length}}" title="{{title}}">
<div class="visual-dl-chart-page-box">
<ui-chart
s-for="tag in filteredTagList"
tagInfo="{{tag}}"
groupNameReg="{{config.groupNameReg}}"
smoothing="{{config.smoothing}}"
horizontal="{{config.horizontal}}"
sortingMethod="{{config.sortingMethod}}"
downloadLink="{{config.downloadLink}}"
outlier="{{config.outlier}}"
runs="{{config.runs}}"
running="{{config.running}}"
runsItems="{{runsItems}}"
></ui-chart>
</div>
<ui-pagination
s-if="total > pageSize"
on-pageChange="handlePageChange($event)"
current="{{currentPage}}"
pageSize="{{pageSize}}"
total="{{total}}"
showSizeChanger="{{false}}"
/>
</ui-expand-panel>
</div>
</template>
<script>
import ExpandPanel from '../../common/component/ExpandPanel';
import chart from './chart';
import Pagination from 'san-mui/Pagination';
import {cloneDeep} from 'lodash';
export default {
components: {
'ui-chart': chart,
'ui-expand-panel': ExpandPanel,
'ui-pagination': Pagination
},
computed: {
filteredRunsList() {
let tagList = this.data.get('tagList') || [];
let runs = this.data.get('config.runs') || [];
let list = cloneDeep(tagList);
return list.slice().map(item => {
item.tagList = item.tagList.filter(one => runs.includes(one.run));
return item;
});
},
filteredTagList() {
let tagList = this.data.get('filteredRunsList') || [];
let currentPage = this.data.get('currentPage');
let pageSize = this.data.get('pageSize');
return tagList.slice((currentPage - 1) * pageSize, currentPage * pageSize);
},
total() {
let tagList = this.data.get('tagList') || [];
return tagList.length;
}
},
initData() {
return {
// current page
currentPage: 1,
// item per page
pageSize: 8
};
},
handlePageChange({pageNum}) {
this.data.set('currentPage', pageNum);
}
};
</script>
<style lang="stylus">
@import '../../style/variables';
+prefix-classes('visual-dl-')
.chart-page
.chart-page-box:after
content: "";
clear: both;
display: block;
</style>
此差异已折叠。
import {useRef, useEffect, useCallback} from 'react';
const useClickOutside = (callback: () => void) => {
const ref = useRef(null);
const escapeListener = useCallback(
(e: KeyboardEvent) => {
if (e.key === 'Escape') {
callback();
}
},
[callback]
);
const clickListener = useCallback(
(e: MouseEvent | TouchEvent) => {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
if (ref.current && !(ref.current! as Node).contains(e.target as Node)) {
callback();
}
},
[callback]
);
useEffect(() => {
if (process.browser) {
document.addEventListener('mousedown', clickListener);
document.addEventListener('touchstart', clickListener);
document.addEventListener('keyup', escapeListener);
return () => {
document.removeEventListener('mousedown', clickListener);
document.removeEventListener('touchstart', clickListener);
document.removeEventListener('keyup', escapeListener);
};
}
}, [clickListener, escapeListener]);
return ref;
};
export default useClickOutside;
此差异已折叠。
此差异已折叠。
module.exports = {
'**/*.ts?(x)': () => ['tsc -p tsconfig.json --noEmit', 'eslint'],
'**/*.js?(x)': () => 'eslint'
};
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
export default ['train', 'test'];
此差异已折叠。
export default {
test: ['layer2/biases/summaries/mean'],
train: ['layer2/biases/summaries/mean', 'layer2/biases/summaries/accuracy', 'layer2/biases/summaries/cost']
};
/// <reference types="next" />
/// <reference types="next/types/global" />
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册