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

Hyper parameters (#960)

* feat: hyper-parameters

* feat: hyper-parameters

* feat: hyper-parameters

* feat: hyper-parameters

* feat: hyper-parameters

* feat: hyper-parameters

* feat: hyper-parameters

* feat: hyper-parameters

* feat: hyper-parameters
上级 e2d15e97
...@@ -71,8 +71,13 @@ const middleware = () => { ...@@ -71,8 +71,13 @@ const middleware = () => {
return async (req, res) => { return async (req, res) => {
const file = path.join(root, req.path.replace(/\.js$/, '.svg')); const file = path.join(root, req.path.replace(/\.js$/, '.svg'));
if ((await fs.stat(file)).isFile()) { if ((await fs.stat(file)).isFile()) {
if (req.path.endsWith('.js')) {
res.type('js'); res.type('js');
res.send(await transform(file, false)); res.send(await transform(file, false));
} else {
res.type(req.path.split('.').pop());
res.send(await fs.readFile(file));
}
} }
}; };
}; };
......
...@@ -38,6 +38,7 @@ ...@@ -38,6 +38,7 @@
"@visualdl/netron": "2.1.5", "@visualdl/netron": "2.1.5",
"@visualdl/wasm": "2.1.5", "@visualdl/wasm": "2.1.5",
"bignumber.js": "9.0.1", "bignumber.js": "9.0.1",
"classnames": "2.3.1",
"d3": "6.6.2", "d3": "6.6.2",
"d3-format": "2.0.0", "d3-format": "2.0.0",
"echarts": "4.9.0", "echarts": "4.9.0",
...@@ -57,6 +58,8 @@ ...@@ -57,6 +58,8 @@
"query-string": "7.0.0", "query-string": "7.0.0",
"react": "17.0.2", "react": "17.0.2",
"react-content-loader": "6.0.3", "react-content-loader": "6.0.3",
"react-dnd": "14.0.2",
"react-dnd-html5-backend": "14.0.0",
"react-dom": "17.0.2", "react-dom": "17.0.2",
"react-helmet": "6.1.0", "react-helmet": "6.1.0",
"react-i18next": "11.8.12", "react-i18next": "11.8.12",
...@@ -66,6 +69,8 @@ ...@@ -66,6 +69,8 @@
"react-redux": "7.2.3", "react-redux": "7.2.3",
"react-router-dom": "5.2.0", "react-router-dom": "5.2.0",
"react-spinners": "0.10.6", "react-spinners": "0.10.6",
"react-table": "7.6.3",
"react-table-sticky": "1.1.3",
"react-toastify": "7.0.3", "react-toastify": "7.0.3",
"redux": "4.0.5", "redux": "4.0.5",
"styled-components": "5.2.3", "styled-components": "5.2.3",
...@@ -104,6 +109,7 @@ ...@@ -104,6 +109,7 @@
"@types/react-rangeslider": "2.2.3", "@types/react-rangeslider": "2.2.3",
"@types/react-redux": "7.1.16", "@types/react-redux": "7.1.16",
"@types/react-router-dom": "5.1.7", "@types/react-router-dom": "5.1.7",
"@types/react-table": "7.0.29",
"@types/snowpack-env": "2.3.3", "@types/snowpack-env": "2.3.3",
"@types/styled-components": "5.1.9", "@types/styled-components": "5.1.9",
"@types/three": "0.127.0", "@types/three": "0.127.0",
......
<svg height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> <svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
<path d="m17.8336309 5.10570888 1.0606602 1.06066018-5.8345822 5.83333984 5.8345822 5.833922-1.0606602 1.0606602-5.833922-5.8345822-5.83333984 5.8345822-1.06066018-1.0606602 5.83300002-5.833922-5.83300002-5.83333984 1.06066018-1.06066018 5.83333984 5.83300002z" fill-rule="nonzero" /> <path d="M32 3.2l-3.2-3.2-12.8 12.8-12.8-12.8-3.2 3.2 12.8 12.8-12.8 12.8 3.2 3.2 12.8-12.8 12.8 12.8 3.2-3.2-12.8-12.8z"></path>
</svg> </svg>
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
<path d="M10 6h4v4h-4v-4z"></path>
<path d="M18 6h4v4h-4v-4z"></path>
<path d="M10 14h4v4h-4v-4z"></path>
<path d="M18 14h4v4h-4v-4z"></path>
<path d="M10 22h4v4h-4v-4z"></path>
<path d="M18 22h4v4h-4v-4z"></path>
</svg>
...@@ -4,6 +4,9 @@ ...@@ -4,6 +4,9 @@
"close": "Close", "close": "Close",
"colon": ": ", "colon": ": ",
"confirm": "Confirm", "confirm": "Confirm",
"download-data": "Download data",
"download-data-format": "In {{format}}",
"download-image": "Download image",
"empty": "Nothing to display", "empty": "Nothing to display",
"error": "Error occurred", "error": "Error occurred",
"graph": "Graphs", "graph": "Graphs",
...@@ -13,16 +16,20 @@ ...@@ -13,16 +16,20 @@
"image": "Image", "image": "Image",
"inactive": "Inactive", "inactive": "Inactive",
"loading": "Please wait while loading data", "loading": "Please wait while loading data",
"maximize": "Maximize",
"minimize": "Minimize",
"more": "More", "more": "More",
"next-page": "Next Page", "next-page": "Next Page",
"pr-curve": "PR Curve", "pr-curve": "PR Curve",
"previous-page": "Prev Page", "previous-page": "Prev Page",
"restore": "Selection restore",
"roc-curve": "ROC Curve", "roc-curve": "ROC Curve",
"run": "Run", "run": "Run",
"running": "Running", "running": "Running",
"runs": "Runs", "runs": "Runs",
"sample": "Samples", "sample": "Samples",
"scalar": "Scalars", "scalar": "Scalars",
"scalar-value": "Value",
"search": "Search", "search": "Search",
"search-empty": "Nothing found. Please try again with another word. <1/>Or you can <3>see all charts</3>.", "search-empty": "Nothing found. Please try again with another word. <1/>Or you can <3>see all charts</3>.",
"search-result": "Search Result", "search-result": "Search Result",
...@@ -46,6 +53,7 @@ ...@@ -46,6 +53,7 @@
"step": "Step", "step": "Step",
"wall": "Wall Time" "wall": "Wall Time"
}, },
"toggle-log-axis": "Logarithmic axis",
"total-page": "{{count}} page, jump to", "total-page": "{{count}} page, jump to",
"total-page_plural": "{{count}} pages, jump to", "total-page_plural": "{{count}} pages, jump to",
"unselected-empty": "Nothing selected. <1/>Please select display data from right side." "unselected-empty": "Nothing selected. <1/>Please select display data from right side."
......
{ {
"download-image": "Download image",
"false-negatives": "FN", "false-negatives": "FN",
"false-positives": "FP", "false-positives": "FP",
"maximize": "Maximize",
"minimize": "Minimize",
"precision": "Precision", "precision": "Precision",
"recall": "Recall", "recall": "Recall",
"restore": "Selection restore",
"threshold": "Threshold", "threshold": "Threshold",
"time-display-type": "Time Display Type", "time-display-type": "Time Display Type",
"true-negatives": "TN", "true-negatives": "TN",
......
{ {
"download-image": "Download image",
"maximize": "Maximize",
"minimize": "Minimize",
"mode": "Mode", "mode": "Mode",
"mode-value": { "mode-value": {
"offset": "Offset", "offset": "Offset",
......
{
"data-type-value": {
"hparams": "Hyper Parameters",
"metrics": "Metrics"
},
"hyper-parameter": {
"order-default": "Default"
},
"max": "Max",
"metric-graphs-empty": "<0>No metrics selected</0><1>Please select some metrics to view graphs here.</1>",
"min": "Min",
"negative-infinity": "-Infinity",
"order-by": "Sort by",
"order-direction": "Direction",
"order-direction-value": {
"asc": "Ascending",
"desc": "Descending"
},
"parameter-importance": "Hyper Parameters Importance",
"positive-infinity": "+Infinity",
"scale-method": {
"linear": "Linear",
"logarithmic": "Logarithmic",
"quantile": "Quantile"
},
"session-table-empty": "<0>Click or hover over a line to display its values here</0><1><0>Hover to display values;</0><0>Click to display metrics graph.</0><1>",
"show-parameter-importance": "Show HParams Importance",
"trial-id": "Trial ID",
"views": {
"parallel-coordinates": "Parallel Coordinates View",
"scatter-plot-matrix": "Scatter Plot Matrix View",
"table": "Table View"
}
}
...@@ -8,7 +8,6 @@ ...@@ -8,7 +8,6 @@
"sample": "Sample", "sample": "Sample",
"sample-rate": "Sample Rate", "sample-rate": "Sample Rate",
"show-actual-size": "Show Actual Image Size", "show-actual-size": "Show Actual Image Size",
"step": "Step",
"step-tip": "You can change step by pressing up & donw on your keyboard", "step-tip": "You can change step by pressing up & donw on your keyboard",
"text": "text" "text": "text"
} }
{ {
"download-data": "Download data",
"download-data-format": "In {{format}}",
"download-image": "Download image",
"ignore-outliers": "Ignore outliers in chart scaling", "ignore-outliers": "Ignore outliers in chart scaling",
"max": "Max.", "max": "Max.",
"maximize": "Maximize",
"min": "Min.", "min": "Min.",
"minimize": "Minimize",
"restore": "Selection restore",
"show-most-value": "Show global extrema", "show-most-value": "Show global extrema",
"smoothed": "Smoothed", "smoothed": "Smoothed",
"smoothed-data-only": "Smoothed Data Only", "smoothed-data-only": "Smoothed Data Only",
"smoothing": "Smoothing", "smoothing": "Smoothing",
"toggle-log-axis": "Logarithmic axis",
"tooltip-sorting": "Tooltip Sorting", "tooltip-sorting": "Tooltip Sorting",
"tooltip-sorting-value": { "tooltip-sorting-value": {
"ascending": "Ascending", "ascending": "Ascending",
...@@ -20,6 +13,5 @@ ...@@ -20,6 +13,5 @@
"descending": "Descending", "descending": "Descending",
"nearest": "Nearest" "nearest": "Nearest"
}, },
"value": "Value",
"x-axis": "X-Axis" "x-axis": "X-Axis"
} }
...@@ -4,6 +4,9 @@ ...@@ -4,6 +4,9 @@
"close": "关闭", "close": "关闭",
"colon": ":", "colon": ":",
"confirm": "确定", "confirm": "确定",
"download-data": "下载数据",
"download-data-format": "{{format}} 格式",
"download-image": "下载图片",
"empty": "暂无数据", "empty": "暂无数据",
"error": "发生错误", "error": "发生错误",
"graph": "网络结构", "graph": "网络结构",
...@@ -13,16 +16,20 @@ ...@@ -13,16 +16,20 @@
"image": "图像", "image": "图像",
"inactive": "待使用", "inactive": "待使用",
"loading": "数据载入中,请稍等", "loading": "数据载入中,请稍等",
"maximize": "最大化",
"minimize": "最小化",
"more": "更多", "more": "更多",
"next-page": "下一页", "next-page": "下一页",
"pr-curve": "PR曲线", "pr-curve": "PR曲线",
"previous-page": "上一页", "previous-page": "上一页",
"restore": "还原图表框选",
"roc-curve": "ROC曲线", "roc-curve": "ROC曲线",
"run": "运行", "run": "运行",
"running": "运行中", "running": "运行中",
"runs": "数据流", "runs": "数据流",
"sample": "样本数据", "sample": "样本数据",
"scalar": "标量数据", "scalar": "标量数据",
"scalar-value": "Value",
"search": "搜索", "search": "搜索",
"search-empty": "没有找到您期望的内容,你可以尝试其他搜索词<1/>或者点击<3>查看全部图表</3>", "search-empty": "没有找到您期望的内容,你可以尝试其他搜索词<1/>或者点击<3>查看全部图表</3>",
"search-result": "搜索结果", "search-result": "搜索结果",
...@@ -46,6 +53,7 @@ ...@@ -46,6 +53,7 @@
"step": "Step", "step": "Step",
"wall": "Wall Time" "wall": "Wall Time"
}, },
"toggle-log-axis": "切换对数坐标轴",
"total-page": "共 {{count}} 页,跳转至", "total-page": "共 {{count}} 页,跳转至",
"total-page_plural": "共 {{count}} 页,跳转至", "total-page_plural": "共 {{count}} 页,跳转至",
"unselected-empty": "未选中任何数据<1/>请在右侧操作栏选择要展示的数据" "unselected-empty": "未选中任何数据<1/>请在右侧操作栏选择要展示的数据"
......
{ {
"download-image": "下载图片",
"false-negatives": "FN", "false-negatives": "FN",
"false-positives": "FP", "false-positives": "FP",
"maximize": "最大化",
"minimize": "最小化",
"precision": "Precision", "precision": "Precision",
"recall": "Recall", "recall": "Recall",
"restore": "还原图表框选",
"threshold": "Threshold", "threshold": "Threshold",
"time-display-type": "时间显示类型", "time-display-type": "时间显示类型",
"true-negatives": "TN", "true-negatives": "TN",
......
{ {
"download-image": "下载图片",
"maximize": "最大化",
"minimize": "最小化",
"mode": "直方图模式", "mode": "直方图模式",
"mode-value": { "mode-value": {
"offset": "Offset", "offset": "Offset",
......
{
"data-type-value": {
"hparams": "超参数",
"metrics": "度量指标"
},
"hyper-parameter": {
"order-default": "默认"
},
"max": "最大值",
"metric-graphs-empty": "<0>无度量指标启用</0><1>请启用一些指标以在此处查看内容</1>",
"min": "最小值",
"negative-infinity": "负无穷",
"order-by": "排序方式",
"order-direction": "排序方向",
"order-direction-value": {
"asc": "升序",
"desc": "降序"
},
"parameter-importance": "超参重要性排序",
"positive-infinity": "正无穷",
"scale-method": {
"linear": "Linear",
"logarithmic": "Logarithmic",
"quantile": "Quantile"
},
"session-table-empty": "<0>请悬停或点击某条折线查看数据</0><1><0>悬停查看该训练轮次的具体数据;</0><0>点击查看选中的训练轮次模型指标图。</0><1>",
"show-parameter-importance": "查看超参重要性排序",
"trial-id": "Trial ID",
"views": {
"parallel-coordinates": "平行坐标图",
"scatter-plot-matrix": "散点图",
"table": "表格视图"
}
}
...@@ -8,7 +8,6 @@ ...@@ -8,7 +8,6 @@
"sample": "样本", "sample": "样本",
"sample-rate": "采样率", "sample-rate": "采样率",
"show-actual-size": "按真实大小展示", "show-actual-size": "按真实大小展示",
"step": "Step",
"step-tip": "您还可以通过键盘 ↑ ↓ 键,快速调节step哦~", "step-tip": "您还可以通过键盘 ↑ ↓ 键,快速调节step哦~",
"text": "文本" "text": "文本"
} }
{ {
"download-data": "下载数据",
"download-data-format": "{{format}} 格式",
"download-image": "下载图片",
"ignore-outliers": "图表缩放时忽略极端值", "ignore-outliers": "图表缩放时忽略极端值",
"max": "最大值", "max": "最大值",
"maximize": "最大化",
"min": "最小值", "min": "最小值",
"minimize": "最小化",
"restore": "还原图表框选",
"show-most-value": "显示最值", "show-most-value": "显示最值",
"smoothed": "Smoothed", "smoothed": "Smoothed",
"smoothed-data-only": "仅显示平滑后数据", "smoothed-data-only": "仅显示平滑后数据",
"smoothing": "平滑度", "smoothing": "平滑度",
"toggle-log-axis": "切换对数坐标轴",
"tooltip-sorting": "详情数据排序", "tooltip-sorting": "详情数据排序",
"tooltip-sorting-value": { "tooltip-sorting-value": {
"ascending": "升序", "ascending": "升序",
...@@ -20,6 +13,5 @@ ...@@ -20,6 +13,5 @@
"descending": "降序", "descending": "降序",
"nearest": "最近" "nearest": "最近"
}, },
"value": "Value",
"x-axis": "X轴" "x-axis": "X轴"
} }
...@@ -28,6 +28,11 @@ export const AsideSection = styled.section` ...@@ -28,6 +28,11 @@ export const AsideSection = styled.section`
margin-bottom: 0; margin-bottom: 0;
${transitionProps('border-color')} ${transitionProps('border-color')}
} }
& > & {
margin-left: 0;
margin-right: 0;
}
`; `;
const Wrapper = styled.div.attrs<{width: string | number}>(({width}) => ({ const Wrapper = styled.div.attrs<{width: string | number}>(({width}) => ({
...@@ -59,6 +64,19 @@ const Wrapper = styled.div.attrs<{width: string | number}>(({width}) => ({ ...@@ -59,6 +64,19 @@ const Wrapper = styled.div.attrs<{width: string | number}>(({width}) => ({
flex: none; flex: none;
box-shadow: 0 -${rem(5)} ${rem(16)} 0 rgba(0, 0, 0, 0.03); box-shadow: 0 -${rem(5)} ${rem(16)} 0 rgba(0, 0, 0, 0.03);
padding: ${rem(20)}; padding: ${rem(20)};
> ${AsideSection} {
margin-left: 0;
margin-right: 0;
&:first-child {
margin-top: 0;
}
&:last-child {
margin-bottom: 0;
}
}
} }
> .aside-resize-bar-left, > .aside-resize-bar-left,
...@@ -68,7 +86,7 @@ const Wrapper = styled.div.attrs<{width: string | number}>(({width}) => ({ ...@@ -68,7 +86,7 @@ const Wrapper = styled.div.attrs<{width: string | number}>(({width}) => ({
height: 100%; height: 100%;
top: 0; top: 0;
cursor: col-resize; cursor: col-resize;
user-select: none; touch-action: none;
&.aside-resize-bar-left { &.aside-resize-bar-left {
left: 0; left: 0;
......
/**
* Copyright 2020 Baidu Inc. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import * as chart from '~/utils/chart';
import React, {useEffect, useImperativeHandle} from 'react';
import {WithStyled, primaryColor} from '~/utils/style';
import useECharts, {Options, Wrapper, useChartTheme} from '~/hooks/useECharts';
import type {EChartOption} from 'echarts';
import GridLoader from 'react-spinners/GridLoader';
import defaultsDeep from 'lodash/defaultsDeep';
type BarChartProps = {
options?: EChartOption;
title?: string;
direction?: 'vertical' | 'horizontal';
categories?: string[];
data?: Partial<NonNullable<EChartOption<EChartOption.SeriesBar>['series']>>;
loading?: boolean;
onInit?: Options['onInit'];
};
export type BarChartRef = {
saveAsImage(): void;
};
const BarChart = React.forwardRef<BarChartRef, BarChartProps & WithStyled>(
({options, categories, direction, data, title, loading, className, onInit}, ref) => {
const {ref: echartRef, echart, wrapper, saveAsImage} = useECharts<HTMLDivElement>({
loading: !!loading,
autoFit: true,
onInit
});
const theme = useChartTheme();
useImperativeHandle(ref, () => ({
saveAsImage: () => {
saveAsImage(title);
}
}));
useEffect(() => {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const {color, colorAlt, series, ...defaults} = chart;
const isHorizontal = direction === 'horizontal';
const valueAxis = {
type: 'value'
};
const categoryAxis = {
type: 'category',
data: categories,
axisLabel: {
formatter: null
}
};
const chartOptions: EChartOption = defaultsDeep(
{
title: {
text: title ?? ''
},
xAxis: isHorizontal ? valueAxis : categoryAxis,
yAxis: isHorizontal ? categoryAxis : valueAxis,
series: data?.map((item, index) =>
defaultsDeep(
item,
{
itemStyle: {
color: color[index % color.length]
}
},
series
)
)
},
options,
theme,
defaults
);
echart?.setOption(chartOptions, {notMerge: true});
}, [options, data, title, theme, echart, direction, categories]);
return (
<Wrapper ref={wrapper} className={className}>
{!echart && (
<div className="loading">
<GridLoader color={primaryColor} size="10px" />
</div>
)}
<div className="echarts" ref={echartRef}></div>
</Wrapper>
);
}
);
export default BarChart;
...@@ -21,6 +21,7 @@ import type {Icons} from '~/components/Icon'; ...@@ -21,6 +21,7 @@ import type {Icons} from '~/components/Icon';
import RawIcon from '~/components/Icon'; import RawIcon from '~/components/Icon';
import {colors} from '~/utils/theme'; import {colors} from '~/utils/theme';
import styled from 'styled-components'; import styled from 'styled-components';
import useClassNames from '~/hooks/useClassNames';
const height = em(36); const height = em(36);
...@@ -128,14 +129,16 @@ const Button: FunctionComponent<ButtonProps & WithStyled> = ({ ...@@ -128,14 +129,16 @@ const Button: FunctionComponent<ButtonProps & WithStyled> = ({
const buttonType = useMemo(() => type || 'default', [type]); const buttonType = useMemo(() => type || 'default', [type]);
const classNames = useClassNames(className, {rounded, disabled, outline: buttonType === 'default' || outline}, [
className,
rounded,
disabled,
buttonType,
outline
]);
return ( return (
<Wrapper <Wrapper className={classNames} type={buttonType} onClick={click}>
className={`${className ?? ''} ${rounded ? 'rounded' : ''} ${disabled ? 'disabled' : ''} ${
buttonType === 'default' || outline ? 'outline' : ''
}`}
type={buttonType}
onClick={click}
>
{icon && <Icon type={icon}></Icon>} {icon && <Icon type={icon}></Icon>}
{children} {children}
</Wrapper> </Wrapper>
......
...@@ -18,6 +18,7 @@ import React, {FunctionComponent} from 'react'; ...@@ -18,6 +18,7 @@ import React, {FunctionComponent} from 'react';
import {WithStyled, borderRadius, headerHeight, math, rem, sameBorder, size, transitionProps} from '~/utils/style'; import {WithStyled, borderRadius, headerHeight, math, rem, sameBorder, size, transitionProps} from '~/utils/style';
import styled from 'styled-components'; import styled from 'styled-components';
import useClassNames from '~/hooks/useClassNames';
const Div = styled.div<{maximized?: boolean; divWidth?: string; divHeight?: string}>` const Div = styled.div<{maximized?: boolean; divWidth?: string; divHeight?: string}>`
${props => ${props =>
...@@ -43,13 +44,10 @@ type ChartProps = { ...@@ -43,13 +44,10 @@ type ChartProps = {
}; };
const Chart: FunctionComponent<ChartProps & WithStyled> = ({maximized, width, height, className, children}) => { const Chart: FunctionComponent<ChartProps & WithStyled> = ({maximized, width, height, className, children}) => {
const classNames = useClassNames({maximized}, className, [maximized, className]);
return ( return (
<Div <Div maximized={maximized} divWidth={width} divHeight={height} className={classNames}>
maximized={maximized}
divWidth={width}
divHeight={height}
className={`${maximized ? 'maximized' : ''} ${className ?? ''}`}
>
{children} {children}
</Div> </Div>
); );
......
...@@ -156,11 +156,11 @@ const ToggleChartToolbox: FunctionComponent<ToggleChartToolboxItem> = ({ ...@@ -156,11 +156,11 @@ const ToggleChartToolbox: FunctionComponent<ToggleChartToolboxItem> = ({
}) => { }) => {
const [active, setActive] = useState(false); const [active, setActive] = useState(false);
const click = useCallback(() => { const click = useCallback(() => {
onClick?.(!active);
setActive(a => { setActive(a => {
onClick?.(!a);
return !a; return !a;
}); });
}, [onClick]); }, [active, onClick]);
const toolboxIcon = useMemo( const toolboxIcon = useMemo(
() => <ChartToolboxIcon icon={icon} activeIcon={activeIcon} activeStatus={active} toggle onClick={click} />, () => <ChartToolboxIcon icon={icon} activeIcon={activeIcon} activeStatus={active} toggle onClick={click} />,
[icon, activeIcon, active, click] [icon, activeIcon, active, click]
......
...@@ -54,7 +54,7 @@ const Inner = styled.div<{checked?: boolean; size?: string; disabled?: boolean}> ...@@ -54,7 +54,7 @@ const Inner = styled.div<{checked?: boolean; size?: string; disabled?: boolean}>
props.disabled props.disabled
? props.checked ? props.checked
? 'var(--text-lighter-color)' ? 'var(--text-lighter-color)'
: 'var(--text-lighter-color)' : 'transparent'
: props.checked : props.checked
? 'var(--primary-color)' ? 'var(--primary-color)'
: 'var(--background-color)'}; : 'var(--background-color)'};
...@@ -84,15 +84,15 @@ const Content = styled.div<{disabled?: boolean}>` ...@@ -84,15 +84,15 @@ const Content = styled.div<{disabled?: boolean}>`
`; `;
type CheckboxProps = { type CheckboxProps = {
value?: boolean; checked?: boolean;
onChange?: (value: boolean) => unknown; onChange?: (checked: boolean) => unknown;
size?: 'small'; size?: 'small';
title?: string; title?: string;
disabled?: boolean; disabled?: boolean;
}; };
const Checkbox: FunctionComponent<CheckboxProps & WithStyled> = ({ const Checkbox: FunctionComponent<CheckboxProps & WithStyled> = ({
value, checked: value,
children, children,
size, size,
disabled, disabled,
...@@ -101,7 +101,7 @@ const Checkbox: FunctionComponent<CheckboxProps & WithStyled> = ({ ...@@ -101,7 +101,7 @@ const Checkbox: FunctionComponent<CheckboxProps & WithStyled> = ({
onChange onChange
}) => { }) => {
const [checked, setChecked] = useState(!!value); const [checked, setChecked] = useState(!!value);
useEffect(() => setChecked(!!value), [setChecked, value]); useEffect(() => setChecked(!!value), [value]);
const onChangeInput = useCallback( const onChangeInput = useCallback(
(e: React.ChangeEvent<HTMLInputElement>) => { (e: React.ChangeEvent<HTMLInputElement>) => {
if (disabled) { if (disabled) {
......
...@@ -215,19 +215,19 @@ const PRCurveChart: FunctionComponent<PRCurveChartProps> = ({type, runs, tag, ru ...@@ -215,19 +215,19 @@ const PRCurveChart: FunctionComponent<PRCurveChartProps> = ({type, runs, tag, ru
{ {
icon: 'maximize', icon: 'maximize',
activeIcon: 'minimize', activeIcon: 'minimize',
tooltip: t('curves:maximize'), tooltip: t('common:maximize'),
activeTooltip: t('curves:minimize'), activeTooltip: t('common:minimize'),
toggle: true, toggle: true,
onClick: () => setMaximized(m => !m) onClick: () => setMaximized(m => !m)
}, },
{ {
icon: 'restore-size', icon: 'restore-size',
tooltip: t('curves:restore'), tooltip: t('common:restore'),
onClick: () => echart.current?.restore() onClick: () => echart.current?.restore()
}, },
{ {
icon: 'download', icon: 'download',
tooltip: t('curves:download-image'), tooltip: t('common:download-image'),
onClick: () => echart.current?.saveAsImage() onClick: () => echart.current?.saveAsImage()
} }
]} ]}
......
...@@ -15,7 +15,7 @@ ...@@ -15,7 +15,7 @@
*/ */
import React, {FunctionComponent} from 'react'; import React, {FunctionComponent} from 'react';
import {em, size, transitionProps, zIndexes} from '~/utils/style'; import {em, transitionProps, zIndexes} from '~/utils/style';
import Icon from '~/components/Icon'; import Icon from '~/components/Icon';
import Properties from '~/components/GraphPage/Properties'; import Properties from '~/components/GraphPage/Properties';
...@@ -58,10 +58,18 @@ const Dialog = styled.div` ...@@ -58,10 +58,18 @@ const Dialog = styled.div`
> .modal-close { > .modal-close {
flex: none; flex: none;
${size(em(20, 18), em(20, 18))} font-size: ${em(16, 18)};
font-size: ${em(20, 18)};
text-align: center;
cursor: pointer; cursor: pointer;
color: var(--text-color);
${transitionProps('color')}
&:hover {
color: var(--text-light-color);
}
&:active {
color: var(--text-lighter-color);
}
} }
} }
......
...@@ -34,6 +34,8 @@ const ReductionTab: FunctionComponent<ReductionTabProps> = ({value, onChange}) = ...@@ -34,6 +34,8 @@ const ReductionTab: FunctionComponent<ReductionTabProps> = ({value, onChange}) =
<Tab <Tab
list={reductions.map(value => ({value, label: t(`high-dimensional:reduction-value.${value}`)}))} list={reductions.map(value => ({value, label: t(`high-dimensional:reduction-value.${value}`)}))}
value={value} value={value}
variant="fullWidth"
appearance="underscore"
onChange={onChange} onChange={onChange}
/> />
); );
......
...@@ -289,14 +289,14 @@ const HistogramChart: FunctionComponent<HistogramChartProps> = ({run, tag, mode, ...@@ -289,14 +289,14 @@ const HistogramChart: FunctionComponent<HistogramChartProps> = ({run, tag, mode,
{ {
icon: 'maximize', icon: 'maximize',
activeIcon: 'minimize', activeIcon: 'minimize',
tooltip: t('histogram:maximize'), tooltip: t('common:maximize'),
activeTooltip: t('histogram:minimize'), activeTooltip: t('common:minimize'),
toggle: true, toggle: true,
onClick: () => setMaximized(m => !m) onClick: () => setMaximized(m => !m)
}, },
{ {
icon: 'download', icon: 'download',
tooltip: t('histogram:download-image'), tooltip: t('common:download-image'),
onClick: () => echart.current?.saveAsImage() onClick: () => echart.current?.saveAsImage()
} }
]} ]}
......
/**
* Copyright 2020 Baidu Inc. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import Select from '~/components/Select';
import type {SelectProps} from '~/components/Select';
import {borderRadius} from '~/utils/style';
import styled from 'styled-components';
// forgive me, I don't want to write a type guard
const BorderLessSelect = styled<React.FunctionComponent<SelectProps<string>>>(Select)`
border: none;
line-height: 1.1;
min-width: 4em;
--height: 1em;
--padding: 0;
.list {
border-radius: ${borderRadius};
}
`;
export default BorderLessSelect;
/**
* Copyright 2020 Baidu Inc. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import * as d3 from 'd3';
import {COLOR_MAP, getColorScale} from '~/resource/hyper-parameter';
import React, {FunctionComponent, useEffect, useMemo, useState} from 'react';
import Select from '~/components/HyperParameterPage/BorderLessSelect';
import type {ViewData} from '~/resource/hyper-parameter';
import type {WithStyled} from '~/utils/style';
import {rem} from '~/utils/style';
import styled from 'styled-components';
const Wrapper = styled.div`
width: ${rem(88)};
margin-left: ${rem(20)};
display: flex;
flex-direction: column;
.color-by {
flex: none;
margin-bottom: 1em;
}
.color-indicator {
flex: auto;
display: flex;
color: var(--text-light-color);
.indicator-image {
flex: none;
width: 1em;
background-image: linear-gradient(0deg, ${COLOR_MAP[0]} 0%, ${COLOR_MAP[1]} 50%, ${COLOR_MAP[2]} 100%);
margin: 0.5em 0;
}
.indicator-text {
flex: auto;
display: flex;
flex-direction: column-reverse;
justify-content: space-between;
margin-left: 0.5em;
}
}
`;
type ColorMapProps = Pick<ViewData, 'indicators' | 'data'> & {
onChange: (colors: string[]) => unknown;
};
const ColorMap: FunctionComponent<ColorMapProps & WithStyled> = ({indicators, data, onChange}) => {
const colorByList = useMemo(() => indicators.filter(i => i.type === 'continuous').map(i => i.name), [indicators]);
const [colorBy, setColorBy] = useState(colorByList[0] ?? null);
const colorByIndicator = useMemo(() => indicators.find(i => i.name === colorBy), [colorBy, indicators]);
const colorByExtent = useMemo(
() =>
colorByIndicator
? d3.extent(data.map(row => +row[colorByIndicator.group][colorByIndicator.name]))
: ([0, 0] as [number, number]),
[colorByIndicator, data]
);
useEffect(() => {
const scale = getColorScale(colorByIndicator ?? null, data);
const colors = data.map((row, i) =>
scale(colorByIndicator ? (row[colorByIndicator.group][colorByIndicator.name] as number) : i)
);
onChange?.(colors);
}, [colorByIndicator, data, onChange]);
return (
<Wrapper className="color-map">
<div className="color-by">
<Select list={colorByList} value={colorBy} onChange={setColorBy} />
</div>
<div className="color-indicator">
<div className="indicator-image"></div>
<div className="indicator-text">
{colorByExtent.map((c: string | number | undefined, i: number) => (
<span key={i}>{c ?? ''}</span>
))}
</div>
</div>
</Wrapper>
);
};
export default ColorMap;
/**
* Copyright 2020 Baidu Inc. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import * as d3 from 'd3';
import React, {FunctionComponent, useEffect, useMemo} from 'react';
import {WithStyled, borderRadius, rem, tint, transitionProps} from '~/utils/style';
import BarChart from '~/components/BarChart';
import Icon from '~/components/Icon';
import type {ImportanceData} from '~/resource/hyper-parameter';
import {colors} from '~/utils/theme';
import styled from 'styled-components';
import useRequest from '~/hooks/useRequest';
import {useTranslation} from 'react-i18next';
const COLOR_RANGE = [tint(0.6, colors.primary.default), colors.primary.default];
const Dialog = styled.div<{visible: boolean}>`
width: ${rem(450)};
height: ${rem(370)};
background-color: var(--background-color);
border-radius: ${borderRadius};
box-shadow: 0 ${rem(4)} ${rem(12)} 0 rgba(0, 0, 0, 0.16);
display: ${props => (props.visible ? 'inline-block' : 'none')};
position: relative;
padding: ${rem(20)};
z-index: 1;
> .close {
position: absolute;
top: ${rem(20)};
right: ${rem(20)};
font-size: ${rem(12)};
cursor: pointer;
color: var(--text-lighter-color);
${transitionProps('color')}
&:hover {
color: var(--text-light-color);
}
&:active {
color: var(--text-color);
}
}
`;
const ImportanceBarChart = styled(BarChart)`
width: 100%;
height: 100%;
`;
interface ImportanceDialogProps {
visible?: boolean;
onClickClose?: () => unknown;
}
const ImportanceDialog: FunctionComponent<ImportanceDialogProps & WithStyled> = ({
visible,
onClickClose,
className
}) => {
const {t} = useTranslation(['hyper-parameter', 'common']);
const {data, mutate, isValidating} = useRequest<ImportanceData[]>('/hparams/importance', {
revalidateOnMount: false
});
useEffect(() => {
if (visible) {
mutate();
}
}, [mutate, visible]);
const reversedData = useMemo(() => {
if (!data) {
return [];
}
const arr = [...data];
arr.reverse();
return arr;
}, [data]);
const options = useMemo(
() => ({
animation: false,
tooltip: {
show: false
}
}),
[]
);
const categories = useMemo(() => reversedData.map(item => item.name) ?? [], [reversedData]);
const series = useMemo(() => {
const ranger = d3.scaleLinear<string, string>().domain([0, reversedData.length]).range(COLOR_RANGE);
return [
{
barMaxWidth: 14,
data:
reversedData.map((item, index) => ({
value: item.value,
itemStyle: {
color: ranger(index)
}
})) ?? [],
type: 'bar' as const
}
];
}, [reversedData]);
return (
<Dialog visible={!!visible} className={className}>
<ImportanceBarChart
title={t('hyper-parameter:parameter-importance')}
direction="horizontal"
options={options}
categories={categories}
data={series}
loading={isValidating}
/>
<a className="close" onClick={() => onClickClose?.()}>
<Icon type="close" />
</a>
</Dialog>
);
};
export default ImportanceDialog;
/**
* Copyright 2020 Baidu Inc. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React, {FunctionComponent, useCallback, useEffect, useRef, useState} from 'react';
import NumberInput from './NumberInput';
import type {Range} from '~/resource/hyper-parameter';
import styled from 'styled-components';
import {useTranslation} from 'react-i18next';
const Input = styled(NumberInput)`
width: 100%;
`;
const Row = styled.div`
display: flex;
align-items: center;
> * {
flex-grow: 1;
}
> .label {
flex-grow: 0;
flex-shrink: 0;
margin-right: 1em;
}
`;
interface ContinuousIndicatorDetailsProps {
min?: number;
max?: number;
onChange?: (range: Range) => unknown;
}
const DiscreteIndicatorDetails: FunctionComponent<ContinuousIndicatorDetailsProps> = ({min, max, onChange}) => {
const {t} = useTranslation('hyper-parameter');
const [range, setRange] = useState<Range>({
min: min ?? Number.NEGATIVE_INFINITY,
max: max ?? Number.POSITIVE_INFINITY
});
const changeMin = useCallback((v: number) => {
setRange(r => ({
...r,
min: v
}));
}, []);
const changeMax = useCallback((v: number) => {
setRange(r => ({
...r,
max: v
}));
}, []);
const change = useRef(onChange);
useEffect(() => {
change.current = onChange;
}, [onChange]);
useEffect(() => {
if (range.min === min && range.max === max) {
return;
}
change.current?.(range);
}, [max, min, range]);
return (
<>
<Row>
<span className="label">{t('hyper-parameter:min')}</span>
<Input
value={range.min}
defaultValue={Number.NEGATIVE_INFINITY}
placeholder={t('hyper-parameter:negative-infinity')}
onChange={changeMin}
/>
</Row>
<Row>
<span className="label">{t('hyper-parameter:max')}</span>
<Input
value={range.max}
defaultValue={Number.POSITIVE_INFINITY}
placeholder={t('hyper-parameter:positive-infinity')}
onChange={changeMax}
/>
</Row>
</>
);
};
export default DiscreteIndicatorDetails;
/**
* Copyright 2020 Baidu Inc. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React, {FunctionComponent, useCallback, useEffect, useState} from 'react';
import Checkbox from '~/components/Checkbox';
import without from 'lodash/without';
interface DiscreteIndicatorDetailsProps<T extends string[] | number[]> {
list: T;
values?: T;
onChange?: (values: T) => unknown;
}
const DiscreteIndicatorDetails = <T extends string[] | number[]>({
list,
values,
onChange
}: DiscreteIndicatorDetailsProps<T>): ReturnType<FunctionComponent> => {
const [selected, setSelected] = useState<(string | number)[]>(values ?? list);
useEffect(() => setSelected(values ?? list), [list, values]);
const changeValue = useCallback(
(value: T[number], checked: boolean) => {
setSelected(v => {
if (checked && !v.includes(value)) {
const nv = [...v, value];
onChange?.(nv as T);
return nv;
}
if (!checked && v.includes(value)) {
const nv = without(v, value);
onChange?.(nv as T);
return nv;
}
return v;
});
},
[onChange]
);
return (
<>
{(list as (string | number)[]).map((value, index) => (
<Checkbox
checked={selected.includes(value)}
onChange={checked => changeValue(value, checked)}
key={index}
>
{value}
</Checkbox>
))}
</>
);
};
export default DiscreteIndicatorDetails;
/**
* Copyright 2020 Baidu Inc. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type {IndicatorType, Range} from '~/resource/hyper-parameter';
import React, {FunctionComponent, useCallback, useEffect, useMemo, useState} from 'react';
import Checkbox from '~/components/Checkbox';
import ContinuousIndicatorDetails from './ContinuousIndicatorDetails';
import DiscreteIndicatorDetails from './DiscreteIndicatorDetails';
import Icon from '~/components/Icon';
import {rem} from '~/utils/style';
import styled from 'styled-components';
const Title = styled.div`
display: flex;
justify-content: space-between;
align-items: center;
> * {
flex-grow: 1;
}
> .expander {
margin-left: 1em;
flex-grow: 0;
cursor: pointer;
color: var(--text-lighter-color);
font-size: 0.75em;
&:hover {
color: var(--text-light-color);
}
}
`;
const Details = styled.div`
display: flex;
flex-direction: column;
justify-content: flex-start;
align-items: stretch;
margin-left: ${rem(24)};
font-size: ${rem(12)};
> * {
margin-top: ${rem(10)};
}
`;
interface IndicatorProps {
name: string;
type: IndicatorType;
selected?: boolean;
values?: string[] | number[];
selectedValues?: string[] | number[];
min?: number;
max?: number;
onToggle?: (value: boolean) => unknown;
onChange?: (data: Range | string[] | number[]) => unknown;
}
const Indicator: FunctionComponent<IndicatorProps> = ({
name,
type,
selected: propsSelected,
values,
selectedValues: propsSelectedValues,
min,
max,
onToggle,
onChange
}) => {
const [expand, setExpand] = useState(true);
const [selected, setSelected] = useState(propsSelected ?? true);
useEffect(() => setSelected(propsSelected ?? true), [propsSelected]);
const [selectedValues, setSelectedValues] = useState(propsSelectedValues ?? []);
useEffect(() => setSelectedValues(propsSelectedValues ?? []), [propsSelectedValues]);
const [range, setRange] = useState<Range>({
min: min ?? Number.NEGATIVE_INFINITY,
max: max ?? Number.POSITIVE_INFINITY
});
useEffect(
() =>
setRange({
min: min ?? Number.NEGATIVE_INFINITY,
max: max ?? Number.POSITIVE_INFINITY
}),
[min, max]
);
const toggle = useCallback(() => {
onToggle?.(!selected);
setSelected(value => !value);
}, [onToggle, selected]);
const change = useCallback(
(data: Range | string[] | number[]) => {
if (type === 'continuous') {
setSelectedValues(data as string[] | number[]);
} else {
setRange(data as Range);
}
onChange?.(data);
},
[onChange, type]
);
const details = useMemo(() => {
switch (type) {
case 'string':
case 'numeric':
return <DiscreteIndicatorDetails list={values ?? []} values={selectedValues} onChange={change} />;
case 'continuous':
return <ContinuousIndicatorDetails min={range.min} max={range.max} onChange={change} />;
default:
return null as never;
}
}, [change, range.max, range.min, selectedValues, type, values]);
return (
<>
<Title>
<Checkbox checked={selected} onChange={toggle}>
{name}
</Checkbox>
<a className="expander" onClick={() => setExpand(v => !v)}>
<Icon type={expand ? 'chevron-down' : 'chevron-up'} />
</a>
</Title>
{expand ? <Details>{details}</Details> : null}
</>
);
};
export default Indicator;
/**
* Copyright 2020 Baidu Inc. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type {Indicator, IndicatorGroup, Range} from '~/resource/hyper-parameter';
import React, {FunctionComponent, useCallback, useEffect, useMemo, useState} from 'react';
import {AsideSection} from '~/components/Aside';
import Field from '~/components/Field';
import IndicatorItem from './Indicator';
import Tab from '~/components/Tab';
import type {TabProps} from '~/components/Tab';
import styled from 'styled-components';
import {useTranslation} from 'react-i18next';
const DataTypeTab = styled<React.FunctionComponent<TabProps<IndicatorGroup>>>(Tab)`
font-weight: 700;
`;
export interface IndicatorFilterProps {
indicators: Indicator[];
onChange?: (indicators: Indicator[]) => unknown;
}
const IndicatorFilter: FunctionComponent<IndicatorFilterProps> = ({indicators, onChange}) => {
const {t} = useTranslation(['hyper-parameter', 'common']);
const dataTypeList = useMemo(
() =>
(['hparams', 'metrics'] as IndicatorGroup[]).map(value => ({
value,
label: t(`hyper-parameter:data-type-value.${value}`)
})),
[t]
);
const [dataType, setDataType] = useState<IndicatorGroup>('hparams');
const indicatorsInGroup = useMemo(() => indicators.filter(indicator => indicator.group === dataType), [
dataType,
indicators
]);
const [result, setResult] = useState(indicators);
useEffect(() => setResult(indicators), [indicators]);
const updateResult = useCallback((indicator: Indicator, data: Partial<Indicator>) => {
setResult(old => {
const index = old.findIndex(i => i.name === indicator.name && i.group === indicator.group);
const n = [...old];
if (index >= 0) {
Object.assign(n[index], data);
}
return n;
});
}, []);
const toggle = useCallback(
(indicator: Indicator, visible: boolean) => updateResult(indicator, {selected: visible}),
[updateResult]
);
const change = useCallback(
(indicator: Indicator, data: Range | string[] | number[]) => {
switch (indicator.type) {
case 'numeric':
case 'string':
return updateResult(indicator, {selectedValues: data as string[] | number[]});
case 'continuous':
return updateResult(indicator, data as Range);
}
},
[updateResult]
);
useEffect(() => {
onChange?.(result);
}, [result, onChange]);
return (
<div>
<AsideSection>
<Field>
<DataTypeTab list={dataTypeList} value={dataType} onChange={setDataType} appearance="underscore" />
</Field>
{indicatorsInGroup.map(indicator => (
<AsideSection key={indicator.group + indicator.name}>
<Field>
<IndicatorItem
name={indicator.name}
type={indicator.type}
selected={indicator.selected}
values={indicator.type === 'continuous' ? undefined : indicator.values}
selectedValues={indicator.type === 'continuous' ? undefined : indicator.selectedValues}
min={indicator.type === 'continuous' ? indicator.min : undefined}
max={indicator.type === 'continuous' ? indicator.max : undefined}
onToggle={visible => toggle(indicator, visible)}
onChange={(data: Range | string[] | number[]) => change(indicator, data)}
/>
</Field>
</AsideSection>
))}
</AsideSection>
</div>
);
};
export default IndicatorFilter;
/**
* Copyright 2020 Baidu Inc. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React, {FunctionComponent, useCallback, useEffect, useState} from 'react';
import Input from '~/components/Input';
import type {WithStyled} from '~/utils/style';
interface NumberInputProps {
value?: number;
defaultValue: number;
placeholder?: string;
disabled?: boolean;
onChange?: (value: number) => unknown;
}
const NumberInput: FunctionComponent<NumberInputProps & WithStyled> = ({
value,
defaultValue,
placeholder,
className,
disabled,
onChange
}) => {
const [inputValue, setInputValue] = useState(Number.isFinite(value) ? value + '' : '');
useEffect(() => setInputValue(Number.isFinite(value) ? value + '' : ''), [value]);
useEffect(() => {
if (inputValue === '' && value !== defaultValue) {
onChange?.(defaultValue);
return;
}
const v = Number.parseFloat(inputValue);
if (!Number.isNaN(v)) {
onChange?.(v);
}
}, [defaultValue, inputValue, onChange, value]);
const check = useCallback(() => {
const v = Number.parseFloat(inputValue);
if (Number.isNaN(v)) {
setInputValue(Number.isFinite(value) ? value + '' : '');
} else {
setInputValue(v + '');
}
}, [inputValue, value]);
return (
<Input
value={inputValue}
placeholder={placeholder}
disabled={disabled}
className={className}
onBlur={check}
onChange={setInputValue}
/>
);
};
export default NumberInput;
/**
* Copyright 2020 Baidu Inc. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import IndicatorFilter from './IndicatorFilter';
export default IndicatorFilter;
export type {IndicatorFilterProps} from './IndicatorFilter';
/**
* Copyright 2020 Baidu Inc. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React, {FunctionComponent} from 'react';
import type {Run} from '~/types';
import ScalarChart from '~/components/HyperParameterPage/ScalarChart';
import {Trans} from 'react-i18next';
import {rem} from '~/utils/style';
import styled from 'styled-components';
const PUBLIC_PATH: string = import.meta.env.SNOWPACK_PUBLIC_PATH;
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;
&.maximized {
margin-right: 0;
}
}
`;
const Empty = styled.div`
width: 100%;
height: ${rem(370)};
background-image: url(${`${PUBLIC_PATH}/images/empty.svg`});
background-repeat: no-repeat;
background-position: calc(50% + ${rem(12)}) ${rem(50)};
background-size: ${rem(200)} ${rem(200)};
padding-top: ${rem(250)};
font-size: ${rem(16)};
text-align: center;
line-height: 2;
> .subtitle {
font-size: 0.875em;
color: var(--text-lighter-color);
}
`;
export interface MetricGraphsProps {
metrics: string[];
run: Run;
}
const MetricGraphs: FunctionComponent<MetricGraphsProps> = ({metrics, run}) => {
return (
<Wrapper>
{metrics.length ? (
metrics.map(metric => <ScalarChart key={metric} metric={metric} run={run} />)
) : (
<Empty>
<Trans i18nKey="hyper-parameter:metric-graphs-empty">
<div>No metrics selected.</div>
<div className="subtitle">Please select some metrics to view graphs here.</div>
</Trans>
</Empty>
)}
</Wrapper>
);
};
export default MetricGraphs;
/**
* Copyright 2020 Baidu Inc. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type {Indicator, ViewData} from '~/resource/hyper-parameter';
import React, {FunctionComponent, useCallback, useEffect, useMemo, useRef, useState} from 'react';
import Graph from './ParallelCoordinatesGraph';
import {ScaleMethod} from '~/resource/hyper-parameter';
import ScaleMethodSelect from '~/components/HyperParameterPage/ScaleMethodSelect';
import type {WithStyled} from '~/utils/style';
import styled from 'styled-components';
const Container = styled.div`
width: 100%;
overflow: auto;
.interpolates {
display: flex;
margin-left: ${Graph.GRID_BRUSH_WIDTH / 2}px;
margin-top: 1em;
}
svg {
.line {
fill: none;
}
.hover-trigger {
cursor: pointer;
}
.select-indicator {
fill: var(--background-color);
stroke-width: 2px;
}
.disabled {
.line {
stroke: var(--hyper-parameter-graph-disabled-data-color);
}
.stroke-width {
stroke: var(--hyper-parameter-graph-disabled-data-color);
}
.hover-trigger {
cursor: unset;
}
}
.grid {
.indicator {
fill: var(--text-color);
dominant-baseline: text-before-edge;
&.metrics {
font-weight: bold;
}
.dragger {
opacity: 0;
cursor: grab;
}
&:hover .dragger {
opacity: 1;
}
}
.axis {
color: var(--hyper-parameter-graph-axis-color);
text {
color: var(--text-light-color);
pointer-events: none;
}
.grid-brush .selection {
fill: var(--hyper-parameter-graph-brush-color);
fill-opacity: 0.4;
}
}
&.dragging {
.indicator {
fill: var(--primary-color);
}
.dragger {
opacity: 1;
cursor: grabbing;
filter: invert(16%) sepia(99%) saturate(5980%) hue-rotate(243deg) brightness(89%) contrast(98%);
}
}
}
}
`;
const StyledScaleMethodSelect = styled(ScaleMethodSelect)`
position: relative;
`;
type ParallelCoordinatesProps = ViewData & {
colors: string[];
onHover?: (index: number | null) => unknown;
onSelect?: (index: number | null) => unknown;
};
const ParallelCoordinates: FunctionComponent<ParallelCoordinatesProps & WithStyled> = ({
indicators,
data,
colors,
onHover,
onSelect,
className
}) => {
const container = useRef<HTMLDivElement>(null);
const graph = useRef<Graph>();
const [columnWidth, setColumnWidth] = useState(0);
const [indicatorsOrder, setIndicatorsOrder] = useState(indicators.map(({name}) => name));
useEffect(() => {
setIndicatorsOrder(indicators.map(({name}) => name));
}, [indicators]);
const orderedIndicators = useMemo(
() =>
indicatorsOrder
.filter(o => indicators.findIndex(i => i.name === o) >= 0)
.map(name => indicators.find(i => i.name === name) as Indicator),
[indicatorsOrder, indicators]
);
const [indicatorScaleMethod, setIndicatorScaleMethod] = useState(
indicators.reduce<Record<string, ScaleMethod>>((result, indicator) => {
if (indicator.type === 'continuous') {
result[indicator.name] = ScaleMethod.LINEAR;
}
return result;
}, {})
);
const [draggingIndicator, setDraggingIndicator] = useState<string | null>(null);
const [draggingIndicatorOffset, setDraggingIndicatorOffset] = useState<number>(0);
const changeIndicatorScaleMethod = useCallback((indicator: Indicator, scale: ScaleMethod) => {
setIndicatorScaleMethod(r => ({
...r,
[indicator.name]: scale
}));
graph.current?.setScaleMethod(indicator.name, scale);
}, []);
useEffect(() => {
if (!container.current) {
return;
}
graph.current = new Graph(container.current);
graph.current.on('dragging', (name, offset, order) => {
setDraggingIndicator(name);
setDraggingIndicatorOffset(offset);
setIndicatorsOrder(order);
});
graph.current.on('dragged', order => {
setDraggingIndicator(null);
setDraggingIndicatorOffset(0);
setIndicatorsOrder(order);
});
return () => graph.current?.dispose();
}, []);
useEffect(() => {
const c = container.current;
if (c) {
const observer = new ResizeObserver(() => {
const rect = c.getBoundingClientRect();
graph.current?.resize(rect.width);
setColumnWidth(graph.current?.columnWidth ?? 0);
});
observer.observe(c);
return () => {
observer.unobserve(c);
};
}
}, []);
useEffect(() => {
if (onHover) {
graph.current?.on('hover', onHover);
return () => {
graph.current?.off('hover', onHover);
};
}
}, [onHover]);
useEffect(() => {
if (onSelect) {
graph.current?.on('select', onSelect);
return () => {
graph.current?.off('select', onSelect);
};
}
}, [onSelect]);
useEffect(() => {
graph.current?.setColors(colors);
}, [colors]);
useEffect(() => {
graph.current?.render(indicators, data);
setColumnWidth(graph.current?.columnWidth ?? 0);
}, [indicators, data]);
return (
<Container className={className}>
<div ref={container}></div>
<div className="interpolates">
{orderedIndicators.map(indicator => (
<div
key={indicator.name}
style={{
width: `${columnWidth}px`,
position: 'relative',
left: `${draggingIndicator === indicator.name ? draggingIndicatorOffset : 0}px`
}}
>
{indicatorScaleMethod[indicator.name] != null ? (
<StyledScaleMethodSelect
direction="top"
scaleMethod={indicatorScaleMethod[indicator.name]}
onChange={scale => changeIndicatorScaleMethod(indicator, scale)}
/>
) : null}
</div>
))}
</div>
</Container>
);
};
export default ParallelCoordinates;
/**
* Copyright 2020 Baidu Inc. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
// cspell:words quantile quantiles unhover
import * as d3 from 'd3';
import type {DataListItem, Indicator} from '~/resource/hyper-parameter';
import EventEmitter from 'eventemitter3';
import {ScaleMethod} from '~/resource/hyper-parameter';
import intersection from 'lodash/intersection';
const PUBLIC_PATH: string = import.meta.env.SNOWPACK_PUBLIC_PATH;
type YScale =
| d3.ScalePoint<string | number>
| d3.ScaleLogarithmic<number, number>
| d3.ScaleLinear<number, number>
| d3.ScaleQuantile<number>;
type GridIndicator = Indicator & {
scale: ScaleMethod;
x: number;
yScale: YScale;
grid: d3.Selection<SVGGElement, unknown, null, undefined> | null;
};
interface LineData {
data: DataListItem;
color: string;
line: d3.Selection<SVGGElement, unknown, null, undefined> | null;
}
const INDICATORS_HEIGHT = 25;
const MIN_COLUMN_WIDTH = 60;
const GRID_PADDING = 5;
interface EventTypes {
hover: [number | null];
select: [number | null];
dragging: [string, number, string[]];
dragged: [string[]];
}
export default class ParallelCoordinatesGraph extends EventEmitter<EventTypes> {
static GRAPH_HEIGHT = 300;
static GRID_BRUSH_WIDTH = 20;
private svg;
private containerWidth;
private colors: string[] = [];
private data: DataListItem[] = [];
private grids: GridIndicator[] = [];
private lines: LineData[] = [];
private hoveredLineIndex: number | null = null;
private selectedLineIndex: number | null = null;
private brushedLineIndexesArray: (number[] | null)[] = [];
private dragStartX = 0;
private draggingIndicator: GridIndicator | null = null;
get svgWidth() {
return this.columnWidth * this.grids.length + ParallelCoordinatesGraph.GRID_BRUSH_WIDTH / 2;
}
get columnWidth() {
if (this.grids.length === 0) {
return 0;
}
return Math.max(
(this.containerWidth - ParallelCoordinatesGraph.GRID_BRUSH_WIDTH) / this.grids.length,
MIN_COLUMN_WIDTH
);
}
get brushedLineIndexes() {
return this.brushedLineIndexesArray.every(i => i == null)
? null
: intersection(...this.brushedLineIndexesArray.filter(i => i != null));
}
get sequenceGrids() {
return [...this.grids].sort((a, b) => a.x - b.x);
}
constructor(container: HTMLElement) {
super();
this.containerWidth = container.getBoundingClientRect().width;
const [width, height] = [this.containerWidth, ParallelCoordinatesGraph.GRAPH_HEIGHT + INDICATORS_HEIGHT];
this.svg = d3
.select(container)
.append('svg')
.attr('width', width)
.attr('height', height)
.attr('viewBox', `0 0 ${width} ${height}`)
.on('click', () => {
this.unselectLine();
});
}
private getDataByIndicator(indicator: Indicator) {
return this.data.map(row => row[indicator.group][indicator.name]);
}
private removeGrids() {
this.brushedLineIndexesArray = [];
this.grids.forEach(indicator => {
indicator.grid?.remove();
indicator.grid = null;
});
}
private drawGrids() {
this.brushedLineIndexesArray = Array(this.grids.length).fill(null);
this.sequenceGrids.forEach((indicator, index) => {
const x = indicator.x;
const g = this.svg.append('g').classed('grid', true).attr('transform', `translate(${x}, 0)`);
const indicatorG = g.append('g').classed('indicator', true).classed(indicator.group, true);
const text = indicatorG.append('text').attr('x', 0).attr('y', 0).text(indicator.name);
let textLength = text.node()?.getComputedTextLength() ?? 0;
while (textLength > this.columnWidth - ParallelCoordinatesGraph.GRID_BRUSH_WIDTH / 2) {
text.text(text.text().slice(0, -1) + '...');
textLength = text.node()?.getComputedTextLength() ?? 0;
}
indicatorG
.append('image')
.classed('dragger', true)
.attr('x', -15)
.attr('y', -1)
.attr('width', 16)
.attr('height', 16)
.attr('href', `${PUBLIC_PATH}/icons/dragger.svg`)
.call(
d3
.drag()
// FIXME: complete types, `NEXT TIME MUST` :)
// eslint-disable-next-line @typescript-eslint/no-explicit-any
.container(this.svg as any)
.on('start', ({x}) => this.dragstart(indicator, x))
.on('drag', ({x}) => this.dragging(indicator, x))
// eslint-disable-next-line @typescript-eslint/no-explicit-any
.on('end', () => this.dragend()) as any
);
const axisG = g.append('g').classed('axis', true).attr('transform', `translate(0, ${INDICATORS_HEIGHT})`);
const scale = indicator.yScale;
const axis = d3.axisRight(scale as d3.AxisScale<d3.AxisDomain>);
if (indicator.scale === ScaleMethod.QUANTILE) {
(axis as d3.Axis<number>)
.tickValues((scale as d3.ScaleQuantile<number>).quantiles())
.tickFormat(d3.format('-.6g'));
}
axisG.call(axis);
const gridHeight = ParallelCoordinatesGraph.GRAPH_HEIGHT - 2 * GRID_PADDING;
const brushWidth = ParallelCoordinatesGraph.GRID_BRUSH_WIDTH;
const brushG = axisG
.append('g')
.classed('grid-brush', true)
.attr('transform', `translate(${-brushWidth / 2}, 0)`);
brushG.call(
d3
.brushY()
.extent([
[0, GRID_PADDING - 0.5],
[brushWidth, gridHeight + GRID_PADDING + 0.5]
])
.on('brush end', ({selection}) => this.brushed(index, selection))
);
brushG
.select('.selection')
.attr('fill', null)
.attr('fill-opacity', null)
.attr('stroke', null)
.attr('stroke-width', null);
indicator.grid = g;
});
}
private removeLines() {
this.lines.forEach(line => {
line.line?.remove();
line.line = null;
});
}
private drawLines() {
this.lines.forEach((row, rowIndex) => {
const g = this.svg.append('g').attr('transform', `translate(0, ${INDICATORS_HEIGHT})`);
g.append('path').classed('line', true).attr('fill', 'none').attr('stroke-width', 1);
g.append('path')
.classed('hover-trigger', true)
.attr('stroke', 'transparent')
.attr('fill', 'none')
.attr('stroke-width', 7)
.on('mouseenter', () => {
if (g.classed('disabled')) {
return;
}
this.hoverLine(rowIndex);
})
.on('mouseleave', () => {
if (g.classed('disabled')) {
return;
}
this.unhoverLine();
})
.on('click', (e: Event) => {
if (g.classed('disabled')) {
return;
}
this.selectLine(rowIndex);
e.stopPropagation();
});
row.line = g;
});
this.updateLines(false);
this.updateLineColors();
this.updateLineWidths();
}
private updateLineColors() {
this.lines.forEach((row, i) => {
const disabled = this.brushedLineIndexes != null && !this.brushedLineIndexes.includes(i);
const group = row.line?.classed('disabled', disabled);
const line = group?.select('.line');
const circles = group?.selectAll('.select-indicator');
if (disabled) {
line?.attr('stroke', null);
circles?.attr('stroke', null);
} else {
this.select(line, true)?.attr('stroke', row.color);
this.select(circles, true)?.attr('stroke', row.color);
}
});
}
private updateLineWidths() {
this.lines.forEach((g, i) => {
let width = 1;
if (i === this.hoveredLineIndex || i === this.selectedLineIndex) {
width = 3;
}
this.select(g.line?.select('.line'), true)?.attr('stroke-width', width);
});
}
private hoverLine(index: number) {
this.emit('hover', index);
this.hoveredLineIndex = index;
this.updateLineWidths();
}
private unhoverLine() {
if (this.hoveredLineIndex != null) {
this.hoveredLineIndex = null;
this.emit('hover', null);
}
this.updateLineWidths();
}
private selectLine(index: number) {
this.emit('select', index);
this.selectedLineIndex = index;
const line = this.lines[index];
this.lines.forEach(line => line.line?.selectAll('.select-indicator').remove());
this.sequenceGrids.forEach(g => {
line.line
?.append('circle')
.classed('select-indicator', true)
.attr('cx', g.x)
.attr('cy', g.yScale(line.data[g.group][g.name] as number) ?? 0)
.attr('r', 4)
.attr('stroke', line.color);
});
this.updateLineWidths();
}
private unselectLine() {
if (this.selectedLineIndex != null) {
this.lines[this.selectedLineIndex].line?.selectAll('.select-indicator').remove();
this.selectedLineIndex = null;
this.emit('select', null);
}
this.updateLineWidths();
}
private calculateLineColors() {
this.lines.forEach((line, i) => {
line.color = this.colors[i] ?? '#000';
});
}
private calculateXScale() {
const d = ParallelCoordinatesGraph.GRID_BRUSH_WIDTH / 2;
const scale = d3
.scalePoint()
.domain(this.sequenceGrids.map(i => i.name))
.range([d, d + this.columnWidth * (this.sequenceGrids.length - 1)]);
this.sequenceGrids.forEach(grid => {
grid.x = scale(grid.name) ?? 0;
});
}
private calculateYScales() {
const yScales = this.grids.map(grid => {
const gridHeight = ParallelCoordinatesGraph.GRAPH_HEIGHT - 2 * GRID_PADDING;
let range = [gridHeight + GRID_PADDING, GRID_PADDING];
if (grid.type === 'continuous') {
let scale;
const values = this.getDataByIndicator(grid).map(v => +v);
const extent = d3.extent(values) as [number, number];
if (grid.scale === ScaleMethod.LOGARITHMIC) {
scale = d3.scaleLog();
} else if (grid.scale === ScaleMethod.QUANTILE) {
const kNumQuantiles = 20;
scale = d3.scaleQuantile();
range = d3.range(kNumQuantiles).map(i => range[0] - (i * gridHeight) / (kNumQuantiles - 1));
} else {
scale = d3.scaleLinear();
}
return (scale.domain(extent) as d3.ScaleLinear<number, number>).range(range) as YScale;
}
return d3
.scalePoint()
.domain((grid.selectedValues as string[]) ?? [])
.range(range)
.padding(0.1) as YScale;
});
this.grids.forEach((grid, i) => {
grid.yScale = yScales[i];
});
}
private getDiscreteLineBySelection(index: number, [y1, y2]: [number, number]) {
const indicator = this.grids[index];
const scale = indicator.yScale as d3.ScalePoint<string | number>;
const domain = scale.domain();
const step = scale.step();
const padding = scale.padding() * step;
const start = domain.length - Math.min(Math.floor((y2 - GRID_PADDING - padding) / step), domain.length - 1) - 1;
const end = domain.length - Math.max(Math.ceil((y1 - GRID_PADDING - padding) / step), 0) - 1;
return this.data.reduce<number[]>((result, row, i) => {
const vi = domain.indexOf(row[indicator.group][indicator.name]);
if (vi >= start && vi <= end) {
result.push(i);
}
return result;
}, []);
}
private getContinuousLineBySelection(index: number, [y1, y2]: [number, number]) {
const indicator = this.grids[index];
const scale = indicator.yScale;
let start: number;
let end: number;
if (indicator.scale === ScaleMethod.QUANTILE) {
const quantileScale = scale as d3.ScaleQuantile<number>;
const range = quantileScale.range();
const domains = range
.filter(y => y1 <= y && y <= y2)
.map(y => {
const domain = quantileScale.invertExtent(y);
return y === range[range.length - 1] ? [domain[0], domain[1] + 1] : domain;
});
[start, end] = d3.extent(d3.merge<number>(domains)) as [number, number];
} else {
const invertScale = (scale as d3.ScaleLogarithmic<number, number> | d3.ScaleLinear<number, number>).invert;
start = invertScale(y1);
end = invertScale(y2);
}
if (start > end) {
[start, end] = [end, start];
}
return this.data.reduce<number[]>((result, row, i) => {
const v = row[indicator.group][indicator.name] as number;
if (v >= start && v <= end) {
result.push(i);
}
return result;
}, []);
}
private brushed(index: number, selection: [number, number] | null) {
const indicator = this.sequenceGrids[index];
if (selection == null) {
this.brushedLineIndexesArray[index] = null;
} else {
if (indicator.type !== 'continuous') {
this.brushedLineIndexesArray[index] = this.getDiscreteLineBySelection(index, selection);
} else {
this.brushedLineIndexesArray[index] = this.getContinuousLineBySelection(index, selection);
}
}
this.updateLineColors();
}
private dragstart(indicator: GridIndicator, x: number) {
this.draggingIndicator = {...indicator};
this.dragStartX = x;
indicator.grid?.classed('dragging', true);
}
private dragging(indicator: GridIndicator, x: number) {
if (this.draggingIndicator) {
const dx = x - this.dragStartX;
const newX = Math.min(
this.svgWidth - this.columnWidth + ParallelCoordinatesGraph.GRID_BRUSH_WIDTH / 2,
Math.max(0, this.draggingIndicator.x + dx)
);
indicator.x = newX;
this.calculateXScale();
this.sequenceGrids.forEach(({grid, name, x}) =>
grid?.attr('transform', `translate(${name === indicator.name ? newX : x}, 0)`)
);
this.updateLines(false, {indicator: indicator.name, x: newX});
this.emit(
'dragging',
indicator.name,
newX - indicator.x,
this.sequenceGrids.map(({name}) => name)
);
}
}
private dragend() {
this.draggingIndicator?.grid?.classed('dragging', false);
this.draggingIndicator = null;
this.updateGrids();
this.updateLines();
this.emit(
'dragged',
this.sequenceGrids.map(({name}) => name)
);
}
private updateGrids(animation = true) {
this.sequenceGrids.forEach(({grid, x}) =>
this.select(grid, animation)?.attr('transform', `translate(${x}, 0)`)
);
}
private updateLines(animation = true, extra?: {indicator: string; x: number}) {
this.lines.forEach(g => {
const circles = g.line?.selectAll('.select-indicator').nodes() ?? [];
const line = d3.line()(
this.sequenceGrids.map(({group, name, x: gx, yScale}, i) => {
let x = gx;
const y = yScale(g.data[group][name] as number) ?? 0;
if (extra && extra.indicator === name) {
x = extra.x;
}
if (circles[i]) {
this.select(d3.select(circles[i]), animation)?.attr('cx', x).attr('cy', y);
}
return [x, y];
})
);
this.select(g.line?.selectAll('path'), animation)?.attr('d', line ?? '');
});
}
private select<T extends d3.BaseType, G extends d3.BaseType>(
selection: d3.Selection<T, unknown, G, unknown> | null | undefined,
animation = false
) {
if (animation) {
return selection?.transition().duration(75);
}
return selection;
}
private setSvgSize() {
const width = this.svgWidth;
const height = ParallelCoordinatesGraph.GRAPH_HEIGHT + INDICATORS_HEIGHT;
this.svg.attr('width', width).attr('height', height).attr('viewBox', `0 0 ${width} ${height}`);
}
resize(containerWidth: number) {
this.containerWidth = containerWidth;
this.setSvgSize();
this.calculateXScale();
this.updateGrids(false);
this.updateLines(false);
}
setColors(colors: string[]) {
this.colors = colors;
this.calculateLineColors();
this.updateLineColors();
}
setScaleMethod(name: string, scaleMethod: ScaleMethod) {
const indicator = this.grids.find(grid => grid.name === name);
if (indicator) {
indicator.scale = scaleMethod;
this.removeGrids();
this.calculateYScales();
this.drawGrids();
this.updateLines();
}
}
render(indicators: Indicator[], data: DataListItem[]) {
if (indicators.length !== this.grids.length) {
this.unselectLine();
} else {
for (const newIdi of indicators) {
const oldIdi = this.grids.find(g => g.name === newIdi.name);
if (!oldIdi || oldIdi.group !== newIdi.group || oldIdi.type !== newIdi.type) {
this.unselectLine();
break;
}
}
}
this.unhoverLine();
this.removeLines();
this.removeGrids();
this.grids = indicators.map(indicator => ({
...indicator,
scale: ScaleMethod.LINEAR,
x: 0,
yScale: d3.scaleLinear(),
grid: null
}));
this.data = data;
this.setSvgSize();
this.calculateXScale();
this.calculateYScales();
this.lines = data.map(row => {
return {
data: row,
color: '',
line: null
};
});
this.calculateLineColors();
this.drawLines();
this.drawGrids();
}
dispose() {
this.removeLines();
this.removeGrids();
this.svg.remove();
}
}
/**
* Copyright 2020 Baidu Inc. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import Component from './Component';
export default Component;
/**
* Copyright 2020 Baidu Inc. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React, {FunctionComponent, useState} from 'react';
import ColorMap from '~/components/HyperParameterPage/ColorMap';
import ParallelCoordinatesGraph from './ParallelCoordinatesGraph';
import SessionTable from '~/components/HyperParameterPage/SessionTable';
import View from '~/components/HyperParameterPage/View';
import type {ViewData} from '~/resource/hyper-parameter';
import {rem} from '~/utils/style';
import styled from 'styled-components';
import {useGraph} from '~/resource/hyper-parameter';
const ParallelCoordinatesContainer = styled.div`
width: 100%;
display: flex;
font-size: ${rem(12)};
align-items: stretch;
justify-content: space-between;
> .graph {
flex: auto;
}
> .color-map {
flex: none;
}
`;
type ParallelCoordinatesViewProps = ViewData;
const ParallelCoordinatesView: FunctionComponent<ParallelCoordinatesViewProps> = ({indicators, list, data}) => {
const {selectedIndicators, sessionData, onHover, onSelect, showMetricsGraph} = useGraph(indicators, list);
const [colors, setColors] = useState<string[]>([]);
return (
<>
<View>
<ParallelCoordinatesContainer>
<ParallelCoordinatesGraph
className="graph"
indicators={selectedIndicators}
list={list}
data={data}
colors={colors}
onHover={onHover}
onSelect={onSelect}
/>
<ColorMap className="color-map" indicators={indicators} data={data} onChange={setColors} />
</ParallelCoordinatesContainer>
</View>
<SessionTable indicators={indicators} data={sessionData} showMetricsGraph={showMetricsGraph} />
</>
);
};
export default ParallelCoordinatesView;
/**
* Copyright 2020 Baidu Inc. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import ParallelCoordinatesView from './ParallelCoordinatesView';
export default ParallelCoordinatesView;
/**
* Copyright 2020 Baidu Inc. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React, {FunctionComponent, useCallback, useMemo} from 'react';
import {calculateRelativeTime, chartData} from '~/resource/hyper-parameter';
import {formatTime, humanizeDuration} from '~/utils';
import type {MetricData} from '~/resource/hyper-parameter';
import type {Run} from '~/types';
import SChart from '~/components/ScalarChart';
import {format} from 'd3-format';
import queryString from 'query-string';
import styled from 'styled-components';
import useRequest from '~/hooks/useRequest';
import {useTranslation} from 'react-i18next';
const valueFormatter = format('.5');
const Error = styled.div`
width: 100%;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
`;
interface ScalarChartProps {
metric: string;
run: Run;
}
const ScalarChart: FunctionComponent<ScalarChartProps> = ({metric, run}) => {
const {t, i18n} = useTranslation(['hyper-parameter', 'common']);
const {data, error, loading} = useRequest<MetricData[]>(
queryString.stringifyUrl({url: '/hparams/metric', query: {run: run.label, metric}})
);
const dataWithRelativeTime = useMemo(() => calculateRelativeTime(data ?? []), [data]);
const series = useMemo(() => chartData(dataWithRelativeTime, run), [dataWithRelativeTime, run]);
const getTooltipTableData = useCallback(
(series: number[]) => {
return {
runs: [run],
columns: [
{
label: t('common:scalar-value'),
width: '4.285714286em'
},
{
label: t('common:time-mode.step'),
width: '2.857142857em'
},
{
label: t('common:time-mode.wall'),
width: '10.714285714em'
},
{
label: t('common:time-mode.relative'),
width: '4.285714286em'
}
],
data: [
[
valueFormatter(series[2] ?? Number.NaN),
series[1],
formatTime(series[0], i18n.language),
humanizeDuration(series[3])
]
]
};
},
[i18n.language, run, t]
);
if (!data && error) {
return <Error>{t('common:error')}</Error>;
}
return <SChart title={metric} data={series} loading={loading} getTooltipTableData={getTooltipTableData} />;
};
export default ScalarChart;
/**
* Copyright 2020 Baidu Inc. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React, {FunctionComponent, useMemo} from 'react';
import {SCALE_METHODS, ScaleMethod} from '~/resource/hyper-parameter';
import Select from '~/components/HyperParameterPage/BorderLessSelect';
import type {SelectProps} from '~/components/Select';
import type {WithStyled} from '~/utils/style';
import styled from 'styled-components';
import {useTranslation} from 'react-i18next';
const StyledSelect = styled(Select)<React.FunctionComponent<SelectProps<ScaleMethod>>>`
min-width: 7em;
`;
interface ScaleMethodSelectProps {
scaleMethod: ScaleMethod;
direction?: 'bottom' | 'top';
onChange?: (scaleMethod: ScaleMethod) => unknown;
}
const ScaleMethodSelect: FunctionComponent<ScaleMethodSelectProps & WithStyled> = ({
scaleMethod,
direction,
onChange
}) => {
const {t} = useTranslation('hyper-parameter');
const scaleMethods = useMemo(
() =>
SCALE_METHODS.map(method => ({
value: method,
label: t(`hyper-parameter:scale-method.${method}`)
})),
[t]
);
return (
<StyledSelect
direction={direction}
list={scaleMethods}
value={scaleMethod}
onChange={(scale: string) => onChange?.(scale as ScaleMethod)}
/>
);
};
export default ScaleMethodSelect;
/**
* Copyright 2020 Baidu Inc. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type {IndicatorType, ScaleMethod} from '~/resource/hyper-parameter';
import type {Options, Point} from './ScatterChart';
import React, {FunctionComponent, useEffect, useRef} from 'react';
import Chart from './ScatterChart';
import styled from 'styled-components';
const Container = styled.div`
svg {
.x-axis,
.y-axis {
color: var(--hyper-parameter-graph-axis-color);
}
.x-grid,
.y-grid {
color: var(--hyper-parameter-graph-grid-color);
}
.x-label,
.y-label {
color: var(--text-lighter-color);
}
.dots {
pointer-events: none;
.disabled {
fill: var(--hyper-parameter-graph-disabled-data-color);
}
}
.hover-dots circle {
cursor: pointer;
&.disabled {
cursor: default;
pointer-events: none;
}
}
.grid-brush .selection {
fill: var(--hyper-parameter-graph-brush-color);
fill-opacity: 0.4;
}
}
`;
export interface Data {
type: [IndicatorType, IndicatorType];
data: Point[];
}
interface ScatterChartProps {
data: Data;
colors?: string[];
scaleMethods?: [ScaleMethod | null, ScaleMethod | null];
options?: Options;
hover?: number | null;
select?: number | null;
brush?: number[] | null;
onHover?: (index: number | null) => unknown;
onSelect?: (index: number | null) => unknown;
onBrush?: (indexes: number[] | null) => unknown;
}
const ScatterChart: FunctionComponent<ScatterChartProps> = ({
data,
colors,
scaleMethods,
options,
hover,
select,
brush,
onHover,
onSelect,
onBrush
}) => {
const optionsRef = useRef(options);
const chart = useRef<Chart>();
const container = useRef<HTMLDivElement>(null);
useEffect(() => {
if (container.current) {
chart.current = new Chart(container.current, optionsRef.current);
return () => {
chart.current?.dispose();
};
}
}, []);
useEffect(() => {
chart.current?.render(data.data, data.type);
}, [data]);
useEffect(() => {
chart.current?.setColors(colors ?? []);
}, [colors]);
useEffect(() => {
if (scaleMethods) {
chart.current?.setScaleMethod(scaleMethods);
}
}, [scaleMethods]);
useEffect(() => {
if (hover !== undefined) {
chart.current?.hover(hover);
}
}, [hover]);
useEffect(() => {
if (select !== undefined) {
chart.current?.select(select);
}
}, [select]);
useEffect(() => {
if (brush !== undefined) {
chart.current?.focus(brush);
}
}, [brush]);
useEffect(() => {
if (onHover) {
chart.current?.on('hover', onHover);
return () => {
chart.current?.off('hover', onHover);
};
}
}, [onHover]);
useEffect(() => {
if (onSelect) {
chart.current?.on('select', onSelect);
return () => {
chart.current?.off('select', onSelect);
};
}
}, [onSelect]);
useEffect(() => {
if (onBrush) {
chart.current?.on('brush', onBrush);
return () => {
chart.current?.off('brush', onBrush);
};
}
}, [onBrush]);
return <Container ref={container}></Container>;
};
export default ScatterChart;
/**
* Copyright 2020 Baidu Inc. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
// cspell:words quantile quantiles
import * as d3 from 'd3';
import EventEmitter from 'eventemitter3';
import type {IndicatorType} from '~/resource/hyper-parameter';
import {ScaleMethod} from '~/resource/hyper-parameter';
import intersection from 'lodash/intersection';
type Scale =
| d3.ScalePoint<string | number>
| d3.ScaleLinear<number, number>
| d3.ScaleLogarithmic<number, number>
| d3.ScaleQuantile<number>;
interface ScatterChartOptions {
width: number;
height: number;
colors: string[];
labelVisible: [boolean, boolean];
}
export type Point = [string | number, string | number];
export type Options = Partial<ScatterChartOptions>;
const DOT_RADIUS = 2;
const DOT_RADIUS_HOVER = 4;
const DOT_RADIUS_SELECT = 5;
const DEFAULT_COLOR = '#000';
const LOGARITHMIC_TICKS = 2;
interface EventTypes {
hover: [number | null];
select: [number | null];
brush: [number[] | null];
}
export default class ScatterChart extends EventEmitter<EventTypes> {
static MARGIN_WITHOUT_LABEL = 8;
static MARGIN_LEFT_WITH_LABEL = 50;
static MARGIN_BOTTOM_WITH_LABEL = 30;
protected container: HTMLElement;
protected width: number;
protected height: number;
protected labelVisible = [true, true];
private scale: [Scale, Scale] = [d3.scaleLinear(), d3.scaleLinear()];
private axis: [d3.Axis<string | number>, d3.Axis<string | number>] = [
d3.axisTop(d3.scaleLinear() as d3.AxisScale<string | number>),
d3.axisRight(d3.scaleLinear() as d3.AxisScale<string | number>)
];
private grid: [d3.Axis<string | number>, d3.Axis<string | number>] = [
d3.axisTop(d3.scaleLinear() as d3.AxisScale<string | number>),
d3.axisRight(d3.scaleLinear() as d3.AxisScale<string | number>)
];
private label: [d3.Axis<string | number>, d3.Axis<string | number>] = [
d3.axisBottom(d3.scaleLinear() as d3.AxisScale<string | number>),
d3.axisLeft(d3.scaleLinear() as d3.AxisScale<string | number>)
];
private svg;
private brush: d3.BrushBehavior<unknown> = d3.brush();
private dots: d3.Selection<SVGCircleElement, unknown, null, undefined>[] = [];
private hoverDots: d3.Selection<SVGCircleElement, unknown, null, undefined>[] = [];
protected data: Point[] = [];
protected type: [IndicatorType, IndicatorType] = ['continuous', 'continuous'];
protected colors: string[] = [];
protected scaleMethod: [ScaleMethod | null, ScaleMethod | null] = [null, null];
private hoveredIndex: number | null = null;
private selectedIndex: number | null = null;
private brushedIndexes: number[] | null = null;
private brushing = false;
get margin() {
const {MARGIN_WITHOUT_LABEL, MARGIN_LEFT_WITH_LABEL, MARGIN_BOTTOM_WITH_LABEL} = ScatterChart;
return {
top: MARGIN_WITHOUT_LABEL,
right: MARGIN_WITHOUT_LABEL,
left: this.labelVisible[1] ? MARGIN_LEFT_WITH_LABEL : MARGIN_WITHOUT_LABEL,
bottom: this.labelVisible[0] ? MARGIN_BOTTOM_WITH_LABEL : MARGIN_WITHOUT_LABEL
};
}
get ticks() {
return [
this.scaleMethod[0] === ScaleMethod.LOGARITHMIC ? LOGARITHMIC_TICKS : this.width / 30,
this.scaleMethod[1] === ScaleMethod.LOGARITHMIC ? LOGARITHMIC_TICKS : this.height / 20
].map(Math.floor);
}
constructor(container: HTMLElement, options?: Options) {
super();
this.container = container;
this.width = options?.width ?? 150;
this.height = options?.height ?? 150;
this.colors = options?.colors ?? [];
this.labelVisible = options?.labelVisible ?? [true, true];
const {left, right, top, bottom} = this.margin;
const width = this.width + left + right;
const height = this.height + top + bottom;
this.svg = d3
.select(container)
.append('svg')
.attr('width', width)
.attr('height', height)
.attr('viewBox', `0 0 ${width} ${height}`)
.on('click', () => this.select(null));
this.init();
}
private init() {
this.svg
.append('g')
.classed('axises', true)
// grids at bottom
.call(g => g.append('g').classed('x-grid', true))
.call(g => g.append('g').classed('y-grid', true))
.call(g => g.append('g').classed('x-axis', true))
.call(g => g.append('g').classed('y-axis', true))
.call(g => g.append('g').classed('x-label', true))
.call(g => g.append('g').classed('y-label', true));
const {left, top} = this.margin;
this.brush = d3
.brush()
.extent([
[left - 0.5, top - 0.5],
[this.width + left + 0.5, this.height + top + 0.5]
])
.on('start', () => (this.brushing = true))
.on('brush', ({selection}) => this.brushed(selection))
.on('end', ({selection}) => {
this.brushed(selection);
this.brushing = false;
});
this.svg
.call(g => g.append('g').classed('dots', true))
.call(g => g.append('g').classed('brush', true).call(this.brush))
.call(g => g.append('g').classed('select-dots', true))
.call(g => g.append('g').classed('hover-dots', true));
}
private createScale(type: IndicatorType, scaleMethod: ScaleMethod | null) {
if (type === 'continuous') {
if (scaleMethod === ScaleMethod.QUANTILE) {
return d3.scaleQuantile();
}
if (scaleMethod === ScaleMethod.LOGARITHMIC) {
return d3.scaleLog();
}
return d3.scaleLinear();
}
return d3.scalePoint<string | number>().padding(0.5);
}
private scaleDots() {
this.dots.forEach((dot, i) => {
dot.attr('r', i === this.hoveredIndex || i === this.selectedIndex ? DOT_RADIUS_HOVER : DOT_RADIUS);
});
}
private colorizeDots() {
this.dots.forEach((dot, i) => {
const disabled = this.brushedIndexes != null && !this.brushedIndexes.includes(i);
dot.classed('disabled', disabled).attr('stroke', 'none');
if (disabled) {
dot.attr('fill', null);
} else {
dot.attr('fill', this.colors[i] ?? DEFAULT_COLOR);
}
});
this.hoverDots.forEach((dot, i) =>
dot.classed('disabled', this.brushedIndexes != null && !this.brushedIndexes.includes(i))
);
if (this.selectedIndex != null) {
this.svg.selectAll('.select-dots circle').attr('stroke', this.colors[this.selectedIndex] ?? DEFAULT_COLOR);
}
}
private drawSelectedDot() {
const selectDots = this.svg.select<SVGGElement>('.select-dots');
selectDots.selectAll('*').remove();
this.dots.forEach((dot, i) => {
if (i === this.selectedIndex) {
const x = dot.attr('cx');
const y = dot.attr('cy');
const color = dot.attr('fill');
selectDots
.append('circle')
.attr('cx', x)
.attr('cy', y)
.attr('r', DOT_RADIUS_SELECT)
.attr('fill', 'none')
.attr('stroke', color)
.attr('stroke-width', 1);
}
});
}
private draw() {
this.scale = this.scale.map((_, i) => this.createScale(this.type[i], this.scaleMethod[i])) as [Scale, Scale];
const {left, top} = this.margin;
const domain = this.type.map((type, i) => {
const values = this.data.map(r => r[i] as number);
if (type !== 'continuous') {
return [...new Set(values)];
}
return d3.extent(values);
});
const range = [
[left, this.width + left],
[this.height + top, top]
];
this.scaleMethod.forEach((scaleMethod, i) => {
if (scaleMethod === ScaleMethod.QUANTILE) {
const ticks = this.ticks[i];
const step = (range[i][1] - range[i][0]) / (ticks - 1);
range[i] = d3.range(ticks).map(j => range[i][0] + step * j);
}
});
this.scale.forEach((scale, i) => {
(scale.domain(domain[i]) as Scale).range(range[i]);
});
[this.axis, this.grid, this.label].forEach(axises => {
axises.forEach((axis, i) => {
axis.scale(this.scale[i] as d3.AxisScale<number | string>);
if (this.scaleMethod[i] === ScaleMethod.QUANTILE) {
(axis as d3.Axis<number>)
.tickValues((this.scale[i] as d3.ScaleQuantile<number>).quantiles())
.tickFormat(d3.format('-.2g'));
} else {
axis.tickValues(null).tickFormat(null);
if (this.type[i] === 'continuous') {
axis.ticks(this.ticks[i]);
}
}
});
});
this.grid[0].tickSize(this.height);
this.grid[1].tickSize(this.width);
const axisGroup = [this.svg.select<SVGGElement>('.x-axis'), this.svg.select<SVGGElement>('.y-axis')];
const gridGroup = [this.svg.select<SVGGElement>('.x-grid'), this.svg.select<SVGGElement>('.y-grid')];
const labelGroup = [this.svg.select<SVGGElement>('.x-label'), this.svg.select<SVGGElement>('.y-label')];
[axisGroup, gridGroup, labelGroup].forEach(group => {
group[0].attr('transform', `translate(0, ${this.height + top})`).call(g => g.selectAll('*').remove());
group[1].attr('transform', `translate(${left}, 0)`).call(g => g.selectAll('*').remove());
});
axisGroup.forEach((group, i) => group.call(this.axis[i]).selectAll('.tick text').remove());
gridGroup.forEach((group, i) =>
group
.call(this.grid[i])
.call(g => g.selectAll('.tick text').remove())
.call(g => g.select('.domain').remove())
);
labelGroup.forEach((group, i) => {
if (this.labelVisible[i]) {
group
.call(this.label[i])
.call(g => g.selectAll('.tick line').remove())
.call(g => g.select('.domain').remove());
}
});
const dots = this.svg.select<SVGGElement>('.dots');
const hoverDots = this.svg.select<SVGGElement>('.hover-dots');
const selectDots = this.svg.select<SVGGElement>('.select-dots');
dots.selectAll('*').remove();
hoverDots.selectAll('*').remove();
selectDots.selectAll('*').remove();
this.dots = [];
this.hoverDots = [];
this.data.forEach((item, i) => {
const x = this.scale[0](item[0] as number) ?? 0;
const y = this.scale[1](item[1] as number) ?? 0;
const dot = dots
.append('circle')
.attr('cx', x)
.attr('cy', y)
.attr('r', DOT_RADIUS)
.attr('fill', this.colors[i] ?? DEFAULT_COLOR);
const hoverDot = hoverDots
.append('circle')
.attr('cx', x)
.attr('cy', y)
.attr('r', DOT_RADIUS_HOVER)
.attr('fill', 'transparent')
.on('mouseenter', () => {
if (dot.classed('disabled') || this.brushing) {
return;
}
this.hover(i);
})
.on('mouseleave', () => {
if (dot.classed('disabled') || this.brushing) {
return;
}
this.hover(null);
})
.on('click', (e: Event) => {
if (dot.classed('disabled') || this.brushing) {
return;
}
this.select(i);
e.stopPropagation();
});
this.dots.push(dot);
this.hoverDots.push(hoverDot);
});
}
private dataInSelection(axisIndex: number, [a, b]: [number, number]) {
const axis = axisIndex === 1 ? 'y' : 'x';
const scaleMethod = this.scaleMethod[axisIndex];
const scale = this.scale[axisIndex];
const type = this.type[axisIndex];
const [f, t] = a < b ? [a, b] : [b, a];
if (type === 'continuous') {
let start: number;
let end: number;
if (scaleMethod === ScaleMethod.QUANTILE) {
const quantileScale = scale as d3.ScaleQuantile<number>;
const range = quantileScale.range();
const domains = range
.filter(v => f <= v && v <= t)
.map(v => {
const domain = quantileScale.invertExtent(v);
return v === range[range.length - 1] ? [domain[0], domain[1] + 1] : domain;
});
[start, end] = d3.extent(d3.merge<number>(domains)) as [number, number];
} else {
const invertScale = (scale as d3.ScaleLogarithmic<number, number> | d3.ScaleLinear<number, number>)
.invert;
start = invertScale(f);
end = invertScale(t);
}
if (axis === 'y') {
[start, end] = [end, start];
}
return this.data.reduce<number[]>((result, item, index) => {
if (item[axisIndex] >= start && item[axisIndex] <= end) {
result.push(index);
}
return result;
}, []);
} else {
const pointScale = scale as d3.ScalePoint<string | number>;
const domain = pointScale.domain();
const step = pointScale.step();
const padding = pointScale.padding() * step;
const margin = axis === 'x' ? this.margin.left : this.margin.top;
let s = (f - margin - padding) / step;
let e = (t - margin - padding) / step;
if (axis === 'y') {
[s, e] = [e, s];
}
let start: number;
let end: number;
if (axis === 'x') {
start = Math.max(Math.floor(s) + 1, 0);
end = Math.min(Math.ceil(e) - 1, domain.length - 1);
} else {
start = domain.length - Math.min(Math.floor(s), domain.length - 1) - 1;
end = domain.length - Math.max(Math.ceil(e), 0) - 1;
}
return this.data.reduce<number[]>((result, item, index) => {
const vi = domain.indexOf(item[axisIndex]);
if (vi >= start && vi <= end) {
result.push(index);
}
return result;
}, []);
}
}
private brushed(selection: [[number, number], [number, number]] | null) {
this.select(null);
let indexes: number[] | null = null;
if (selection) {
indexes = intersection(
...selection.map((_, i) => this.dataInSelection(i, [selection[0][i], selection[1][i]]))
);
}
this.clearableBrush(indexes);
this.emit('brush', indexes);
}
private clearableBrush(indexes: number[] | null, clearBrush = false) {
if (clearBrush) {
this.brush.clear(this.svg.select('.brush'));
}
this.brushedIndexes = indexes;
this.colorizeDots();
}
focus(indexes: number[] | null) {
this.clearableBrush(indexes, true);
}
hover(index: number | null) {
if (index === this.hoveredIndex) {
return;
}
this.hoveredIndex = index;
this.scaleDots();
this.emit('hover', index);
}
select(index: number | null) {
this.selectedIndex = index;
this.scaleDots();
this.drawSelectedDot();
this.emit('select', index);
}
setColors(colors: string[]) {
this.colors = colors;
this.colorizeDots();
this.drawSelectedDot();
}
setScaleMethod(scaleMethod: [ScaleMethod | null, ScaleMethod | null]) {
this.scaleMethod = scaleMethod;
this.draw();
this.colorizeDots();
this.scaleDots();
this.drawSelectedDot();
}
render(data: Point[], type: [IndicatorType, IndicatorType]) {
this.hoveredIndex = null;
this.selectedIndex = null;
this.brushedIndexes = [];
this.brush.clear(this.svg.select('.brush'));
this.data = data;
this.type = type;
this.colors = [];
this.draw();
}
dispose() {
this.svg.remove();
}
}
/**
* Copyright 2020 Baidu Inc. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import Component from './Component';
export default Component;
/**
* Copyright 2020 Baidu Inc. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
// cspell:words onhover
import type {Indicator, IndicatorType, ViewData} from '~/resource/hyper-parameter';
import React, {FunctionComponent, useCallback, useMemo, useRef, useState} from 'react';
import ChartClass from './ScatterChart/ScatterChart';
import {ScaleMethod} from '~/resource/hyper-parameter';
import ScaleMethodSelect from '~/components/HyperParameterPage/ScaleMethodSelect';
import ScatterChart from './ScatterChart';
import type {WithStyled} from '~/utils/style';
import {rem} from '~/utils/style';
import styled from 'styled-components';
type ScatterPlotMatrixProps = ViewData & {
colors: string[];
onHover?: (index: number | null) => unknown;
onSelect?: (index: number | null) => unknown;
};
const Container = styled.div`
overflow-x: auto;
/* this is not allowed, so we change our dropdown list popup direction to top */
/* overflow-y: visible; */
display: flex;
flex-direction: column;
.row {
display: flex;
flex-direction: row;
}
.metrics {
display: block;
writing-mode: vertical-rl;
text-align: center;
font-weight: 700;
width: ${rem(14)};
line-height: ${rem(14)};
}
.hparams {
display: block;
width: ${150 + ChartClass.MARGIN_WITHOUT_LABEL * 2}px;
text-align: center;
flex: none;
}
.metrics.hparams {
width: calc(${rem(14)} + ${ChartClass.MARGIN_LEFT_WITH_LABEL - ChartClass.MARGIN_WITHOUT_LABEL}px);
}
.row-scale-method-selector {
margin: ${rem(10)} 0 ${rem(10)} ${rem(24)};
height: 1em;
}
.column-scale-method-selector {
margin-top: ${rem(10)};
}
`;
const ScatterPlotMatrix: FunctionComponent<ScatterPlotMatrixProps & WithStyled> = ({
indicators,
data,
colors,
onHover,
onSelect
}) => {
const options = useRef({colors});
const metricsIndicators = useMemo(() => indicators.filter(i => i.group === 'metrics'), [indicators]);
const hparamsIndicators = useMemo(() => indicators.filter(i => i.group === 'hparams'), [indicators]);
const [hover, setHover] = useState<number | null>(null);
const [select, setSelect] = useState<number | null>(null);
const [brush, setBrush] = useState<number[] | null>(null);
const [brushedChart, setBrushedChart] = useState<[number, number] | null>(null);
const onhover = useCallback(
(index: number | null) => {
setHover(index);
onHover?.(index);
},
[onHover]
);
const onselect = useCallback(
(index: number | null) => {
setSelect(index);
onSelect?.(index);
},
[onSelect]
);
const onBrush = useCallback((row: number, column: number, indexes: number[] | null) => {
setBrush(indexes);
setBrushedChart(indexes != null ? [row, column] : null);
}, []);
const getBrushValue = useCallback(
(row: number, column: number) =>
brushedChart && brushedChart[0] === row && brushedChart[1] === column ? undefined : brush,
[brush, brushedChart]
);
const [scaleMethods, setScaleMethods] = useState<WeakMap<Indicator, ScaleMethod>>(new WeakMap());
const metricsScaleMethods = useMemo(
() =>
metricsIndicators.map(indicator =>
indicator.type === 'continuous' ? scaleMethods.get(indicator) ?? ScaleMethod.LINEAR : null
),
[metricsIndicators, scaleMethods]
);
const hparamsScaleMethods = useMemo(
() =>
hparamsIndicators.map(indicator =>
indicator.type === 'continuous' ? scaleMethods.get(indicator) ?? ScaleMethod.LINEAR : null
),
[hparamsIndicators, scaleMethods]
);
const changeScaleMethod = useCallback(
(indicator: Indicator, scaleMethod: ScaleMethod) => {
setScaleMethods(m => {
const n = new WeakMap();
indicators.forEach(idi => {
if (m.has(idi)) {
n.set(idi, m.get(idi));
}
});
n.set(indicator, scaleMethod);
return n;
});
},
[indicators]
);
const chartData = useMemo(
() =>
metricsIndicators.map(mi =>
hparamsIndicators.map(hi => ({
data: data.map(
row => [row[hi.group][hi.name], row[mi.group][mi.name]] as [string | number, string | number]
),
type: [hi.type, mi.type] as [IndicatorType, IndicatorType]
}))
),
[data, hparamsIndicators, metricsIndicators]
);
const matrixData = useMemo(
() =>
chartData.map((row, ri) =>
row.map((column, ci) => ({
data: column,
options: {
...options.current,
labelVisible: [ri === chartData.length - 1, ci === 0] as [boolean, boolean]
},
scaleMethods: [hparamsScaleMethods[ci], metricsScaleMethods[ri]] as [
ScaleMethod | null,
ScaleMethod | null
]
}))
),
[chartData, hparamsScaleMethods, metricsScaleMethods]
);
return (
<Container>
{matrixData.map((row, ri) => (
<React.Fragment key={ri}>
<div className="row-scale-method-selector">
{metricsScaleMethods[ri] != null ? (
<ScaleMethodSelect
scaleMethod={metricsScaleMethods[ri] as ScaleMethod}
onChange={scaleMethod => changeScaleMethod(metricsIndicators[ri], scaleMethod)}
/>
) : null}
</div>
<div className="row">
<span className="metrics">{metricsIndicators[ri].name}</span>
{row.map((cell, ci) => (
<div className="cell" key={ci}>
<ScatterChart
{...cell}
colors={colors}
hover={hover}
select={select}
brush={getBrushValue(ri, ci)}
onHover={onhover}
onSelect={onselect}
onBrush={indexes => onBrush(ri, ci, indexes)}
/>
</div>
))}
</div>
</React.Fragment>
))}
<div className="row">
<span className="metrics hparams"></span>
{hparamsIndicators.map((hi, hii) => (
<div className="hparams" key={hii}>
<span>{hi.name}</span>
<div className="column-scale-method-selector">
{hparamsScaleMethods[hii] != null ? (
<ScaleMethodSelect
direction="top"
scaleMethod={hparamsScaleMethods[hii] as ScaleMethod}
onChange={scaleMethod => changeScaleMethod(hparamsIndicators[hii], scaleMethod)}
/>
) : null}
</div>
</div>
))}
</div>
</Container>
);
};
export default ScatterPlotMatrix;
/**
* Copyright 2020 Baidu Inc. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React, {FunctionComponent, useState} from 'react';
import ColorMap from '~/components/HyperParameterPage/ColorMap';
import ScatterPlotMatrix from './ScatterPlotMatrix';
import SessionTable from '~/components/HyperParameterPage/SessionTable';
import View from '~/components/HyperParameterPage/View';
import type {ViewData} from '~/resource/hyper-parameter';
import {rem} from '~/utils/style';
import styled from 'styled-components';
import {useGraph} from '~/resource/hyper-parameter';
const Wrapper = styled.div`
width: 100%;
display: flex;
font-size: ${rem(12)};
align-items: flex-start;
justify-content: space-between;
> .graph {
flex: auto;
}
> .color-map {
flex: none;
height: ${rem(180)};
}
`;
type ScatterPlotMatrixViewProps = ViewData;
const ScatterPlotMatrixView: FunctionComponent<ScatterPlotMatrixViewProps> = ({indicators, list, data}) => {
const {selectedIndicators, sessionData, onHover, onSelect, showMetricsGraph} = useGraph(indicators, list);
const [colors, setColors] = useState<string[]>([]);
return (
<>
<View>
<Wrapper>
<ScatterPlotMatrix
className="graph"
indicators={selectedIndicators}
data={data}
list={list}
colors={colors}
onHover={onHover}
onSelect={onSelect}
/>
<ColorMap className="color-map" indicators={indicators} data={data} onChange={setColors} />
</Wrapper>
</View>
<SessionTable indicators={indicators} data={sessionData} showMetricsGraph={showMetricsGraph} />
</>
);
};
export default ScatterPlotMatrixView;
/**
* Copyright 2020 Baidu Inc. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import ScatterPlotMatrixView from './ScatterPlotMatrixView';
export default ScatterPlotMatrixView;
/**
* Copyright 2020 Baidu Inc. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type {Indicator, ListItem} from '~/resource/hyper-parameter';
import React, {FunctionComponent, useMemo} from 'react';
import {Trans, useTranslation} from 'react-i18next';
import {borderRadius, rem} from '~/utils/style';
import Table from './TableView/Table';
import styled from 'styled-components';
const Wrapper = styled.div`
background-color: var(--background-color);
border-radius: ${borderRadius};
padding: ${rem(20)};
&:not(:first-child) {
margin-top: ${rem(20)};
}
`;
const Empty = styled.div`
min-height: ${rem(188)};
h3 {
font-size: ${rem(16)};
line-height: 1;
margin: 0 0 1em 0;
}
ol {
padding-left: 1.2em;
color: var(--text-light-color);
line-height: 1.5;
li:empty {
display: none;
}
}
`;
interface SessionTableProps {
indicators: Indicator[];
data: ListItem | null;
showMetricsGraph: boolean;
}
const SessionTable: FunctionComponent<SessionTableProps> = ({indicators, data, showMetricsGraph}) => {
useTranslation('hyper-parameter');
const dataList = useMemo(() => (data ? [data] : []), [data]);
return (
<Wrapper>
{data ? (
<Table indicators={indicators} list={dataList} data={dataList} expandAll={showMetricsGraph} />
) : (
<Empty>
<Trans i18nKey="hyper-parameter:session-table-empty">
<h3>Click or hover over a line to display its values here.</h3>
<ol>
<li>Hover to display values;</li>
<li>Click to display metrics graph.</li>
</ol>
</Trans>
</Empty>
)}
</Wrapper>
);
};
export default SessionTable;
/**
* Copyright 2020 Baidu Inc. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React, {FunctionComponent} from 'react';
import type {CellProps} from 'react-table';
const Cell = <D extends Record<string, unknown>>({cell}: CellProps<D>): ReturnType<FunctionComponent> => {
return <span>{cell.value}</span>;
};
export default Cell;
/**
* Copyright 2020 Baidu Inc. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React, {CSSProperties, FunctionComponent, useCallback, useEffect, useRef, useState} from 'react';
import {useDrag, useDrop} from 'react-dnd';
import {DND_TYPE} from '~/resource/hyper-parameter';
import {Dragger} from '~/components/Table';
import {Th} from '~/components/Table';
import type {WithStyled} from '~/utils/style';
interface DraggableHeaderProps {
style?: CSSProperties;
id?: string;
draggable?: boolean;
onDragStart?: (id: string) => unknown;
onDragEnd?: (id: string) => unknown;
onChangeDropSide?: (id: string, side: 'before' | 'after') => unknown;
onDrop?: (id: string, side: 'before' | 'after') => unknown;
}
interface DragItem {
id: string;
}
const DraggableHeader: FunctionComponent<DraggableHeaderProps & WithStyled> = ({
id,
draggable,
onDragStart,
onDragEnd,
onChangeDropSide,
onDrop,
className,
style,
children
}) => {
const ref = useRef<HTMLDivElement>(null);
const [{isDragging}, drag, preview] = useDrag(() => ({
type: DND_TYPE,
item: {
id: id ?? ''
},
options: {
dropEffect: 'move'
},
previewOptions: {
offsetX: 8,
offsetY: 25
},
collect: monitor => ({
isDragging: monitor.isDragging()
})
}));
const start = useRef(onDragStart);
useEffect(() => {
start.current = onDragStart;
}, [onDragStart]);
const end = useRef(onDragEnd);
useEffect(() => {
end.current = onDragEnd;
}, [onDragEnd]);
useEffect(() => {
if (isDragging) {
start.current?.(id ?? '');
} else {
end.current?.(id ?? '');
}
}, [id, isDragging]);
const [dropSide, setDropSide] = useState<'before' | 'after' | null>(null);
const dropFn = useCallback(() => {
if (dropSide) {
onDrop?.(id ?? '', dropSide);
}
}, [dropSide, id, onDrop]);
const dropFnRef = useRef(dropFn);
useEffect(() => {
dropFnRef.current = dropFn;
}, [dropFn]);
const [{handlerId}, drop] = useDrop(() => ({
accept: DND_TYPE,
collect: monitor => ({
handlerId: monitor.getHandlerId()
}),
canDrop: () => !!draggable,
hover: (item: DragItem, monitor) => {
if (!ref.current) {
return;
}
if (!monitor.canDrop()) {
return;
}
const rect = ref.current.getBoundingClientRect();
if (rect) {
const middle = (rect.right - rect.left) / 2;
const clientOffset = monitor.getClientOffset();
if (clientOffset) {
const offsetX = clientOffset.x - rect.left;
if (offsetX < middle) {
setDropSide('before');
} else {
setDropSide('after');
}
}
}
},
drop: (item: DragItem) => {
dropFnRef.current();
return item;
}
}));
const changeSide = useRef(onChangeDropSide);
useEffect(() => {
changeSide.current = onChangeDropSide;
}, [onChangeDropSide]);
useEffect(() => {
if (dropSide && id) {
changeSide.current?.(id, dropSide);
}
}, [dropSide, id]);
preview(drop(ref));
return (
<Th ref={ref} data-handler-id={handlerId} className={className} style={style}>
{draggable ? <Dragger ref={drag} /> : null}
{children}
</Th>
);
};
export default DraggableHeader;
/**
* Copyright 2020 Baidu Inc. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React, {FunctionComponent} from 'react';
import {Resizer} from '~/components/Table';
import type {HeaderProps as TableHeaderProps} from 'react-table';
import type {WithStyled} from '~/utils/style';
const Header: FunctionComponent<TableHeaderProps<Record<string, unknown>> & WithStyled> = ({
column,
className,
children
}) => {
return (
<>
<span className={className}>{children ?? column.id}</span>
{column.canResize ? (
<Resizer {...column.getResizerProps()} className={column.isResizing ? 'is-resizing' : ''} />
) : null}
</>
);
};
export default Header;
/**
* Copyright 2020 Baidu Inc. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React, {FunctionComponent} from 'react';
import Header from './Header';
import type {HeaderProps} from 'react-table';
import styled from 'styled-components';
const BoldHeader = styled(Header)`
font-weight: 700;
`;
const MetricsHeader: FunctionComponent<HeaderProps<Record<string, unknown>>> = props => {
return <BoldHeader {...props} />;
};
export default MetricsHeader;
/**
* Copyright 2020 Baidu Inc. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React, {FunctionComponent} from 'react';
import type {CellProps} from 'react-table';
import {Expander} from '~/components/Table';
import styled from 'styled-components';
const Cell = styled.span`
display: inline-flex;
align-items: center;
`;
const ExpandableCell = <D extends Record<string, unknown>>({
row,
cell
}: CellProps<D>): ReturnType<FunctionComponent> => {
return (
<Cell>
<Expander {...row.getToggleRowExpandedProps()} isExpanded={row.isExpanded} />
<span>{cell.value}</span>
</Cell>
);
};
export default ExpandableCell;
/**
* Copyright 2020 Baidu Inc. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React, {FunctionComponent} from 'react';
import Header from './Header';
import type {HeaderProps} from 'react-table';
import styled from 'styled-components';
import {useTranslation} from 'react-i18next';
const StyledHeader = styled(Header)`
padding-left: 2em;
`;
const NameHeader: FunctionComponent<HeaderProps<Record<string, unknown>>> = props => {
const {t} = useTranslation('hyper-parameter');
return <StyledHeader {...props}>{t('hyper-parameter:trial-id')}</StyledHeader>;
};
export default NameHeader;
/**
* Copyright 2020 Baidu Inc. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type {Column, Row, SortingRule, TableKeyedProps} from 'react-table';
import {ExpandContainer, TBody, THead, Table, Td, Tr} from '~/components/Table';
import type {IndicatorGroup, ViewData} from '~/resource/hyper-parameter';
import React, {FunctionComponent, useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState} from 'react';
import {color, colorAlt} from '~/utils/chart';
import {useColumnOrder, useExpanded, useFlexLayout, useResizeColumns, useSortBy, useTable} from 'react-table';
import Cell from './Cell';
import {DndProvider} from 'react-dnd';
import DraggableTh from './DraggableTh';
import {HTML5Backend} from 'react-dnd-html5-backend';
import Header from './Header';
import MetricGraphs from '~/components/HyperParameterPage/MetricGraphs';
import type {MetricGraphsProps} from '~/components/HyperParameterPage/MetricGraphs';
import MetricsHeader from './MetricsHeader';
import NameCell from './NameCell';
import NameHeader from './NameHeader';
import classNames from 'classnames';
import useClassNames from '~/hooks/useClassNames';
import {useSticky} from 'react-table-sticky';
type TableViewTableProps = ViewData & {
sortBy?: SortingRule<string>[];
expand?: boolean;
expandAll?: boolean;
};
type Data = TableViewTableProps['list'][number];
const TableViewTable: FunctionComponent<TableViewTableProps> = ({
indicators,
list: data,
sortBy,
expand,
expandAll
}) => {
const table = useRef<HTMLDivElement>(null);
const defaultColumn = useMemo(
() => ({
minWidth: 100,
draggable: true
}),
[]
);
const columns: Column<Data>[] = useMemo(
() => [
{
accessor: 'name',
Header: expand ? NameHeader : Header,
Cell: expand ? NameCell : Cell,
width: 200,
sticky: 'left',
draggable: false
},
...indicators.map(({name, group}) => ({
accessor: `${group}.${name}` as IndicatorGroup, // fix react-table's type error
id: name,
Header: group === 'metrics' ? MetricsHeader : Header,
minWidth: 200
}))
],
[expand, indicators]
);
const {
getTableProps,
headerGroups,
rows,
prepareRow,
setSortBy,
setColumnOrder,
state,
totalColumnsWidth,
columns: tableColumns,
allColumns
} = useTable(
{
columns,
data,
defaultColumn,
initialState: {
sortBy: sortBy ?? []
},
autoResetExpanded: false
},
useFlexLayout,
useSticky,
useResizeColumns,
useSortBy,
useColumnOrder,
useExpanded
);
useEffect(() => {
allColumns.forEach(column => {
const indicator = indicators.find(i => i.name === column.id);
if (indicator) {
column.toggleHidden(!indicator.selected);
}
});
}, [allColumns, indicators]);
useEffect(() => setSortBy(sortBy ?? []), [setSortBy, sortBy]);
const [draggingColumnId, setDraggingColumnId] = useState<string | null>(null);
const [droppableColumn, setDroppableColumn] = useState<[string, 'before' | 'after'] | null>(null);
const startDrag = useCallback((id: string) => setDraggingColumnId(id), []);
const stopDrag = useCallback(() => setDraggingColumnId(null), []);
const changeDropSide = useCallback((id: string, side: 'before' | 'after') => setDroppableColumn([id, side]), []);
const orderedColumnIds = useMemo(
() => (state.columnOrder.length ? state.columnOrder : tableColumns.map(c => c.id)),
[state.columnOrder, tableColumns]
);
const droppableColumnId = useMemo(() => {
if (draggingColumnId != null && droppableColumn != null) {
const [id, side] = droppableColumn;
const index = orderedColumnIds.findIndex(c => c === id);
if (side === 'before' && index > 0) {
return orderedColumnIds[index - 1];
} else if (side === 'after' && index < orderedColumnIds.length - 1) {
return orderedColumnIds[index];
}
}
return null;
}, [draggingColumnId, droppableColumn, orderedColumnIds]);
const isTableDroppableLeft = useMemo(
() =>
draggingColumnId != null &&
droppableColumn &&
droppableColumn[1] === 'before' &&
tableColumns[0]?.id === droppableColumn[0],
[draggingColumnId, droppableColumn, tableColumns]
);
const isTableDroppableRight = useMemo(
() =>
draggingColumnId != null &&
droppableColumn &&
droppableColumn[1] === 'after' &&
tableColumns[tableColumns.length - 1]?.id === droppableColumn[0],
[draggingColumnId, droppableColumn, tableColumns]
);
const drop = useCallback(
(id: string, side: 'before' | 'after') => {
if (draggingColumnId == null) {
return;
}
const ids = orderedColumnIds.filter(id => id !== draggingColumnId);
const originalIndex = orderedColumnIds.findIndex(id => id === draggingColumnId);
const index = ids.findIndex(c => c === id);
let insert: number | null = null;
if (index === -1) {
insert = originalIndex;
} else if (side === 'before') {
insert = index;
} else if (side === 'after') {
insert = index + 1;
}
if (insert != null) {
ids.splice(insert, 0, draggingColumnId);
setColumnOrder(ids);
}
},
[draggingColumnId, orderedColumnIds, setColumnOrder]
);
const [tableWidth, setTableWidth] = useState(0);
useLayoutEffect(() => {
const t = table.current;
if (t) {
const observer = new ResizeObserver(() => {
const rect = t.getBoundingClientRect();
setTableWidth(rect.width);
});
observer.observe(t);
return () => observer.unobserve(t);
}
}, []);
const tableClassNames = useClassNames(
'sticky',
{
'is-droppable-left': isTableDroppableLeft,
'is-droppable-right': isTableDroppableRight
},
[isTableDroppableLeft, isTableDroppableRight]
);
const getColumnProps = useCallback<(column: Column<Data>) => Partial<TableKeyedProps>>(
column => ({
className: classNames({
'is-sticky': !!column.sticky,
'is-resizing': state.columnResizing.isResizingColumn === column.id,
'is-dragging': draggingColumnId === column.id,
'is-droppable': droppableColumnId === column.id
}),
style: {
position: column.sticky ? 'sticky' : 'relative'
}
}),
[draggingColumnId, droppableColumnId, state.columnResizing.isResizingColumn]
);
const getGroupWidthProps = useCallback(() => {
if (totalColumnsWidth > tableWidth) {
return {
style: {
width: 'fit-content'
}
};
}
return {
style: {
width: 'auto'
}
};
}, [tableWidth, totalColumnsWidth]);
const getExpanderWidthProps = useCallback(() => {
if (totalColumnsWidth > tableWidth) {
return {
style: {
width: tableWidth - 2
}
};
}
return {
style: {
width: 'auto'
}
};
}, [tableWidth, totalColumnsWidth]);
const getRowMetricGraphsProps = useCallback<(row: Row<ViewData['list'][number]>) => MetricGraphsProps>(
({index, values}) => {
return {
metrics: indicators.filter(i => i.group === 'metrics' && i.selected).map(i => i.name),
run: {
label: values.name,
colors: [color[index % color.length], colorAlt[index % colorAlt.length]]
}
};
},
[indicators]
);
return (
<DndProvider backend={HTML5Backend}>
<Table
{...getTableProps({
className: tableClassNames
})}
ref={table}
>
<THead {...getGroupWidthProps()}>
{headerGroups.map(headerGroup => (
// eslint-disable-next-line react/jsx-key
<Tr {...headerGroup.getHeaderGroupProps()}>
{headerGroup.headers.map(column => (
// eslint-disable-next-line react/jsx-key
<DraggableTh
{...column.getHeaderProps([
{
className: column.className,
style: column.style
},
getColumnProps(column)
])}
id={column.id}
draggable={column.draggable}
onDragStart={startDrag}
onDragEnd={stopDrag}
onChangeDropSide={changeDropSide}
onDrop={drop}
>
{column.render('Header')}
</DraggableTh>
))}
</Tr>
))}
</THead>
<TBody {...getGroupWidthProps()}>
{rows.map(row => {
prepareRow(row);
const {key, ...rowProps} = row.getRowProps();
return (
<React.Fragment key={key}>
<Tr {...rowProps}>
{row.cells.map(cell => (
// eslint-disable-next-line react/jsx-key
<Td
{...cell.getCellProps([
{
className: cell.column.className,
style: cell.column.style
},
getColumnProps(cell.column)
])}
>
{cell.render('Cell')}
</Td>
))}
</Tr>
{row.isExpanded || expandAll ? (
<ExpandContainer {...getExpanderWidthProps()}>
<MetricGraphs {...getRowMetricGraphsProps(row)} />
</ExpandContainer>
) : null}
</React.Fragment>
);
})}
</TBody>
</Table>
</DndProvider>
);
};
export default TableViewTable;
/**
* Copyright 2020 Baidu Inc. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {DEFAULT_ORDER_INDICATOR, OrderDirection} from '~/resource/hyper-parameter';
import React, {FunctionComponent, useMemo, useState} from 'react';
import Select from '~/components/Select';
import Table from './Table';
import View from '~/components/HyperParameterPage/View';
import type {ViewData} from '~/resource/hyper-parameter';
import {rem} from '~/utils/style';
import styled from 'styled-components';
import {useTranslation} from 'react-i18next';
const Wrapper = styled(View)`
display: flex;
width: 100%;
height: 100%;
flex-direction: column;
justify-content: stretch;
align-items: stretch;
`;
const OrderSection = styled.div`
display: flex;
justify-content: flex-start;
align-items: center;
margin-bottom: ${rem(20)};
> span {
margin-right: 0.5em;
&:not(:first-child) {
margin-left: 1.5em;
}
}
> .order-select {
width: ${rem(160)};
}
`;
const TableSection = styled.div`
width: 100%;
`;
type TableViewProps = ViewData;
const TableView: FunctionComponent<TableViewProps> = ({indicators, list, data}) => {
const {t} = useTranslation('hyper-parameter');
const indicatorNameList = useMemo(() => indicators.map(({name}) => name), [indicators]);
const indicatorOrderList = useMemo(
() => [
{value: DEFAULT_ORDER_INDICATOR, label: t('hyper-parameter.order-default')},
...indicatorNameList.map(value => ({value, label: value}))
],
[indicatorNameList, t]
);
const [indicatorOrder, setIndicatorOrder] = useState<string | symbol>(DEFAULT_ORDER_INDICATOR);
const orderDirectionList = useMemo(
() =>
[OrderDirection.ASCENDING, OrderDirection.DESCENDING].map(value => ({
value,
label: t(`hyper-parameter:order-direction-value.${value}`)
})),
[t]
);
const [orderDirection, setOrderDirection] = useState(OrderDirection.ASCENDING);
const sortBy = useMemo(
() =>
indicatorOrder === DEFAULT_ORDER_INDICATOR
? []
: [{id: indicatorOrder as string, desc: orderDirection === OrderDirection.DESCENDING}],
[orderDirection, indicatorOrder]
);
return (
<Wrapper>
<OrderSection>
<span>{t('hyper-parameter:order-by')}</span>
<Select
className="order-select"
list={indicatorOrderList}
value={indicatorOrder}
onChange={setIndicatorOrder}
/>
{indicatorOrder !== DEFAULT_ORDER_INDICATOR ? (
<>
<span>{t('hyper-parameter:order-direction')}</span>
<Select
className="order-select"
list={orderDirectionList}
value={orderDirection}
onChange={setOrderDirection}
/>
</>
) : null}
</OrderSection>
<TableSection>
<Table indicators={indicators} list={list} data={data} sortBy={sortBy} expand />
</TableSection>
</Wrapper>
);
};
export default TableView;
/**
* Copyright 2020 Baidu Inc. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import TableView from './TableView';
export default TableView;
/**
* Copyright 2020 Baidu Inc. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {borderRadius, rem} from '~/utils/style';
import styled from 'styled-components';
const View = styled.div`
width: 100%;
background-color: var(--background-color);
border-radius: ${borderRadius};
border-top-left-radius: 0;
padding: ${rem(20)};
`;
export default View;
...@@ -18,6 +18,7 @@ import React, {FunctionComponent, Suspense, useMemo} from 'react'; ...@@ -18,6 +18,7 @@ import React, {FunctionComponent, Suspense, useMemo} from 'react';
import type {WithStyled} from '~/utils/style'; import type {WithStyled} from '~/utils/style';
import styled from 'styled-components'; import styled from 'styled-components';
import useClassNames from '~/hooks/useClassNames';
const PUBLIC_PATH: string = import.meta.env.SNOWPACK_PUBLIC_PATH; const PUBLIC_PATH: string = import.meta.env.SNOWPACK_PUBLIC_PATH;
...@@ -40,8 +41,10 @@ type IconProps = { ...@@ -40,8 +41,10 @@ type IconProps = {
const Icon: FunctionComponent<IconProps & WithStyled> = ({type, onClick, className}) => { const Icon: FunctionComponent<IconProps & WithStyled> = ({type, onClick, className}) => {
const Svg = useMemo(() => React.lazy(() => import(`${PUBLIC_PATH}/icons/${type}.js`)), [type]); const Svg = useMemo(() => React.lazy(() => import(`${PUBLIC_PATH}/icons/${type}.js`)), [type]);
const classNames = useClassNames('vdl-icon', `icon-${type}`, className, [type, className]);
return ( return (
<Wrapper className={`vdl-icon icon-${type} ${className ?? ''}`} onClick={() => onClick?.()}> <Wrapper className={classNames} onClick={() => onClick?.()}>
<Suspense fallback=""> <Suspense fallback="">
<Svg /> <Svg />
</Suspense> </Suspense>
......
...@@ -34,8 +34,8 @@ const StyledInput = styled.input<{rounded?: boolean}>` ...@@ -34,8 +34,8 @@ const StyledInput = styled.input<{rounded?: boolean}>`
caret-color: var(--text-color); caret-color: var(--text-color);
${transitionProps(['border-color', 'background-color', 'caret-color', 'color'])} ${transitionProps(['border-color', 'background-color', 'caret-color', 'color'])}
&:hover, &:hover:not(:disabled),
&:focus { &:focus:not(:disabled) {
border-color: var(--border-focused-color); border-color: var(--border-focused-color);
} }
...@@ -43,6 +43,11 @@ const StyledInput = styled.input<{rounded?: boolean}>` ...@@ -43,6 +43,11 @@ const StyledInput = styled.input<{rounded?: boolean}>`
color: var(--text-lighter-color); color: var(--text-lighter-color);
${transitionProps('color')} ${transitionProps('color')}
} }
&:disabled {
cursor: not-allowed;
color: var(--text-lighter-color);
}
`; `;
type CustomInputProps = { type CustomInputProps = {
......
...@@ -84,7 +84,16 @@ const LineChart = React.forwardRef<LineChartRef, LineChartProps & WithStyled>( ...@@ -84,7 +84,16 @@ const LineChart = React.forwardRef<LineChartRef, LineChartProps & WithStyled>(
title: { title: {
text: title ?? '' text: title ?? ''
}, },
series: data?.map(item => xAxis: {
splitLine: {
show: false
},
splitNumber: 5
},
yAxis: {
splitNumber: 4
},
series: data?.map((item, index) =>
defaultsDeep( defaultsDeep(
{ {
// show symbol if there is only one point // show symbol if there is only one point
...@@ -92,6 +101,12 @@ const LineChart = React.forwardRef<LineChartRef, LineChartProps & WithStyled>( ...@@ -92,6 +101,12 @@ const LineChart = React.forwardRef<LineChartRef, LineChartProps & WithStyled>(
type: 'line' type: 'line'
}, },
item, item,
{
lineStyle: {
color: color[index % color.length],
width: 1.5
}
},
series series
) )
) )
......
...@@ -31,6 +31,7 @@ import logo from '~/assets/images/logo.svg'; ...@@ -31,6 +31,7 @@ import logo from '~/assets/images/logo.svg';
import queryString from 'query-string'; import queryString from 'query-string';
import styled from 'styled-components'; import styled from 'styled-components';
import useAvailableComponents from '~/hooks/useAvailableComponents'; import useAvailableComponents from '~/hooks/useAvailableComponents';
import useClassNames from '~/hooks/useClassNames';
import {useTranslation} from 'react-i18next'; import {useTranslation} from 'react-i18next';
const BASE_URI: string = import.meta.env.SNOWPACK_PUBLIC_BASE_URI; const BASE_URI: string = import.meta.env.SNOWPACK_PUBLIC_BASE_URI;
...@@ -207,11 +208,13 @@ const NavbarLink: FunctionComponent<{to?: string} & Omit<LinkProps, 'to'>> = ({t ...@@ -207,11 +208,13 @@ const NavbarLink: FunctionComponent<{to?: string} & Omit<LinkProps, 'to'>> = ({t
// FIXME: why we need to add children type here... that's weird... // FIXME: why we need to add children type here... that's weird...
const NavbarItem = React.forwardRef<HTMLDivElement, NavbarItemProps & {children?: React.ReactNode}>( const NavbarItem = React.forwardRef<HTMLDivElement, NavbarItemProps & {children?: React.ReactNode}>(
({path, active, showDropdownIcon, children}, ref) => { ({path, active, showDropdownIcon, children}, ref) => {
const classNames = useClassNames('nav-text', {'dropdown-icon': showDropdownIcon}, [showDropdownIcon]);
if (path) { if (path) {
return ( return (
<NavItem active={active} ref={ref}> <NavItem active={active} ref={ref}>
<NavbarLink to={path} className="nav-link"> <NavbarLink to={path} className="nav-link">
<span className={`nav-text ${showDropdownIcon ? 'dropdown-icon' : ''}`}>{children}</span> <span className={classNames}>{children}</span>
</NavbarLink> </NavbarLink>
</NavItem> </NavItem>
); );
...@@ -219,7 +222,7 @@ const NavbarItem = React.forwardRef<HTMLDivElement, NavbarItemProps & {children? ...@@ -219,7 +222,7 @@ const NavbarItem = React.forwardRef<HTMLDivElement, NavbarItemProps & {children?
return ( return (
<NavItem active={active} ref={ref}> <NavItem active={active} ref={ref}>
<span className={`nav-text ${showDropdownIcon ? 'dropdown-icon' : ''}`}>{children}</span> <span className={classNames}>{children}</span>
</NavItem> </NavItem>
); );
} }
......
...@@ -160,7 +160,7 @@ const RunAside: FunctionComponent<RunAsideProps> = ({ ...@@ -160,7 +160,7 @@ const RunAside: FunctionComponent<RunAsideProps> = ({
placeholder={t('common:search-runs')} placeholder={t('common:search-runs')}
rounded rounded
/> />
<Checkbox value={selectAll} onChange={toggleSelectAll}> <Checkbox checked={selectAll} onChange={toggleSelectAll}>
{t('common:select-all')} {t('common:select-all')}
</Checkbox> </Checkbox>
<div className="run-list"> <div className="run-list">
...@@ -170,7 +170,7 @@ const RunAside: FunctionComponent<RunAsideProps> = ({ ...@@ -170,7 +170,7 @@ const RunAside: FunctionComponent<RunAsideProps> = ({
filteredRuns.map((run, index) => ( filteredRuns.map((run, index) => (
<div key={index}> <div key={index}>
<Checkbox <Checkbox
value={selectedRuns?.map(r => r.label)?.includes(run.label)} checked={selectedRuns?.map(r => r.label)?.includes(run.label)}
title={run.label} title={run.label}
onChange={value => setSelectedRuns(run, value)} onChange={value => setSelectedRuns(run, value)}
> >
......
...@@ -317,7 +317,7 @@ const ImagePreviewer: FunctionComponent<ImagePreviewerProps> = ({ ...@@ -317,7 +317,7 @@ const ImagePreviewer: FunctionComponent<ImagePreviewerProps> = ({
<Wrapper> <Wrapper>
<Header> <Header>
<div className="step-slider"> <div className="step-slider">
<span>{t('sample:step')}</span> <span>{t('common:time-mode.step')}</span>
<RangeSlider <RangeSlider
className="slider" className="slider"
min={0} min={0}
......
...@@ -110,7 +110,7 @@ const StepSlider: FunctionComponent<StepSliderProps> = ({onChange, onChangeCompl ...@@ -110,7 +110,7 @@ const StepSlider: FunctionComponent<StepSliderProps> = ({onChange, onChangeCompl
<> <>
<Label> <Label>
<div className="step-indicator"> <div className="step-indicator">
<div>{`${t('sample:step')}: ${steps[step] ?? '...'}`}</div> <div>{`${t('common:time-mode.step')}: ${steps[step] ?? '...'}`}</div>
<Tippy placement="right" theme="tooltip" content={t('sample:step-tip')}> <Tippy placement="right" theme="tooltip" content={t('sample:step-tip')}>
<div className="step-buttons"> <div className="step-buttons">
<a onClick={prevStep}> <a onClick={prevStep}>
......
...@@ -170,7 +170,7 @@ const Text: FunctionComponent<TextProps> = ({run, tag, step, wallTime, index}) = ...@@ -170,7 +170,7 @@ const Text: FunctionComponent<TextProps> = ({run, tag, step, wallTime, index}) =
<> <>
<span className="step"> <span className="step">
<span> <span>
{t('sample:step')} {step} {t('common:time-mode.step')} {step}
</span> </span>
</span> </span>
<span className="text" title={text ?? ''}> <span className="text" title={text ?? ''}>
......
/**
* Copyright 2020 Baidu Inc. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import LineChart, {LineChartRef, XAxisType, YAxisType} from '~/components/LineChart';
import type {Range, Run} from '~/types';
import React, {FunctionComponent, useCallback, useMemo, useRef, useState} from 'react';
import {rem, size} from '~/utils/style';
import Chart from '~/components/Chart';
import ChartToolbox from '~/components/ChartToolbox';
import type {EChartOption} from 'echarts';
import TooltipTable from '~/components/TooltipTable';
import {format} from 'd3-format';
import {renderToStaticMarkup} from 'react-dom/server';
import styled from 'styled-components';
import {useTranslation} from 'react-i18next';
const labelFormatter = format('.8');
const Wrapper = styled.div`
${size('100%', '100%')}
display: flex;
flex-direction: column;
align-items: stretch;
justify-content: space-between;
`;
const StyledLineChart = styled(LineChart)`
flex-grow: 1;
`;
const Toolbox = styled(ChartToolbox)`
margin-left: ${rem(20)};
margin-right: ${rem(20)};
margin-bottom: ${rem(18)};
`;
export const chartSize = {
width: 430,
height: 337
};
export const chartSizeInRem = {
width: rem(chartSize.width),
height: rem(chartSize.height)
};
export const DownloadDataTypes = {
csv: 'csv',
tsv: 'tsv'
// excel: 'xlsx'
} as const;
interface TooltipTableData {
runs: Run[];
columns: {
label: string;
width: string;
}[];
data: (string | number)[][];
}
interface ScalarChartProps {
title: string;
data: EChartOption.SeriesLine[];
loading: boolean;
xAxisType?: XAxisType;
xRange?: Range;
yRange?: Range;
getTooltipTableData: (series: number[]) => TooltipTableData;
downloadData?: (type: keyof typeof DownloadDataTypes) => void;
}
const ScalarChart: FunctionComponent<ScalarChartProps> = ({
title,
data,
loading,
xAxisType,
xRange,
yRange,
getTooltipTableData,
downloadData
}) => {
const {t} = useTranslation('common');
const echart = useRef<LineChartRef>(null);
const [maximized, setMaximized] = useState<boolean>(false);
const [yAxisType, setYAxisType] = useState<YAxisType>(YAxisType.value);
const toggleYAxisType = useCallback(() => {
setYAxisType(t => (t === YAxisType.log ? YAxisType.value : YAxisType.log));
}, [setYAxisType]);
const formatter = useCallback(
(params: EChartOption.Tooltip.Format | EChartOption.Tooltip.Format[]) => {
const series: number[] = Array.isArray(params) ? params[0].data : params.data;
return renderToStaticMarkup(<TooltipTable run={t('common:runs')} {...getTooltipTableData(series)} />);
},
[getTooltipTableData, t]
);
const options = useMemo(
() => ({
legend: {
data: []
},
tooltip: {
position: ['10%', '100%'],
formatter,
hideDelay: 300,
enterable: true
},
xAxis: {
type: xAxisType ?? XAxisType.value,
...xRange,
axisPointer: {
label: {
formatter:
xAxisType === XAxisType.time
? undefined
: ({value}: {value: number}) => labelFormatter(value)
}
}
},
yAxis: {
type: yAxisType,
...yRange
}
}),
[formatter, xAxisType, xRange, yAxisType, yRange]
);
const toolbox = useMemo(
() => [
{
icon: 'maximize',
activeIcon: 'minimize',
tooltip: t('common:maximize'),
activeTooltip: t('common:minimize'),
toggle: true,
onClick: () => setMaximized(m => !m)
},
{
icon: 'restore-size',
tooltip: t('common:restore'),
onClick: () => echart.current?.restore()
},
{
icon: 'log-axis',
tooltip: t('common:toggle-log-axis'),
toggle: true,
onClick: toggleYAxisType
},
{
icon: 'download',
menuList: [
{
label: t('common:download-image'),
onClick: () => echart.current?.saveAsImage()
},
...(downloadData
? [
{
label: t('common:download-data'),
children: Object.keys(DownloadDataTypes)
.sort((a, b) => a.localeCompare(b))
.map(format => ({
label: t('common:download-data-format', {format}),
onClick: () => downloadData(format as keyof typeof DownloadDataTypes)
}))
}
]
: [])
]
}
],
[downloadData, t, toggleYAxisType]
);
return (
<Chart maximized={maximized} {...chartSizeInRem}>
<Wrapper>
<StyledLineChart ref={echart} title={title} options={options} data={data} loading={loading} zoom />
<Toolbox items={toolbox} />
</Wrapper>
</Chart>
);
};
export default ScalarChart;
...@@ -15,79 +15,39 @@ ...@@ -15,79 +15,39 @@
*/ */
import type {Dataset, Range, ScalarDataset} from '~/resource/scalar'; import type {Dataset, Range, ScalarDataset} from '~/resource/scalar';
import LineChart, {LineChartRef, XAxisType, YAxisType} from '~/components/LineChart'; import React, {FunctionComponent, useCallback, useMemo} from 'react';
import React, {FunctionComponent, useCallback, useMemo, useRef, useState} from 'react'; import SChart, {DownloadDataTypes, chartSize, chartSizeInRem} from '~/components/ScalarChart';
import { import {
SortingMethod, SortingMethod,
XAxis, XAxis,
chartData, chartData,
options as chartOptions,
nearestPoint, nearestPoint,
singlePointRange, singlePointRange,
sortingMethodMap, sortingMethodMap,
tooltip, tooltip,
xAxisMap xAxisMap
} from '~/resource/scalar'; } from '~/resource/scalar';
import {rem, size} from '~/utils/style';
import Chart from '~/components/Chart'; import Chart from '~/components/Chart';
import {Chart as ChartLoader} from '~/components/Loader/ChartPage'; import {Chart as ChartLoader} from '~/components/Loader/ChartPage';
import ChartToolbox from '~/components/ChartToolbox';
import type {EChartOption} from 'echarts';
import type {Run} from '~/types'; import type {Run} from '~/types';
import TooltipTable from '~/components/TooltipTable'; import {XAxisType} from '~/components/LineChart';
import {cycleFetcher} from '~/utils/fetch'; import {cycleFetcher} from '~/utils/fetch';
import {format} from 'd3-format';
import queryString from 'query-string'; import queryString from 'query-string';
import {renderToStaticMarkup} from 'react-dom/server';
import saveFile from '~/utils/saveFile'; import saveFile from '~/utils/saveFile';
import styled from 'styled-components'; import styled from 'styled-components';
import {useRunningRequest} from '~/hooks/useRequest'; import {useRunningRequest} from '~/hooks/useRequest';
import {useTranslation} from 'react-i18next'; import {useTranslation} from 'react-i18next';
import useWebAssembly from '~/hooks/useWebAssembly'; import useWebAssembly from '~/hooks/useWebAssembly';
const DownloadDataTypes = {
csv: 'csv',
tsv: 'tsv'
// excel: 'xlsx'
} as const;
const labelFormatter = format('.8');
const Wrapper = styled.div`
${size('100%', '100%')}
display: flex;
flex-direction: column;
align-items: stretch;
justify-content: space-between;
`;
const StyledLineChart = styled(LineChart)`
flex-grow: 1;
`;
const Toolbox = styled(ChartToolbox)`
margin-left: ${rem(20)};
margin-right: ${rem(20)};
margin-bottom: ${rem(18)};
`;
const Error = styled.div` const Error = styled.div`
${size('100%', '100%')} width: 100%;
height: 100%;
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
`; `;
const chartSize = {
width: 430,
height: 337
};
const chartSizeInRem = {
width: rem(chartSize.width),
height: rem(chartSize.height)
};
type ScalarChartProps = { type ScalarChartProps = {
runs: Run[]; runs: Run[];
tag: string; tag: string;
...@@ -113,23 +73,14 @@ const ScalarChart: FunctionComponent<ScalarChartProps> = ({ ...@@ -113,23 +73,14 @@ const ScalarChart: FunctionComponent<ScalarChartProps> = ({
}) => { }) => {
const {t, i18n} = useTranslation(['scalar', 'common']); const {t, i18n} = useTranslation(['scalar', 'common']);
const echart = useRef<LineChartRef>(null);
const {data: datasets, error, loading} = useRunningRequest<(ScalarDataset | null)[]>( const {data: datasets, error, loading} = useRunningRequest<(ScalarDataset | null)[]>(
runs.map(run => `/scalar/list?${queryString.stringify({run: run.label, tag})}`), runs.map(run => `/scalar/list?${queryString.stringify({run: run.label, tag})}`),
!!running, !!running,
(...urls) => cycleFetcher(urls) (...urls) => cycleFetcher(urls)
); );
const [maximized, setMaximized] = useState<boolean>(false);
const xAxisType = useMemo(() => (xAxis === XAxis.WallTime ? XAxisType.time : XAxisType.value), [xAxis]); const xAxisType = useMemo(() => (xAxis === XAxis.WallTime ? XAxisType.time : XAxisType.value), [xAxis]);
const [yAxisType, setYAxisType] = useState<YAxisType>(YAxisType.value);
const toggleYAxisType = useCallback(() => {
setYAxisType(t => (t === YAxisType.log ? YAxisType.value : YAxisType.log));
}, [setYAxisType]);
const transformParams = useMemo(() => [datasets?.map(data => data ?? []) ?? [], smoothing], [datasets, smoothing]); const transformParams = useMemo(() => [datasets?.map(data => data ?? []) ?? [], smoothing], [datasets, smoothing]);
const {data: smoothedDatasetsOrUndefined} = useWebAssembly<Dataset[]>('scalar_transform', transformParams); const {data: smoothedDatasetsOrUndefined} = useWebAssembly<Dataset[]>('scalar_transform', transformParams);
const smoothedDatasets = useMemo<NonNullable<typeof smoothedDatasetsOrUndefined>>( const smoothedDatasets = useMemo<NonNullable<typeof smoothedDatasetsOrUndefined>>(
...@@ -173,10 +124,8 @@ const ScalarChart: FunctionComponent<ScalarChartProps> = ({ ...@@ -173,10 +124,8 @@ const ScalarChart: FunctionComponent<ScalarChartProps> = ({
() => String(Math.max(...smoothedDatasets.map(i => Math.max(...i.map(j => j[1]))))).length, () => String(Math.max(...smoothedDatasets.map(i => Math.max(...i.map(j => j[1]))))).length,
[smoothedDatasets] [smoothedDatasets]
); );
const getTooltipTableData = useCallback(
const formatter = useCallback( (series: number[]) => {
(params: EChartOption.Tooltip.Format | EChartOption.Tooltip.Format[]) => {
const series: Dataset[number] = Array.isArray(params) ? params[0].data : params.data;
const idx = xAxisMap[xAxis]; const idx = xAxisMap[xAxis];
const points = nearestPoint(smoothedDatasets ?? [], runs, idx, series[idx]).map((point, index) => ({ const points = nearestPoint(smoothedDatasets ?? [], runs, idx, series[idx]).map((point, index) => ({
...point, ...point,
...@@ -185,40 +134,13 @@ const ScalarChart: FunctionComponent<ScalarChartProps> = ({ ...@@ -185,40 +134,13 @@ const ScalarChart: FunctionComponent<ScalarChartProps> = ({
const sort = sortingMethodMap[sortingMethod]; const sort = sortingMethodMap[sortingMethod];
const sorted = sort(points, series); const sorted = sort(points, series);
const {columns, data} = tooltip(sorted, maxStepLength, i18n); const {columns, data} = tooltip(sorted, maxStepLength, i18n);
return renderToStaticMarkup( return {
<TooltipTable run={t('common:runs')} runs={sorted.map(i => i.run)} columns={columns} data={data} /> runs: sorted.map(i => i.run),
); columns,
data
};
}, },
[smoothedDatasets, datasetRanges, runs, sortingMethod, xAxis, maxStepLength, t, i18n] [smoothedDatasets, datasetRanges, runs, sortingMethod, xAxis, maxStepLength, i18n]
);
const options = useMemo(
() => ({
...chartOptions,
tooltip: {
...chartOptions.tooltip,
formatter,
hideDelay: 300,
enterable: true
},
xAxis: {
type: xAxisType,
...ranges.x,
axisPointer: {
label: {
formatter:
xAxisType === XAxisType.time
? undefined
: ({value}: {value: number}) => labelFormatter(value)
}
}
},
yAxis: {
type: yAxisType,
...ranges.y
}
}),
[formatter, ranges, xAxisType, yAxisType]
); );
const downloadData = useCallback( const downloadData = useCallback(
...@@ -239,61 +161,22 @@ const ScalarChart: FunctionComponent<ScalarChartProps> = ({ ...@@ -239,61 +161,22 @@ const ScalarChart: FunctionComponent<ScalarChartProps> = ({
[runs, tag] [runs, tag]
); );
const toolbox = useMemo(
() => [
{
icon: 'maximize',
activeIcon: 'minimize',
tooltip: t('scalar:maximize'),
activeTooltip: t('scalar:minimize'),
toggle: true,
onClick: () => setMaximized(m => !m)
},
{
icon: 'restore-size',
tooltip: t('scalar:restore'),
onClick: () => echart.current?.restore()
},
{
icon: 'log-axis',
tooltip: t('scalar:toggle-log-axis'),
toggle: true,
onClick: toggleYAxisType
},
{
icon: 'download',
menuList: [
{
label: t('scalar:download-image'),
onClick: () => echart.current?.saveAsImage()
},
{
label: t('scalar:download-data'),
children: Object.keys(DownloadDataTypes)
.sort((a, b) => a.localeCompare(b))
.map(format => ({
label: t('scalar:download-data-format', {format}),
onClick: () => downloadData(format as keyof typeof DownloadDataTypes)
}))
}
]
}
],
[downloadData, t, toggleYAxisType]
);
// display error only on first fetch // display error only on first fetch
if (!data && error) { if (!data && error) {
return <Error>{t('common:error')}</Error>; return <Error>{t('common:error')}</Error>;
} }
return ( return (
<Chart maximized={maximized} {...chartSizeInRem}> <SChart
<Wrapper> title={tag}
<StyledLineChart ref={echart} title={tag} options={options} data={data} loading={loading} zoom /> data={data}
<Toolbox items={toolbox} /> loading={loading}
</Wrapper> xAxisType={xAxisType}
</Chart> xRange={ranges.x}
yRange={ranges.y}
getTooltipTableData={getTooltipTableData}
downloadData={downloadData}
/>
); );
}; };
......
...@@ -40,8 +40,10 @@ export const padding = em(10); ...@@ -40,8 +40,10 @@ export const padding = em(10);
export const height = em(36); export const height = em(36);
const Wrapper = styled.div<{opened?: boolean}>` const Wrapper = styled.div<{opened?: boolean}>`
height: ${height}; --height: ${height};
line-height: calc(${height} - 2px); --padding: ${padding};
height: var(--height);
line-height: calc(var(--height) - 2px);
max-width: 100%; max-width: 100%;
display: inline-block; display: inline-block;
position: relative; position: relative;
...@@ -56,7 +58,7 @@ const Wrapper = styled.div<{opened?: boolean}>` ...@@ -56,7 +58,7 @@ const Wrapper = styled.div<{opened?: boolean}>`
`; `;
const Trigger = styled.div<{selected?: boolean}>` const Trigger = styled.div<{selected?: boolean}>`
padding: ${padding}; padding: var(--padding);
display: inline-flex; display: inline-flex;
${size('100%')} ${size('100%')}
justify-content: space-between; justify-content: space-between;
...@@ -78,12 +80,26 @@ const TriggerIcon = styled(Icon)<{opened?: boolean}>` ...@@ -78,12 +80,26 @@ const TriggerIcon = styled(Icon)<{opened?: boolean}>`
const Label = styled.span` const Label = styled.span`
flex-grow: 1; flex-grow: 1;
padding-right: ${em(10)}; padding-right: ${em(10)};
line-height: 1;
${ellipsis()} ${ellipsis()}
`; `;
const List = styled.div<{opened?: boolean; empty?: boolean}>` const List = styled.div<{opened?: boolean; empty?: boolean; direction?: 'bottom' | 'top'}>`
position: absolute; position: absolute;
top: 100%; ${props =>
props.direction === 'top'
? {
bottom: '100%',
borderBottomColor: 'var(--border-color)',
boxShadow: '0 -5px 6px 0 rgba(0, 0, 0, 0.05)',
...borderRadiusShortHand('top', borderRadius)
}
: {
top: '100%',
borderTopColor: 'var(--border-color)',
boxShadow: '0 5px 6px 0 rgba(0, 0, 0, 0.05)',
...borderRadiusShortHand('bottom', borderRadius)
}}
width: calc(100% + 2px); width: calc(100% + 2px);
max-height: ${math(`4.35 * ${height} + 2 * ${padding}`)}; max-height: ${math(`4.35 * ${height} + 2 * ${padding}`)};
overflow-x: hidden; overflow-x: hidden;
...@@ -91,13 +107,10 @@ const List = styled.div<{opened?: boolean; empty?: boolean}>` ...@@ -91,13 +107,10 @@ const List = styled.div<{opened?: boolean; empty?: boolean}>`
left: -1px; left: -1px;
padding: ${padding} 0; padding: ${padding} 0;
border: inherit; border: inherit;
border-top-color: var(--border-color);
${borderRadiusShortHand('bottom', borderRadius)}
display: ${props => (props.opened ? 'block' : 'none')}; display: ${props => (props.opened ? 'block' : 'none')};
z-index: ${zIndexes.component}; z-index: ${zIndexes.component};
line-height: 1; line-height: 1;
background-color: inherit; background-color: inherit;
box-shadow: 0 5px 6px 0 rgba(0, 0, 0, 0.05);
${transitionProps(['border-color', 'color'])} ${transitionProps(['border-color', 'color'])}
${props => ${props =>
props.empty props.empty
...@@ -168,6 +181,7 @@ export type SelectListItem<T> = { ...@@ -168,6 +181,7 @@ export type SelectListItem<T> = {
export type SelectProps<T> = { export type SelectProps<T> = {
list?: (SelectListItem<T> | T)[]; list?: (SelectListItem<T> | T)[];
placeholder?: string; placeholder?: string;
direction?: 'bottom' | 'top';
} & ( } & (
| { | {
value?: T; value?: T;
...@@ -185,6 +199,7 @@ const Select = <T extends unknown>({ ...@@ -185,6 +199,7 @@ const Select = <T extends unknown>({
list: propList, list: propList,
value: propValue, value: propValue,
placeholder, placeholder,
direction,
multiple, multiple,
className, className,
onChange onChange
...@@ -267,14 +282,14 @@ const Select = <T extends unknown>({ ...@@ -267,14 +282,14 @@ const Select = <T extends unknown>({
<Label>{label}</Label> <Label>{label}</Label>
<TriggerIcon opened={isOpened} type="chevron-down" /> <TriggerIcon opened={isOpened} type="chevron-down" />
</Trigger> </Trigger>
<List opened={isOpened} empty={isListEmpty}> <List className="list" opened={isOpened} empty={isListEmpty} direction={direction}>
{isListEmpty {isListEmpty
? t('common:empty') ? t('common:empty')
: list.map((item, index) => { : list.map((item, index) => {
if (multiple) { if (multiple) {
return ( return (
<MultipleListItem <MultipleListItem
value={(value as T[]).includes(item.value)} checked={(value as T[]).includes(item.value)}
key={index} key={index}
title={item.label} title={item.label}
disabled={item.disabled} disabled={item.disabled}
......
...@@ -15,47 +15,108 @@ ...@@ -15,47 +15,108 @@
*/ */
import React, {FunctionComponent, useCallback, useEffect, useState} from 'react'; import React, {FunctionComponent, useCallback, useEffect, useState} from 'react';
import {em, rem, transitionProps} from '~/utils/style'; import {WithStyled, borderRadius, rem, transitionProps} from '~/utils/style';
import styled from 'styled-components'; import styled from 'styled-components';
import useClassNames from '~/hooks/useClassNames';
const Wrapper = styled.div` const Wrapper = styled.div`
display: flex; display: flex;
align-items: stretch; align-items: stretch;
justify-content: flex-start;
&.fullWidth {
justify-content: space-between; justify-content: space-between;
}
&.centered {
justify-content: center;
}
> a { > a {
cursor: pointer; cursor: pointer;
display: block; display: block;
font-size: ${rem(16)}; font-size: ${rem(16)};
border-bottom: 2px solid transparent; background-color: var(--tab-inactive-background-color);
${transitionProps(['border-color', 'color'])} padding: 0.75em 1.25em;
position: relative;
&:not(:last-child) { &:first-child {
margin-right: ${em(20)}; border-top-left-radius: ${borderRadius};
}
&:last-child {
border-top-right-radius: ${borderRadius};
}
&:not(.active) + a:not(.active)::before {
content: '';
display: block;
width: 1px;
height: calc(100% - 1.5em);
background-color: var(--border-color);
position: absolute;
left: 0;
top: 0.75em;
} }
&.active { &.active {
color: var(--primary-color); color: var(--primary-color);
border-bottom-color: var(--primary-color); background-color: var(--background-color);
} }
&:hover { &:hover {
color: var(--primary-color); color: var(--primary-color);
} }
} }
&.underscore > a {
border-bottom: 2px solid transparent;
${transitionProps(['border-color', 'color'])}
background-color: transparent;
padding: 0;
padding-bottom: 0.4em;
&:first-child {
border-top-left-radius: 0;
}
&:last-child {
border-top-right-radius: 0;
}
&:not(:last-child) {
margin-right: 1.25em;
}
&:not(.active) + a:not(.active)::before {
display: none;
}
&.active {
border-bottom-color: var(--primary-color);
background-color: transparent;
}
}
`; `;
type TabProps<T> = { export type TabProps<T> = {
list?: { list?: {
value: T; value: T;
label: string; label: string;
}[]; }[];
value?: T; value?: T;
variant?: 'fullWidth' | 'centered';
appearance?: 'underscore';
onChange?: (value: T) => unknown; onChange?: (value: T) => unknown;
}; };
const Tab = <T extends unknown>({list, value, onChange}: TabProps<T>): ReturnType<FunctionComponent> => { const Tab = <T extends unknown>({
list,
value,
variant,
appearance,
className,
onChange
}: TabProps<T> & WithStyled): ReturnType<FunctionComponent> => {
const [selected, setSelected] = useState<T | undefined>(value); const [selected, setSelected] = useState<T | undefined>(value);
useEffect(() => setSelected(value), [value]); useEffect(() => setSelected(value), [value]);
const change = useCallback( const change = useCallback(
...@@ -68,8 +129,10 @@ const Tab = <T extends unknown>({list, value, onChange}: TabProps<T>): ReturnTyp ...@@ -68,8 +129,10 @@ const Tab = <T extends unknown>({list, value, onChange}: TabProps<T>): ReturnTyp
[selected, onChange] [selected, onChange]
); );
const classNames = useClassNames(className, variant, appearance, [appearance, className, variant]);
return ( return (
<Wrapper> <Wrapper className={classNames}>
{list?.map((item, index) => ( {list?.map((item, index) => (
<a <a
key={index} key={index}
......
/**
* Copyright 2020 Baidu Inc. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {css, dragger, em, rem, sameBorder} from '~/utils/style';
import classNames from 'classnames';
import styled from 'styled-components';
const Dragger = styled.span`
${dragger}
`;
const table = css`
border-spacing: 0;
${sameBorder({radius: true})};
border-bottom-left-radius: 0;
border-bottom-right-radius: 0;
&.is-droppable-left {
border-left-color: var(--primary-color);
}
&.is-droppable-right {
border-right-color: var(--primary-color);
}
&.sticky {
overflow: auto;
}
`;
const group = css``;
const row = css``;
const cell = css`
--resizer-pad: ${rem(2)};
margin: 0;
padding: ${rem(15)} ${rem(20)};
border-bottom: 1px solid var(--border-color);
border-right: 1px solid var(--border-color);
position: relative;
&:last-child {
border-right: none;
}
&.is-sticky {
position: sticky;
}
&.is-resizing {
border-right-color: var(--primary-color);
}
&.is-dragging {
opacity: 0.4;
}
&.is-droppable:not(:last-child) {
border-right-color: var(--primary-color);
}
&[data-sticky-td] {
position: sticky;
}
&[data-sticky-last-left-td] {
box-shadow: 5px 0 3px -3px var(--table-sticky-shadow-color);
&:not(.is-resizing):not(.is-droppable) {
border-right-color: var(--table-sticky-shadow-color);
}
}
`;
const th = css`
background-color: var(--table-header-background-color);
${Dragger} {
position: absolute;
left: var(--resizer-pad);
top: 50%;
transform: translateY(-50%);
color: var(--table-dragger-color);
opacity: 0;
}
&:hover ${Dragger} {
opacity: 1;
}
`;
const td = css`
background-color: var(--table-background-color);
.tr:hover > & {
background-color: var(--table-row-hover-background-color);
}
.tr:last-child > & {
border-bottom: none;
}
`;
const thead = css`
background-color: var(--table-header-background-color);
.sticky > & {
top: 0;
position: sticky;
z-index: 1;
}
`;
const tbody = css`
.sticky > & {
position: relative;
z-index: 0;
}
`;
const createStyledTableComponent = (name: string) =>
styled.div.attrs(({className}) => ({
className: classNames([className, name])
}));
// keep next line to fix coding highlight
//``
const Table = createStyledTableComponent('table')`
${table};
`;
const THead = createStyledTableComponent('thead')`
${group}
${thead}
`;
const TBody = createStyledTableComponent('tbody')`
${group}
${tbody}
`;
const TFoot = createStyledTableComponent('tfoot')`
${group}
`;
const Tr = createStyledTableComponent('tr')`
${row}
`;
const Th = createStyledTableComponent('th')`
${cell}
${th}
`;
const Td = createStyledTableComponent('td')`
${cell}
${td}
`;
const Resizer = styled.span`
box-sizing: content-box;
background-clip: content-box;
width: 1px;
height: 100%;
position: absolute;
top: 0;
right: calc(var(--resizer-pad) * -1 - 1px);
z-index: 1;
touch-action: none;
border-left: var(--resizer-pad) solid transparent;
border-right: var(--resizer-pad) solid transparent;
&:hover {
background-color: var(--primary-color);
}
&.is-resizing {
background-color: var(--primary-color);
border-color: var(--primary-color);
}
.th:last-child > & {
display: none;
}
`;
const Expander = styled.span<{isExpanded: boolean}>`
display: inline-block;
position: relative;
font-size: ${em(16)};
width: 1em;
height: 1em;
${sameBorder({
color: 'var(--table-expander-border-color)',
radius: '0.125em'
})}
margin-right: 0.75em;
color: var(--table-expander-color);
&::before,
&::after {
content: '';
display: block;
background-color: currentColor;
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
}
&::before {
width: 0.5em;
height: 0.0625em;
}
&::after {
width: 0.0625em;
height: 0.5em;
display: ${props => (props.isExpanded ? 'none' : 'block')};
}
&:hover {
color: var(--table-expander-hover-color);
border-color: var(--table-expander-hover-border-color);
}
`;
const ExpandContainer = styled.div`
${cell}
background-color: var(--table-header-background-color);
border-right: none;
&:last-child {
border-bottom: none;
}
.sticky > .tbody > & {
position: sticky;
left: 0;
}
`;
export {Table, THead, TBody, TFoot, Tr, Th, Td, Resizer, Expander, ExpandContainer, Dragger};
...@@ -34,6 +34,7 @@ const Wrapper = styled.div` ...@@ -34,6 +34,7 @@ const Wrapper = styled.div`
th, th,
td { td {
margin: 0; margin: 0;
line-height: 1;
> span { > span {
display: inline-block; display: inline-block;
...@@ -46,11 +47,11 @@ const Wrapper = styled.div` ...@@ -46,11 +47,11 @@ const Wrapper = styled.div`
th { th {
font-size: 1.166666667em; font-size: 1.166666667em;
font-weight: bold; font-weight: bold;
padding: 0 0.285714286em; padding: 0.15em 0.285714286em;
} }
td { td {
padding: 0 0.333333333em; padding: 0.15em 0.333333333em;
&.run-indicator > span { &.run-indicator > span {
${size(12, 12)} ${size(12, 12)}
......
/**
* Copyright 2020 Baidu Inc. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/* eslint-disable @typescript-eslint/no-explicit-any */
import classNames from 'classnames';
import {useMemo} from 'react';
export default function useClassNames(...args: [...Parameters<typeof classNames>, any[]]) {
const deps = args.pop() as any[];
// eslint-disable-next-line react-hooks/exhaustive-deps
return useMemo(() => classNames(...args), deps);
}
...@@ -188,10 +188,10 @@ export const useChartTheme = (gl?: boolean) => { ...@@ -188,10 +188,10 @@ export const useChartTheme = (gl?: boolean) => {
}, },
xAxis3D: { xAxis3D: {
nameTextStyle: { nameTextStyle: {
color: tt.textLighterColor color: tt.textLightColor
}, },
axisLabel: { axisLabel: {
color: tt.textLighterColor color: tt.textLightColor
}, },
axisLine: { axisLine: {
lineStyle: { lineStyle: {
...@@ -206,10 +206,10 @@ export const useChartTheme = (gl?: boolean) => { ...@@ -206,10 +206,10 @@ export const useChartTheme = (gl?: boolean) => {
}, },
yAxis3D: { yAxis3D: {
nameTextStyle: { nameTextStyle: {
color: tt.textLighterColor color: tt.textLightColor
}, },
axisLabel: { axisLabel: {
color: tt.textLighterColor color: tt.textLightColor
}, },
axisLine: { axisLine: {
lineStyle: { lineStyle: {
...@@ -224,10 +224,10 @@ export const useChartTheme = (gl?: boolean) => { ...@@ -224,10 +224,10 @@ export const useChartTheme = (gl?: boolean) => {
}, },
zAxis3D: { zAxis3D: {
nameTextStyle: { nameTextStyle: {
color: tt.textLighterColor color: tt.textLightColor
}, },
axisLabel: { axisLabel: {
color: tt.textLighterColor color: tt.textLightColor
}, },
axisLine: { axisLine: {
lineStyle: { lineStyle: {
...@@ -257,10 +257,10 @@ export const useChartTheme = (gl?: boolean) => { ...@@ -257,10 +257,10 @@ export const useChartTheme = (gl?: boolean) => {
}, },
xAxis: { xAxis: {
nameTextStyle: { nameTextStyle: {
color: tt.textLighterColor color: tt.textLightColor
}, },
axisLabel: { axisLabel: {
color: tt.textLighterColor color: tt.textLightColor
}, },
axisLine: { axisLine: {
lineStyle: { lineStyle: {
...@@ -275,10 +275,10 @@ export const useChartTheme = (gl?: boolean) => { ...@@ -275,10 +275,10 @@ export const useChartTheme = (gl?: boolean) => {
}, },
yAxis: { yAxis: {
nameTextStyle: { nameTextStyle: {
color: tt.textLighterColor color: tt.textLightColor
}, },
axisLabel: { axisLabel: {
color: tt.textLighterColor color: tt.textLightColor
}, },
axisLine: { axisLine: {
lineStyle: { lineStyle: {
......
...@@ -49,6 +49,9 @@ function useRequest<D = unknown, E extends Error = Error>( ...@@ -49,6 +49,9 @@ function useRequest<D = unknown, E extends Error = Error>(
const key = args[0]; const key = args[0];
const {data, error, ...other} = useSWR<D, E>(...args); const {data, error, ...other} = useSWR<D, E>(...args);
// loading referrers to first loading
// if you want to check if there is an active request
// please use `isValidating` instead
const loading = useMemo(() => !!key && data === void 0 && !error, [key, data, error]); const loading = useMemo(() => !!key && data === void 0 && !error, [key, data, error]);
useEffect(() => { useEffect(() => {
......
...@@ -225,17 +225,17 @@ const Graph: FunctionComponent = () => { ...@@ -225,17 +225,17 @@ const Graph: FunctionComponent = () => {
<AsideSection> <AsideSection>
<Field label={t('graph:display-data')}> <Field label={t('graph:display-data')}>
<div> <div>
<Checkbox value={showAttributes} onChange={setShowAttributes}> <Checkbox checked={showAttributes} onChange={setShowAttributes}>
{t('graph:show-attributes')} {t('graph:show-attributes')}
</Checkbox> </Checkbox>
</div> </div>
<div> <div>
<Checkbox value={showInitializers} onChange={setShowInitializers}> <Checkbox checked={showInitializers} onChange={setShowInitializers}>
{t('graph:show-initializers')} {t('graph:show-initializers')}
</Checkbox> </Checkbox>
</div> </div>
<div> <div>
<Checkbox value={showNames} onChange={setShowNames}> <Checkbox checked={showNames} onChange={setShowNames}>
{t('graph:show-node-names')} {t('graph:show-node-names')}
</Checkbox> </Checkbox>
</div> </div>
......
/**
* Copyright 2020 Baidu Inc. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import Aside, {AsideSection} from '~/components/Aside';
import type {Indicator, IndicatorData, ListItem, ViewData} from '~/resource/hyper-parameter';
import React, {FunctionComponent, useCallback, useMemo, useState} from 'react';
import {asideWidth, rem} from '~/utils/style';
import {filter, format, formatIndicators} from '~/resource/hyper-parameter';
import BodyLoading from '~/components/BodyLoading';
import Button from '~/components/Button';
import Content from '~/components/Content';
import Field from '~/components/Field';
import ImportanceDialog from '~/components/HyperParameterPage/ImportanceDialog';
import IndicatorFilter from '~/components/HyperParameterPage/IndicatorFilter/IndicatorFilter';
import ParallelCoordinatesView from '~/components/HyperParameterPage/ParallelCoordinatesView';
import ScatterPlotMatrixView from '~/components/HyperParameterPage/ScatterPlotMatrixView';
import Tab from '~/components/Tab';
import TableView from '~/components/HyperParameterPage/TableView';
import Title from '~/components/Title';
import queryString from 'query-string';
import saveFile from '~/utils/saveFile';
import styled from 'styled-components';
import useRequest from '~/hooks/useRequest';
import {useTranslation} from 'react-i18next';
const ImportanceButton = styled(Button)`
width: 100%;
`;
const HParamsImportanceDialog = styled(ImportanceDialog)`
position: fixed;
right: calc(${asideWidth} + ${rem(20)});
bottom: ${rem(20)};
`;
const DownloadButtons = styled.div`
display: flex;
justify-content: space-between;
align-items: center;
> * {
flex-grow: 1;
&:not(:last-child) {
margin-right: ${rem(16)};
}
}
`;
const HPWrapper = styled.div`
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
justify-content: stretch;
align-items: stretch;
`;
const ViewWrapper = styled.div`
width: 100%;
flex-grow: 1;
position: relative;
`;
const HyperParameter: FunctionComponent = () => {
const {t} = useTranslation(['hyper-parameter', 'common']);
const {data: indicatorsData, loading: loadingIndicators} = useRequest<IndicatorData>('/hparams/indicators');
const indicators = useMemo(
() => [
...formatIndicators(indicatorsData?.hparams ?? [], 'hparams'),
...formatIndicators(indicatorsData?.metrics ?? [], 'metrics')
],
[indicatorsData]
);
const {data: list, loading: loadingList} = useRequest<ListItem[]>('/hparams/list');
const [filteredIndicators, setFilteredIndicators] = useState<Indicator[]>(indicators);
const filteredList = useMemo(() => filter(list ?? [], filteredIndicators), [filteredIndicators, list]);
const formattedList = useMemo(() => format(filteredList, indicators), [filteredList, indicators]);
const loading = useMemo(() => loadingIndicators || loadingList, [loadingIndicators, loadingList]);
const tabs = useMemo(
() =>
['table', 'parallel-coordinates', 'scatter-plot-matrix'].map(value => ({
value,
label: t(`hyper-parameter:views.${value}`)
})),
[t]
);
const [tabView, setTabView] = useState(tabs[0].value);
const viewData = useMemo<ViewData>(
() => ({
indicators: filteredIndicators,
list: formattedList,
data: filteredList
}),
[filteredIndicators, filteredList, formattedList]
);
const view = useMemo(() => {
switch (tabView) {
case 'table':
return <TableView {...viewData} />;
case 'parallel-coordinates':
return <ParallelCoordinatesView {...viewData} />;
case 'scatter-plot-matrix':
return <ScatterPlotMatrixView {...viewData} />;
default:
return null;
}
}, [tabView, viewData]);
const [importanceDialogVisible, setImportanceDialogVisible] = useState(false);
const downloadData = useCallback(
(type: 'tsv' | 'csv') =>
saveFile(
queryString.stringifyUrl({
url: '/hparams/data',
query: {
type
}
}),
`visualdl-hyper-parameters.${type}`
),
[]
);
const aside = useMemo(
() => (
<Aside
bottom={
<>
<AsideSection>
<ImportanceButton
rounded
outline
type="primary"
onClick={() => setImportanceDialogVisible(v => !v)}
>
{t('hyper-parameter:show-parameter-importance')}
</ImportanceButton>
</AsideSection>
<AsideSection>
<Field label={t('common:download-data')}>
<DownloadButtons>
<Button rounded outline onClick={() => downloadData('csv')}>
{t('common:download-data-format', {format: 'CSV'})}
</Button>
<Button rounded outline onClick={() => downloadData('tsv')}>
{t('common:download-data-format', {format: 'TSV'})}
</Button>
</DownloadButtons>
</Field>
</AsideSection>
</>
}
>
<IndicatorFilter indicators={indicators} onChange={setFilteredIndicators} />
</Aside>
),
[downloadData, indicators, t]
);
return (
<>
<Title>{t('common:hyper-parameter')}</Title>
<Content aside={aside}>
{loading ? <BodyLoading /> : null}
<HPWrapper>
<Tab list={tabs} value={tabView} onChange={setTabView} />
<ViewWrapper>
{view}
<HParamsImportanceDialog
visible={importanceDialogVisible}
onClickClose={() => setImportanceDialogVisible(false)}
/>
</ViewWrapper>
</HPWrapper>
</Content>
</>
);
};
export default HyperParameter;
...@@ -53,7 +53,7 @@ const ImageSample: FunctionComponent = () => { ...@@ -53,7 +53,7 @@ const ImageSample: FunctionComponent = () => {
loading={loading} loading={loading}
> >
<AsideSection> <AsideSection>
<Checkbox value={showActualSize} onChange={setShowActualSize}> <Checkbox checked={showActualSize} onChange={setShowActualSize}>
{t('sample:show-actual-size')} {t('sample:show-actual-size')}
</Checkbox> </Checkbox>
</AsideSection> </AsideSection>
......
...@@ -89,12 +89,12 @@ const Scalar: FunctionComponent = () => { ...@@ -89,12 +89,12 @@ const Scalar: FunctionComponent = () => {
> >
<AsideSection> <AsideSection>
<Field> <Field>
<Checkbox value={ignoreOutliers} onChange={setIgnoreOutliers}> <Checkbox checked={ignoreOutliers} onChange={setIgnoreOutliers}>
{t('scalar:ignore-outliers')} {t('scalar:ignore-outliers')}
</Checkbox> </Checkbox>
</Field> </Field>
<Field> <Field>
<Checkbox value={showMostValue} onChange={setShowMostValue}> <Checkbox checked={showMostValue} onChange={setShowMostValue}>
{t('scalar:show-most-value')} {t('scalar:show-most-value')}
</Checkbox> </Checkbox>
</Field> </Field>
...@@ -115,7 +115,7 @@ const Scalar: FunctionComponent = () => { ...@@ -115,7 +115,7 @@ const Scalar: FunctionComponent = () => {
<Slider min={0} max={0.99} step={0.01} value={smoothing} onChangeComplete={setSmoothing} /> <Slider min={0} max={0.99} step={0.01} value={smoothing} onChangeComplete={setSmoothing} />
</Field> </Field>
<Field> <Field>
<Checkbox value={smoothedDataOnly} onChange={setSmoothedDataOnly}> <Checkbox checked={smoothedDataOnly} onChange={setSmoothedDataOnly}>
{t('scalar:smoothed-data-only')} {t('scalar:smoothed-data-only')}
</Checkbox> </Checkbox>
</Field> </Field>
......
/**
* Copyright 2020 Baidu Inc. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type {DataListItem, Indicator} from './types';
function filterRow(row: DataListItem, indicators: Indicator[]): boolean {
for (const indicator of indicators) {
const value = row[indicator.group][indicator.name];
// ignore null value
if (value == null) {
continue;
}
switch (indicator.type) {
case 'numeric':
case 'string': {
if (
indicator.selectedValues != null &&
!(indicator.selectedValues as (string | number)[]).includes(value)
) {
return false;
}
continue;
}
case 'continuous': {
if (indicator.min != null && indicator.max != null) {
if ('number' !== typeof value || value < indicator.min || value > indicator.max) {
return false;
}
}
continue;
}
}
}
return true;
}
export function filter(list: DataListItem[], indicators: Indicator[]): DataListItem[] {
return list.reduce<typeof list>((m, row) => {
if (filterRow(row, indicators)) {
m.push(row);
}
return m;
}, []);
}
/**
* Copyright 2020 Baidu Inc. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import * as d3 from 'd3';
import type {DataListItem, Indicator, IndicatorGroup, IndicatorRaw, ListItem} from './types';
export const COLOR_MAP = ['#2932E1', '#FE4A3B', '#FFAA00'];
export function format(list: DataListItem[], indicators: Indicator[]): ListItem[] {
return list.map(row =>
indicators.reduce<ListItem>(
(m, {name, group, type}) => {
switch (type) {
case 'string':
m[group][name] = row[group][name] + '';
break;
case 'numeric':
case 'continuous':
m[group][name] = Number.parseFloat(row[group][name] + '') + '';
break;
}
return m;
},
{name: row.name, hparams: {}, metrics: {}}
)
);
}
export function formatIndicators(indicators: IndicatorRaw[], group: IndicatorGroup): Indicator[] {
return indicators.map(indicator => {
switch (indicator.type) {
case 'numeric':
case 'string':
return {
...indicator,
group,
selected: true,
selectedValues: [...indicator.values] as string[] | number[]
};
case 'continuous':
return {
...indicator,
group,
selected: true,
min: Number.NEGATIVE_INFINITY,
max: Number.POSITIVE_INFINITY
};
default:
return null as never;
}
});
}
export function getColorScale(indicator: Indicator | null, data: DataListItem[]): d3.ScaleLinear<string, string> {
let domain: number[];
if (indicator == null) {
domain = [0, data.length - 1];
} else if (indicator.type !== 'continuous') {
throw new Error('cannot color lines by `' + indicator.name + '`');
} else {
const values = data.map(row => +row[indicator.group][indicator.name]);
domain = d3.extent(values) as [number, number];
}
const min = domain[0];
const max = domain.pop() as number;
for (let i = 1; i < COLOR_MAP.length - 1; i++) {
domain.push(min + ((max - min) / (COLOR_MAP.length - 1)) * i);
}
domain.push(max);
return (
d3
.scaleLinear<string, string>()
.domain(domain)
.range(COLOR_MAP)
// d3 types sucks
// eslint-disable-next-line @typescript-eslint/no-explicit-any
.interpolate(d3.interpolateRgb as any)
);
}
/**
* Copyright 2020 Baidu Inc. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type {Indicator, ListItem} from '../types';
import {useCallback, useMemo, useState} from 'react';
export default function useGraph(indicators: Indicator[], list: ListItem[]) {
const selectedIndicators = useMemo(() => indicators.filter(i => i.selected), [indicators]);
const [sessionData, setSessionData] = useState<ListItem | null>(null);
const [showMetricsGraph, setShowMetricsGraph] = useState(false);
const onHover = useCallback(
(index: number | null) => {
if (!showMetricsGraph) {
setSessionData(index == null ? null : list[index]);
}
},
[list, showMetricsGraph]
);
const onSelect = useCallback(
(index: number | null) => {
setSessionData(index == null ? null : list[index]);
setShowMetricsGraph(index != null);
},
[list]
);
return {
selectedIndicators,
sessionData,
onHover,
onSelect,
showMetricsGraph
};
}
/**
* Copyright 2020 Baidu Inc. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
// cspell:words quantile
import {ScaleMethod} from './types';
export type {
DataListItem,
ImportanceData,
Indicator,
IndicatorData,
IndicatorGroup,
IndicatorRaw,
IndicatorType,
ListItem,
MetricData,
Range,
ViewData
} from './types';
export {OrderDirection, ScaleMethod} from './types';
export {format, formatIndicators, getColorScale, COLOR_MAP} from './format';
export {filter} from './filter';
export {calculateRelativeTime, chartData} from './metric';
export {default as useGraph} from './hooks/useGraph';
export const DEFAULT_ORDER_INDICATOR = Symbol('DEFAULT_ORDER_INDICATOR');
export const DND_TYPE = Symbol('DND_TYPE');
export const SCALE_METHODS = [ScaleMethod.LINEAR, ScaleMethod.LOGARITHMIC, ScaleMethod.QUANTILE];
/**
* Copyright 2020 Baidu Inc. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type {EChartOption} from 'echarts';
import type {MetricData} from './types';
import type {Run} from '~/types';
export function calculateRelativeTime(data: MetricData[]) {
let startTime = 0;
return data.map((row, index) => {
const time = Math.floor(row[0]);
if (index === 0) {
startTime = time;
}
const relative = time - startTime;
return [time, row[1], row[2], relative];
});
}
export function chartData(data: number[][], run: Run): EChartOption.SeriesLine[] {
const name = run.label;
const color = run.colors[0];
return [
{
name,
itemStyle: {
color
},
lineStyle: {
color
},
data,
encode: {
x: [1],
y: [2]
}
}
];
}
/**
* Copyright 2020 Baidu Inc. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
// cspell:words quantile
export type {Range} from '~/types';
export enum OrderDirection {
ASCENDING = 'asc',
DESCENDING = 'desc'
}
export enum ScaleMethod {
LINEAR = 'linear',
LOGARITHMIC = 'logarithmic',
QUANTILE = 'quantile'
}
export type IndicatorType = 'string' | 'numeric' | 'continuous';
export type IndicatorGroup = 'hparams' | 'metrics';
interface IndicatorBase<T extends IndicatorType> {
name: string;
type: T;
}
interface DiscreteIndicator<T extends 'string' | 'numeric'> extends IndicatorBase<T> {
values: T extends 'string' ? string[] : T extends 'numeric' ? number[] : never[];
}
type DiscreteStringIndicator = DiscreteIndicator<'string'>;
type DiscreteNumericIndicator = DiscreteIndicator<'numeric'>;
type ContinuousIndicator = IndicatorBase<'continuous'>;
// eslint-disable-next-line @typescript-eslint/ban-types
export type IndicatorRaw<T extends {} = {}> = (
| DiscreteStringIndicator
| DiscreteNumericIndicator
| ContinuousIndicator
) &
T;
type IndicatorWithGroup = IndicatorRaw & {
group: IndicatorGroup;
};
interface FilterData {
selected: boolean;
min?: number;
max?: number;
selectedValues?: number[] | string[];
}
export type Indicator = IndicatorWithGroup & FilterData;
// eslint-disable-next-line @typescript-eslint/ban-types
export interface IndicatorData<T extends {} = {}> {
hparams: IndicatorRaw<T>[];
metrics: IndicatorRaw<T>[];
}
export interface DataListItem<T extends string | number = string | number> {
name: string;
hparams: Record<string, T>;
metrics: Record<string, T>;
}
export type ListItem = DataListItem<string>;
export interface ViewData {
indicators: Indicator[];
list: ListItem[];
data: DataListItem[];
}
export interface ImportanceData {
name: string;
value: number;
}
type Value = number;
type WallTime = number;
type Step = number;
export type MetricData = [WallTime, Step, Value];
...@@ -17,36 +17,16 @@ ...@@ -17,36 +17,16 @@
// cSpell:words maxs coord // cSpell:words maxs coord
import type {Dataset, Range, TooltipData, XAxis} from './types'; import type {Dataset, Range, TooltipData, XAxis} from './types';
import {formatTime, humanizeDuration} from '~/utils';
import type {EChartOption} from 'echarts';
import type I18n from 'i18next'; import type I18n from 'i18next';
import type {Run} from '~/types'; import type {Run} from '~/types';
import {format} from 'd3-format'; import {format} from 'd3-format';
import {formatTime} from '~/utils';
import moment from 'moment';
import {xAxisMap} from './index'; import {xAxisMap} from './index';
const valueFormatter = format('.5'); const valueFormatter = format('.5');
const humanizeDuration = (ms: number) => {
const time = moment.duration(ms);
const hour = time.hours();
if (hour) {
time.subtract(hour, 'hour');
}
const minute = time.minutes();
if (minute) {
time.subtract(minute, 'minute');
}
const second = time.asSeconds();
let str = Math.floor(second) + 's';
if (hour) {
str = `${hour}h${minute}m${str}`;
} else if (minute) {
str = `${minute}m${str}`;
}
return str;
};
export const options = { export const options = {
legend: { legend: {
data: [] data: []
...@@ -68,7 +48,7 @@ export const chartData = ({ ...@@ -68,7 +48,7 @@ export const chartData = ({
runs: Run[]; runs: Run[];
xAxis: XAxis; xAxis: XAxis;
smoothedOnly?: boolean; smoothedOnly?: boolean;
}) => }): EChartOption.SeriesLine[] =>
data data
.map((dataset, i) => { .map((dataset, i) => {
// smoothed data: // smoothed data:
...@@ -152,7 +132,7 @@ export const tooltip = (data: TooltipData[], stepLength: number, i18n: typeof I1 ...@@ -152,7 +132,7 @@ export const tooltip = (data: TooltipData[], stepLength: number, i18n: typeof I1
width: '5em' width: '5em'
}, },
{ {
label: i18n.t('scalar:value'), label: i18n.t('common:scalar-value'),
width: '4.285714286em' width: '4.285714286em'
}, },
{ {
......
...@@ -16,6 +16,8 @@ ...@@ -16,6 +16,8 @@
import {Run, TimeMode} from '~/types'; import {Run, TimeMode} from '~/types';
export type {Range} from '~/types';
type Value = number; type Value = number;
type WallTime = number; type WallTime = number;
type Step = number; type Step = number;
...@@ -33,11 +35,6 @@ export enum SortingMethod { ...@@ -33,11 +35,6 @@ export enum SortingMethod {
Nearest = 'nearest' Nearest = 'nearest'
} }
export type Range = {
min: number;
max: number;
};
export type TooltipData = { export type TooltipData = {
run: Run; run: Run;
item: Dataset[number]; item: Dataset[number];
......
...@@ -97,6 +97,11 @@ const routes: Route[] = [ ...@@ -97,6 +97,11 @@ const routes: Route[] = [
id: Pages.ROCCurve, id: Pages.ROCCurve,
path: '/roc-curve', path: '/roc-curve',
component: React.lazy(() => import('~/pages/curves/roc')) component: React.lazy(() => import('~/pages/curves/roc'))
},
{
id: Pages.HyperParameter,
path: '/hyper-parameter',
component: React.lazy(() => import('~/pages/hyper-parameter'))
} }
]; ];
......
...@@ -43,6 +43,11 @@ export enum TimeMode { ...@@ -43,6 +43,11 @@ export enum TimeMode {
export type Point2D = [number, number]; export type Point2D = [number, number];
export type Point3D = [number, number, number]; export type Point3D = [number, number, number];
export interface Range {
min: number;
max: number;
}
export enum HighDimensionalColorType { export enum HighDimensionalColorType {
Null, Null,
Value, Value,
......
...@@ -151,15 +151,15 @@ export const xAxis = { ...@@ -151,15 +151,15 @@ export const xAxis = {
formatter: format('.4') formatter: format('.4')
}, },
splitLine: { splitLine: {
show: false lineStyle: {
}, color: '#EEE'
splitNumber: 5 }
}
}; };
export const yAxis = { export const yAxis = {
type: 'value', type: 'value',
name: '', name: '',
splitNumber: 4,
nameTextStyle: { nameTextStyle: {
fontSize: 12, fontSize: 12,
color: '#666' color: '#666'
...@@ -186,9 +186,5 @@ export const yAxis = { ...@@ -186,9 +186,5 @@ export const yAxis = {
export const series = { export const series = {
hoverAnimation: false, hoverAnimation: false,
animationDuration: 100, animationDuration: 100
lineStyle: {
color: colors.primary.default,
width: 1.5
}
}; };
...@@ -22,6 +22,26 @@ import moment from 'moment'; ...@@ -22,6 +22,26 @@ import moment from 'moment';
export const formatTime = (value: number, language: string, formatter = 'L LTS') => export const formatTime = (value: number, language: string, formatter = 'L LTS') =>
moment(Math.floor(value), 'x').locale(language).format(formatter); moment(Math.floor(value), 'x').locale(language).format(formatter);
export const humanizeDuration = (ms: number) => {
const time = moment.duration(ms);
const hour = time.hours();
if (hour) {
time.subtract(hour, 'hour');
}
const minute = time.minutes();
if (minute) {
time.subtract(minute, 'minute');
}
const second = time.asSeconds();
let str = Math.floor(second) + 's';
if (hour) {
str = `${hour}h${minute}m${str}`;
} else if (minute) {
str = `${minute}m${str}`;
}
return str;
};
export const quantile = (values: number[], p: number) => { export const quantile = (values: number[], p: number) => {
const n = values.length; const n = values.length;
if (!n) { if (!n) {
......
...@@ -121,6 +121,34 @@ export const link = css` ...@@ -121,6 +121,34 @@ export const link = css`
} }
`; `;
export const dragger = css`
--padding-v: ${em(8, 14)};
--padding-h: ${em(6, 14)};
width: ${em(6, 14)};
height: ${em(10, 14)};
box-sizing: content-box;
padding: var(--padding-v) var(--padding-h);
cursor: grab;
display: inline-block;
position: relative;
&::before {
--dot-size: ${em(2, 14)};
content: '';
display: block;
position: absolute;
width: var(--dot-size);
height: var(--dot-size);
background-color: currentColor;
top: var(--padding-v);
left: var(--padding-h);
box-shadow: 0 0, calc(var(--dot-size) * 2) 0, 0 calc(var(--dot-size) * 2),
calc(var(--dot-size) * 2) calc(var(--dot-size) * 2), 0 calc(var(--dot-size) * 4),
calc(var(--dot-size) * 2) calc(var(--dot-size) * 4);
}
`;
const spinner = keyframes` const spinner = keyframes`
0% { 0% {
transform: rotate(0deg); transform: rotate(0deg);
......
...@@ -46,27 +46,33 @@ export const colors = { ...@@ -46,27 +46,33 @@ export const colors = {
export const themes = { export const themes = {
light: { light: {
// text colors
textColor: '#333', textColor: '#333',
textLightColor: '#666', textLightColor: '#666',
textLighterColor: '#999', textLighterColor: '#999',
textInvertColor: '#fff', textInvertColor: '#fff',
// background colors
backgroundColor: '#fff', backgroundColor: '#fff',
backgroundFocusedColor: '#f6f6f6', backgroundFocusedColor: '#f6f6f6',
bodyBackgroundColor: '#f4f4f4', bodyBackgroundColor: '#f4f4f4',
// border colors
borderColor: '#ddd', borderColor: '#ddd',
borderFocusedColor: darken(0.15, '#ddd'), borderFocusedColor: darken(0.15, '#ddd'),
borderActiveColor: darken(0.3, '#ddd'), borderActiveColor: darken(0.3, '#ddd'),
// loader colors
loaderBackgroundColor: '#f5f6f7', loaderBackgroundColor: '#f5f6f7',
loaderForegroundColor: '#eee', loaderForegroundColor: '#eee',
// navbar colors
navbarTextColor: '#fff', navbarTextColor: '#fff',
navbarBackgroundColor: '#1527c2', navbarBackgroundColor: '#1527c2',
navbarHoverBackgroundColor: lighten(0.05, '#1527c2'), navbarHoverBackgroundColor: lighten(0.05, '#1527c2'),
navbarHighlightColor: '#596cd6', navbarHighlightColor: '#596cd6',
// components colors
tagBackgroundColor: '#f4f5fc', tagBackgroundColor: '#f4f5fc',
tagFocusedBackgroundColor: darken(0.03, '#f4f5fc'), tagFocusedBackgroundColor: darken(0.03, '#f4f5fc'),
tagActiveBackgroundColor: darken(0.06, '#f4f5fc'), tagActiveBackgroundColor: darken(0.06, '#f4f5fc'),
...@@ -83,20 +89,38 @@ export const themes = { ...@@ -83,20 +89,38 @@ export const themes = {
progressBarColor: '#fff', progressBarColor: '#fff',
maskColor: 'rgba(255, 255, 255, 0.8)', maskColor: 'rgba(255, 255, 255, 0.8)',
darkMaskColor: 'rgba(0, 0, 0, 0.5)', darkMaskColor: 'rgba(0, 0, 0, 0.5)',
tabInactiveBackgroundColor: '#ebebeb',
tableBackgroundColor: '#fff',
tableHeaderBackgroundColor: '#f9f9f9',
tableDraggerColor: '#6f6f6f',
tableRowHoverBackgroundColor: '#f6f6f6',
tableExpanderColor: '#999',
tableExpanderHoverColor: '#666',
tableExpanderBorderColor: '#ccc',
tableExpanderHoverBorderColor: '#999',
tableStickyShadowColor: '#f3f3f3',
// graph page
graphUploaderBackgroundColor: '#f9f9f9', graphUploaderBackgroundColor: '#f9f9f9',
graphUploaderActiveBackgroundColor: '#f2f6ff', graphUploaderActiveBackgroundColor: '#f2f6ff',
graphCopyrightColor: '#ddd', graphCopyrightColor: '#ddd',
graphCopyrightLogoFilter: 'opacity(25%)', graphCopyrightLogoFilter: 'opacity(25%)',
// text sample page
textChartTitleBackgroundColor: '#f8f8f8', textChartTitleBackgroundColor: '#f8f8f8',
textChartTitleIndicatorColor: '#000', textChartTitleIndicatorColor: '#000',
textChartTagBackgroundColor: '#f6f6f6' textChartTagBackgroundColor: '#f6f6f6',
// hyper-parameter page
hyperParameterGraphAxisColor: '#ccc',
hyperParameterGraphGridColor: '#ebebeb',
hyperParameterGraphDisabledDataColor: '#ccc',
hyperParameterGraphBrushColor: '#787878'
}, },
dark: { dark: {
textColor: '#cfcfd1', textColor: '#cfcfd1',
textLightColor: '#575757', textLightColor: '#757575',
textLighterColor: '#757575', textLighterColor: '#575757',
textInvertColor: '#000', textInvertColor: '#000',
backgroundColor: '#1d1d1f', backgroundColor: '#1d1d1f',
...@@ -131,6 +155,16 @@ export const themes = { ...@@ -131,6 +155,16 @@ export const themes = {
progressBarColor: '#fff', progressBarColor: '#fff',
maskColor: 'rgba(0, 0, 0, 0.8)', maskColor: 'rgba(0, 0, 0, 0.8)',
darkMaskColor: 'rgba(0, 0, 0, 0.8)', darkMaskColor: 'rgba(0, 0, 0, 0.8)',
tabInactiveBackgroundColor: '#262629',
tableBackgroundColor: '#1d1d1f',
tableHeaderBackgroundColor: '#202020',
tableDraggerColor: '#6f6f6f',
tableRowHoverBackgroundColor: '#232323',
tableExpanderColor: '#757575',
tableExpanderHoverColor: '#999',
tableExpanderBorderColor: '#444',
tableExpanderHoverBorderColor: '#757575',
tableStickyShadowColor: '#131313',
graphUploaderBackgroundColor: '#262629', graphUploaderBackgroundColor: '#262629',
graphUploaderActiveBackgroundColor: '#303033', graphUploaderActiveBackgroundColor: '#303033',
...@@ -140,7 +174,12 @@ export const themes = { ...@@ -140,7 +174,12 @@ export const themes = {
textChartTitleBackgroundColor: '#2a2a2a', textChartTitleBackgroundColor: '#2a2a2a',
textChartTitleIndicatorColor: '#fff', textChartTitleIndicatorColor: '#fff',
textChartTagBackgroundColor: '#222' textChartTagBackgroundColor: '#222',
hyperParameterGraphAxisColor: '#999',
hyperParameterGraphGridColor: '#262629',
hyperParameterGraphDisabledDataColor: '#999',
hyperParameterGraphBrushColor: '#787878'
} }
} as const; } as const;
......
/**
* Copyright 2020 Baidu Inc. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
// cSpell:words unobserves
/**
* The **ResizeObserver** interface reports changes to the dimensions of an
* [Element](https://developer.mozilla.org/en-US/docs/Web/API/Element)'s content
* or border box, or the bounding box of an
* [SVGElement](https://developer.mozilla.org/en-US/docs/Web/API/SVGElement).
*
* > **Note**: The content box is the box in which content can be placed,
* > meaning the border box minus the padding and border width. The border box
* > encompasses the content, padding, and border. See
* > [The box model](https://developer.mozilla.org/en-US/docs/Learn/CSS/Building_blocks/The_box_model)
* > for further explanation.
*
* `ResizeObserver` avoids infinite callback loops and cyclic dependencies that
* are often created when resizing via a callback function. It does this by only
* processing elements deeper in the DOM in subsequent frames. Implementations
* should, if they follow the specification, invoke resize events before paint
* and after layout.
*
* @see https://developer.mozilla.org/en-US/docs/Web/API/ResizeObserver
*/
declare class ResizeObserver {
/**
* The **ResizeObserver** constructor creates a new `ResizeObserver` object,
* which can be used to report changes to the content or border box of an
* `Element` or the bounding box of an `SVGElement`.
*
* @example
* var ResizeObserver = new ResizeObserver(callback)
*
* @param callback
* The function called whenever an observed resize occurs. The function is
* called with two parameters:
* * **entries**
* An array of
* [ResizeObserverEntry](https://developer.mozilla.org/en-US/docs/Web/API/ResizeObserverEntry)
* objects that can be used to access the new dimensions of the element
* after each change.
* * **observer**
* A reference to the `ResizeObserver` itself, so it will definitely be
* accessible from inside the callback, should you need it. This could be
* used for example to automatically unobserve the observer when a certain
* condition is reached, but you can omit it if you don't need it.
*
* The callback will generally follow a pattern along the lines of:
* ```js
* function(entries, observer) {
* for (let entry of entries) {
* // Do something to each entry
* // and possibly something to the observer itself
* }
* }
* ```
*
* The following snippet is taken from the
* [resize-observer-text.html](https://mdn.github.io/dom-examples/resize-observer/resize-observer-text.html)
* ([see source](https://github.com/mdn/dom-examples/blob/master/resize-observer/resize-observer-text.html))
* example:
* @example
* const resizeObserver = new ResizeObserver(entries => {
* for (let entry of entries) {
* if(entry.contentBoxSize) {
* h1Elem.style.fontSize = Math.max(1.5, entry.contentBoxSize.inlineSize/200) + 'rem';
* pElem.style.fontSize = Math.max(1, entry.contentBoxSize.inlineSize/600) + 'rem';
* } else {
* h1Elem.style.fontSize = Math.max(1.5, entry.contentRect.width/200) + 'rem';
* pElem.style.fontSize = Math.max(1, entry.contentRect.width/600) + 'rem';
* }
* }
* });
*
* resizeObserver.observe(divElem);
*/
constructor(callback: ResizeObserverCallback);
/**
* The **disconnect()** method of the
* [ResizeObserver](https://developer.mozilla.org/en-US/docs/Web/API/ResizeObserver)
* interface unobserves all observed
* [Element](https://developer.mozilla.org/en-US/docs/Web/API/Element) or
* [SVGElement](https://developer.mozilla.org/en-US/docs/Web/API/SVGElement)
* targets.
*/
disconnect: () => void;
/**
* The `observe()` method of the
* [ResizeObserver](https://developer.mozilla.org/en-US/docs/Web/API/ResizeObserver)
* interface starts observing the specified
* [Element](https://developer.mozilla.org/en-US/docs/Web/API/Element) or
* [SVGElement](https://developer.mozilla.org/en-US/docs/Web/API/SVGElement).
*
* @example
* resizeObserver.observe(target, options);
*
* @param target
* A reference to an
* [Element](https://developer.mozilla.org/en-US/docs/Web/API/Element) or
* [SVGElement](https://developer.mozilla.org/en-US/docs/Web/API/SVGElement)
* to be observed.
*
* @param options
* An options object allowing you to set options for the observation.
* Currently this only has one possible option that can be set.
*/
observe: (target: Element, options?: ResizeObserverObserveOptions) => void;
/**
* The **unobserve()** method of the
* [ResizeObserver](https://developer.mozilla.org/en-US/docs/Web/API/ResizeObserver)
* interface ends the observing of a specified
* [Element](https://developer.mozilla.org/en-US/docs/Web/API/Element) or
* [SVGElement](https://developer.mozilla.org/en-US/docs/Web/API/SVGElement).
*/
unobserve: (target: Element) => void;
}
interface ResizeObserverObserveOptions {
/**
* Sets which box model the observer will observe changes to. Possible values
* are `content-box` (the default), and `border-box`.
*
* @default "content-box"
*/
box?: 'content-box' | 'border-box';
}
/**
* The function called whenever an observed resize occurs. The function is
* called with two parameters:
*
* @param entries
* An array of
* [ResizeObserverEntry](https://developer.mozilla.org/en-US/docs/Web/API/ResizeObserverEntry)
* objects that can be used to access the new dimensions of the element after
* each change.
*
* @param observer
* A reference to the `ResizeObserver` itself, so it will definitely be
* accessible from inside the callback, should you need it. This could be used
* for example to automatically unobserve the observer when a certain condition
* is reached, but you can omit it if you don't need it.
*
* The callback will generally follow a pattern along the lines of:
* @example
* function(entries, observer) {
* for (let entry of entries) {
* // Do something to each entry
* // and possibly something to the observer itself
* }
* }
*
* @example
* const resizeObserver = new ResizeObserver(entries => {
* for (let entry of entries) {
* if(entry.contentBoxSize) {
* h1Elem.style.fontSize = Math.max(1.5, entry.contentBoxSize.inlineSize/200) + 'rem';
* pElem.style.fontSize = Math.max(1, entry.contentBoxSize.inlineSize/600) + 'rem';
* } else {
* h1Elem.style.fontSize = Math.max(1.5, entry.contentRect.width/200) + 'rem';
* pElem.style.fontSize = Math.max(1, entry.contentRect.width/600) + 'rem';
* }
* }
* });
*
* resizeObserver.observe(divElem);
*/
type ResizeObserverCallback = (entries: ResizeObserverEntry[], observer: ResizeObserver) => void;
/**
* The **ResizeObserverEntry** interface represents the object passed to the
* [ResizeObserver()](https://developer.mozilla.org/en-US/docs/Web/API/ResizeObserver/ResizeObserver)
* constructor's callback function, which allows you to access the new
* dimensions of the
* [Element](https://developer.mozilla.org/en-US/docs/Web/API/Element) or
* [SVGElement](https://developer.mozilla.org/en-US/docs/Web/API/SVGElement)
* being observed.
*/
interface ResizeObserverEntry {
/**
* An object containing the new border box size of the observed element when
* the callback is run.
*/
readonly borderBoxSize: ResizeObserverEntryBoxSize;
/**
* An object containing the new content box size of the observed element when
* the callback is run.
*/
readonly contentBoxSize: ResizeObserverEntryBoxSize;
/**
* A [DOMRectReadOnly](https://developer.mozilla.org/en-US/docs/Web/API/DOMRectReadOnly)
* object containing the new size of the observed element when the callback is
* run. Note that this is better supported than the above two properties, but
* it is left over from an earlier implementation of the Resize Observer API,
* is still included in the spec for web compat reasons, and may be deprecated
* in future versions.
*/
// node_modules/typescript/lib/lib.dom.d.ts
readonly contentRect: DOMRectReadOnly;
/**
* A reference to the
* [Element](https://developer.mozilla.org/en-US/docs/Web/API/Element) or
* [SVGElement](https://developer.mozilla.org/en-US/docs/Web/API/SVGElement)
* being observed.
*/
readonly target: Element;
}
/**
* The **borderBoxSize** read-only property of the
* [ResizeObserverEntry](https://developer.mozilla.org/en-US/docs/Web/API/ResizeObserverEntry)
* interface returns an object containing the new border box size of the
* observed element when the callback is run.
*/
interface ResizeObserverEntryBoxSize {
/**
* The length of the observed element's border box in the block dimension. For
* boxes with a horizontal
* [writing-mode](https://developer.mozilla.org/en-US/docs/Web/CSS/writing-mode),
* this is the vertical dimension, or height; if the writing-mode is vertical,
* this is the horizontal dimension, or width.
*/
blockSize: number;
/**
* The length of the observed element's border box in the inline dimension.
* For boxes with a horizontal
* [writing-mode](https://developer.mozilla.org/en-US/docs/Web/CSS/writing-mode),
* this is the horizontal dimension, or width; if the writing-mode is
* vertical, this is the vertical dimension, or height.
*/
inlineSize: number;
}
interface Window {
ResizeObserver: ResizeObserver;
}
/**
* Copyright 2020 Baidu Inc. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/* eslint-disable @typescript-eslint/no-explicit-any */
import {
UseColumnOrderInstanceProps,
UseColumnOrderState,
UseExpandedHooks,
UseExpandedInstanceProps,
UseExpandedOptions,
UseExpandedRowProps,
UseExpandedState,
UseFiltersColumnOptions,
UseFiltersColumnProps,
UseFiltersInstanceProps,
UseFiltersOptions,
UseFiltersState,
UseGlobalFiltersColumnOptions,
UseGlobalFiltersInstanceProps,
UseGlobalFiltersOptions,
UseGlobalFiltersState,
UseGroupByCellProps,
UseGroupByColumnOptions,
UseGroupByColumnProps,
UseGroupByHooks,
UseGroupByInstanceProps,
UseGroupByOptions,
UseGroupByRowProps,
UseGroupByState,
UsePaginationInstanceProps,
UsePaginationOptions,
UsePaginationState,
UseResizeColumnsColumnOptions,
UseResizeColumnsColumnProps,
UseResizeColumnsOptions,
UseResizeColumnsState,
UseRowSelectHooks,
UseRowSelectInstanceProps,
UseRowSelectOptions,
UseRowSelectRowProps,
UseRowSelectState,
UseRowStateCellProps,
UseRowStateInstanceProps,
UseRowStateOptions,
UseRowStateRowProps,
UseRowStateState,
UseSortByColumnOptions,
UseSortByColumnProps,
UseSortByHooks,
UseSortByInstanceProps,
UseSortByOptions,
UseSortByState
} from 'react-table';
import {CSSProperties} from 'react';
declare module 'react-table' {
// take this file as-is, or comment out the sections that don't apply to your plugin configuration
export interface TableOptions<D extends Record<string, unknown>>
extends UseExpandedOptions<D>,
UseFiltersOptions<D>,
UseGlobalFiltersOptions<D>,
UseGroupByOptions<D>,
UsePaginationOptions<D>,
UseResizeColumnsOptions<D>,
UseRowSelectOptions<D>,
UseRowStateOptions<D>,
UseSortByOptions<D>,
// note that having Record here allows you to add anything to the options, this matches the spirit of the
// underlying js library, but might be cleaner if it's replaced by a more specific type that matches your
// feature set, this is a safe default.
Record<string, any> {}
export interface Hooks<D extends Record<string, unknown> = Record<string, unknown>>
extends UseExpandedHooks<D>,
UseGroupByHooks<D>,
UseRowSelectHooks<D>,
UseSortByHooks<D> {}
export interface TableInstance<D extends Record<string, unknown> = Record<string, unknown>>
extends UseColumnOrderInstanceProps<D>,
UseExpandedInstanceProps<D>,
UseFiltersInstanceProps<D>,
UseGlobalFiltersInstanceProps<D>,
UseGroupByInstanceProps<D>,
UsePaginationInstanceProps<D>,
UseRowSelectInstanceProps<D>,
UseRowStateInstanceProps<D>,
UseSortByInstanceProps<D> {}
export interface TableState<D extends Record<string, unknown> = Record<string, unknown>>
extends UseColumnOrderState<D>,
UseExpandedState<D>,
UseFiltersState<D>,
UseGlobalFiltersState<D>,
UseGroupByState<D>,
UsePaginationState<D>,
UseResizeColumnsState<D>,
UseRowSelectState<D>,
UseRowStateState<D>,
UseSortByState<D> {}
export interface ColumnInterface<D extends Record<string, unknown> = Record<string, unknown>>
extends UseFiltersColumnOptions<D>,
UseGlobalFiltersColumnOptions<D>,
UseGroupByColumnOptions<D>,
UseResizeColumnsColumnOptions<D>,
UseSortByColumnOptions<D> {
sticky?: 'left' | 'right';
draggable?: boolean;
}
export interface ColumnInstance<D extends Record<string, unknown> = Record<string, unknown>>
extends UseFiltersColumnProps<D>,
UseGroupByColumnProps<D>,
UseResizeColumnsColumnProps<D>,
UseSortByColumnProps<D> {
className?: string;
style?: CSSProperties;
}
export interface Cell<D extends Record<string, unknown> = Record<string, unknown>, V = any>
extends UseGroupByCellProps<D>,
UseRowStateCellProps<D> {}
export interface Row<D extends Record<string, unknown> = Record<string, unknown>>
extends UseExpandedRowProps<D>,
UseGroupByRowProps<D>,
UseRowSelectRowProps<D>,
UseRowStateRowProps<D> {}
}
/**
* Copyright 2020 Baidu Inc. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {Request, Response} from 'express';
export default (req: Request, res: Response) => {
const {type} = req.query;
switch (type) {
case 'tsv':
res.setHeader('Content-Type', 'text/tab-separated-values');
break;
case 'csv':
res.setHeader('Content-Type', 'text/comma-separated-values');
break;
}
return `hparams\n${type}`;
};
/**
* Copyright 2020 Baidu Inc. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* GET hparams/importance
*
* request
* {}
*/
export default [
{
name: 'param1',
value: 0.4
},
{
name: 'param2',
value: 0.3
},
{
name: 'param3',
value: 0.2
},
{
name: 'param4',
value: 0.1
}
];
/**
* Copyright 2020 Baidu Inc. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* GET hparams/indicators
*
* request
* {}
*/
export default {
hparams: [
{
name: 'lr',
type: 'string',
values: ['a', 'b', 'c', 'd']
},
{
name: 'bsize',
type: 'continuous'
}
],
metrics: [
{
name: 'accuracy',
type: 'numeric',
values: [1, 2, 3, 4, 5]
},
{
name: 'loss',
type: 'continuous'
}
]
};
/**
* Copyright 2020 Baidu Inc. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* GET hparams/list
*
* request
* {}
*/
const a = ['a', 'b', 'c', 'd'];
const b = [3, 2, 1, 4, 5];
export default () =>
Array(5)
.fill(undefined)
.map((_, index) => ({
name: `run${index}`,
hparams: {
lr: a[index % a.length],
bsize: index * 0.5 + 0.5
},
metrics: {
accuracy: b[index % b.length],
loss: 100 - index * 0.2
}
}));
/**
* Copyright 2020 Baidu Inc. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* GET hparams/metric
*
* request
* {
* run: "run1",
* metric: "accuracy"
* }
*/
export default [
[1511842145705.075, 1, 0.05000000074505806],
[1511842145738.8, 2, 0.12999999523162842],
[1511842145774.563, 3, 0.27000001072883606],
[1511842145806.828, 4, 0.4399999976158142],
[1511842145838.082, 5, 0.47999998927116394],
[1511842145868.955, 6, 0.5899999737739563],
[1511842145899.323, 7, 0.6100000143051147],
[1511842145930.518, 8, 0.699999988079071]
];
...@@ -3178,6 +3178,21 @@ ...@@ -3178,6 +3178,21 @@
resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.9.1.tgz#7f554e7368c9ab679a11f4a042ca17149d70cf12" resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.9.1.tgz#7f554e7368c9ab679a11f4a042ca17149d70cf12"
integrity sha512-DvJbbn3dUgMxDnJLH+RZQPnXak1h4ZVYQ7CWiFWjQwBFkVajT4rfw2PdpHLTSTwxrYfnoEXkuBiwkDm6tPMQeA== integrity sha512-DvJbbn3dUgMxDnJLH+RZQPnXak1h4ZVYQ7CWiFWjQwBFkVajT4rfw2PdpHLTSTwxrYfnoEXkuBiwkDm6tPMQeA==
"@react-dnd/asap@^4.0.0":
version "4.0.0"
resolved "https://registry.yarnpkg.com/@react-dnd/asap/-/asap-4.0.0.tgz#b300eeed83e9801f51bd66b0337c9a6f04548651"
integrity sha512-0XhqJSc6pPoNnf8DhdsPHtUhRzZALVzYMTzRwV4VI6DJNJ/5xxfL9OQUwb8IH5/2x7lSf7nAZrnzUD+16VyOVQ==
"@react-dnd/invariant@^2.0.0":
version "2.0.0"
resolved "https://registry.yarnpkg.com/@react-dnd/invariant/-/invariant-2.0.0.tgz#09d2e81cd39e0e767d7da62df9325860f24e517e"
integrity sha512-xL4RCQBCBDJ+GRwKTFhGUW8GXa4yoDfJrPbLblc3U09ciS+9ZJXJ3Qrcs/x2IODOdIE5kQxvMmE2UKyqUictUw==
"@react-dnd/shallowequal@^2.0.0":
version "2.0.0"
resolved "https://registry.yarnpkg.com/@react-dnd/shallowequal/-/shallowequal-2.0.0.tgz#a3031eb54129f2c66b2753f8404266ec7bf67f0a"
integrity sha512-Pc/AFTdwZwEKJxFJvlxrSmGe/di+aAOBn60sremrpLo6VI/6cmiUYNNwlI5KNYttg7uypzA3ILPMPgxB2GYZEg==
"@rollup/plugin-alias@^3.0.1": "@rollup/plugin-alias@^3.0.1":
version "3.1.1" version "3.1.1"
resolved "https://registry.yarnpkg.com/@rollup/plugin-alias/-/plugin-alias-3.1.1.tgz#bb96cf37fefeb0a953a6566c284855c7d1cd290c" resolved "https://registry.yarnpkg.com/@rollup/plugin-alias/-/plugin-alias-3.1.1.tgz#bb96cf37fefeb0a953a6566c284855c7d1cd290c"
...@@ -4092,6 +4107,13 @@ ...@@ -4092,6 +4107,13 @@
"@types/history" "*" "@types/history" "*"
"@types/react" "*" "@types/react" "*"
"@types/react-table@7.0.29":
version "7.0.29"
resolved "https://registry.yarnpkg.com/@types/react-table/-/react-table-7.0.29.tgz#af2c82f2d6a39be5bc0f191b30501309a8db0949"
integrity sha512-RCGVKGlTDv3jbj37WJ5HhN3sPb0W/2rqlvyGUtvawnnyrxgI2BGgASvU93rq2jwanVp5J9l1NYAeiGlNhdaBGw==
dependencies:
"@types/react" "*"
"@types/react@*": "@types/react@*":
version "17.0.0" version "17.0.0"
resolved "https://registry.yarnpkg.com/@types/react/-/react-17.0.0.tgz#5af3eb7fad2807092f0046a1302b7823e27919b8" resolved "https://registry.yarnpkg.com/@types/react/-/react-17.0.0.tgz#5af3eb7fad2807092f0046a1302b7823e27919b8"
...@@ -5549,6 +5571,11 @@ class-utils@^0.3.5: ...@@ -5549,6 +5571,11 @@ class-utils@^0.3.5:
isobject "^3.0.0" isobject "^3.0.0"
static-extend "^0.1.1" static-extend "^0.1.1"
classnames@2.3.1:
version "2.3.1"
resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.3.1.tgz#dfcfa3891e306ec1dad105d0e88f4417b8535e8e"
integrity sha512-OlQdbZ7gLfGarSqxesMesDa5uz7KFbID8Kpq/SxIoNGDqY8lSYs0D+hhtBXhcdB3rcbXArFr7vlHheLk1voeNA==
classnames@^2.2.3: classnames@^2.2.3:
version "2.2.6" version "2.2.6"
resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.2.6.tgz#43935bffdd291f326dad0a205309b38d00f650ce" resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.2.6.tgz#43935bffdd291f326dad0a205309b38d00f650ce"
...@@ -7001,6 +7028,15 @@ dir-glob@^3.0.1: ...@@ -7001,6 +7028,15 @@ dir-glob@^3.0.1:
dependencies: dependencies:
path-type "^4.0.0" path-type "^4.0.0"
dnd-core@14.0.0:
version "14.0.0"
resolved "https://registry.yarnpkg.com/dnd-core/-/dnd-core-14.0.0.tgz#973ab3470d0a9ac5a0fa9021c4feba93ad12347d"
integrity sha512-wTDYKyjSqWuYw3ZG0GJ7k+UIfzxTNoZLjDrut37PbcPGNfwhlKYlPUqjAKUjOOv80izshUiqusaKgJPItXSevA==
dependencies:
"@react-dnd/asap" "^4.0.0"
"@react-dnd/invariant" "^2.0.0"
redux "^4.0.5"
doctrine@^2.1.0: doctrine@^2.1.0:
version "2.1.0" version "2.1.0"
resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-2.1.0.tgz#5cd01fc101621b42c4cd7f5d1a66243716d3f39d" resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-2.1.0.tgz#5cd01fc101621b42c4cd7f5d1a66243716d3f39d"
...@@ -7789,7 +7825,7 @@ faker@5.5.2: ...@@ -7789,7 +7825,7 @@ faker@5.5.2:
resolved "https://registry.yarnpkg.com/faker/-/faker-5.5.2.tgz#d6f99923fb757b26733a6d2396ddb448ac5bb446" resolved "https://registry.yarnpkg.com/faker/-/faker-5.5.2.tgz#d6f99923fb757b26733a6d2396ddb448ac5bb446"
integrity sha512-6G3lzZXWjWfqTJDS9KhHFIislZMGdrzDqews3T14E/dsANVbs3YT4A3jSNDrbA/gbtmjLuKJx9DzcLucdXBqBw== integrity sha512-6G3lzZXWjWfqTJDS9KhHFIislZMGdrzDqews3T14E/dsANVbs3YT4A3jSNDrbA/gbtmjLuKJx9DzcLucdXBqBw==
fast-deep-equal@^3.1.1: fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3:
version "3.1.3" version "3.1.3"
resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525"
integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==
...@@ -12534,6 +12570,24 @@ react-content-loader@6.0.3: ...@@ -12534,6 +12570,24 @@ react-content-loader@6.0.3:
resolved "https://registry.yarnpkg.com/react-content-loader/-/react-content-loader-6.0.3.tgz#32e28ca7120e0a2552fc26655d0d4448cc1fc0c5" resolved "https://registry.yarnpkg.com/react-content-loader/-/react-content-loader-6.0.3.tgz#32e28ca7120e0a2552fc26655d0d4448cc1fc0c5"
integrity sha512-CIRgTHze+ls+jGDIfCitw27YkW2XcaMpsYORTUdBxsMFiKuUYMnlvY76dZE4Lsaa9vFXVw+41ieBEK7SJt0nug== integrity sha512-CIRgTHze+ls+jGDIfCitw27YkW2XcaMpsYORTUdBxsMFiKuUYMnlvY76dZE4Lsaa9vFXVw+41ieBEK7SJt0nug==
react-dnd-html5-backend@14.0.0:
version "14.0.0"
resolved "https://registry.yarnpkg.com/react-dnd-html5-backend/-/react-dnd-html5-backend-14.0.0.tgz#28d660a2ad1e07447c34a65cd25f7de8f1657194"
integrity sha512-2wAQqRFC1hbRGmk6+dKhOXsyQQOn3cN8PSZyOUeOun9J8t3tjZ7PS2+aFu7CVu2ujMDwTJR3VTwZh8pj2kCv7g==
dependencies:
dnd-core "14.0.0"
react-dnd@14.0.2:
version "14.0.2"
resolved "https://registry.yarnpkg.com/react-dnd/-/react-dnd-14.0.2.tgz#57266baec92b887301f81fa3b77f87168d159733"
integrity sha512-JoEL78sBCg8SzjOKMlkR70GWaPORudhWuTNqJ56lb2P8Vq0eM2+er3ZrMGiSDhOmzaRPuA9SNBz46nHCrjn11A==
dependencies:
"@react-dnd/invariant" "^2.0.0"
"@react-dnd/shallowequal" "^2.0.0"
dnd-core "14.0.0"
fast-deep-equal "^3.1.3"
hoist-non-react-statics "^3.3.2"
react-dom@17.0.2: react-dom@17.0.2:
version "17.0.2" version "17.0.2"
resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-17.0.2.tgz#ecffb6845e3ad8dbfcdc498f0d0a939736502c23" resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-17.0.2.tgz#ecffb6845e3ad8dbfcdc498f0d0a939736502c23"
...@@ -12655,6 +12709,16 @@ react-spinners@0.10.6: ...@@ -12655,6 +12709,16 @@ react-spinners@0.10.6:
dependencies: dependencies:
"@emotion/core" "^10.0.35" "@emotion/core" "^10.0.35"
react-table-sticky@1.1.3:
version "1.1.3"
resolved "https://registry.yarnpkg.com/react-table-sticky/-/react-table-sticky-1.1.3.tgz#af27c0afb2c4a32c292d486b21d9a896d354ba70"
integrity sha512-9hyjbveY1aDyo9wkyMOsmKIpQdFUaw2yG6/f8c5SPE4pOTuKm7L2zLnDa+AxpG+3315USulViaO4XSOkX1KdVA==
react-table@7.6.3:
version "7.6.3"
resolved "https://registry.yarnpkg.com/react-table/-/react-table-7.6.3.tgz#76434392b3f62344bdb704f5b227c2f29c1ffb14"
integrity sha512-hfPF13zDLxPMpLKzIKCE8RZud9T/XrRTsaCIf8zXpWZIZ2juCl7qrGpo3AQw9eAetXV5DP7s2GDm+hht7qq5Dw==
react-toastify@7.0.3: react-toastify@7.0.3:
version "7.0.3" version "7.0.3"
resolved "https://registry.yarnpkg.com/react-toastify/-/react-toastify-7.0.3.tgz#93804c777ecf918872ba3b5be9c654db14547f85" resolved "https://registry.yarnpkg.com/react-toastify/-/react-toastify-7.0.3.tgz#93804c777ecf918872ba3b5be9c654db14547f85"
...@@ -12851,7 +12915,7 @@ redent@^3.0.0: ...@@ -12851,7 +12915,7 @@ redent@^3.0.0:
indent-string "^4.0.0" indent-string "^4.0.0"
strip-indent "^3.0.0" strip-indent "^3.0.0"
redux@4.0.5, redux@^4.0.0: redux@4.0.5, redux@^4.0.0, redux@^4.0.5:
version "4.0.5" version "4.0.5"
resolved "https://registry.yarnpkg.com/redux/-/redux-4.0.5.tgz#4db5de5816e17891de8a80c424232d06f051d93f" resolved "https://registry.yarnpkg.com/redux/-/redux-4.0.5.tgz#4db5de5816e17891de8a80c424232d06f051d93f"
integrity sha512-VSz1uMAH24DM6MF72vcojpYPtrTUu3ByVWfPL1nPfVRb5mZVTve5GnNCUV53QM/BZ66xfWrm0CTWoM+Xlz8V1w== integrity sha512-VSz1uMAH24DM6MF72vcojpYPtrTUu3ByVWfPL1nPfVRb5mZVTve5GnNCUV53QM/BZ66xfWrm0CTWoM+Xlz8V1w==
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册