未验证 提交 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 = () => {
return async (req, res) => {
const file = path.join(root, req.path.replace(/\.js$/, '.svg'));
if ((await fs.stat(file)).isFile()) {
res.type('js');
res.send(await transform(file, false));
if (req.path.endsWith('.js')) {
res.type('js');
res.send(await transform(file, false));
} else {
res.type(req.path.split('.').pop());
res.send(await fs.readFile(file));
}
}
};
};
......
......@@ -38,6 +38,7 @@
"@visualdl/netron": "2.1.5",
"@visualdl/wasm": "2.1.5",
"bignumber.js": "9.0.1",
"classnames": "2.3.1",
"d3": "6.6.2",
"d3-format": "2.0.0",
"echarts": "4.9.0",
......@@ -57,6 +58,8 @@
"query-string": "7.0.0",
"react": "17.0.2",
"react-content-loader": "6.0.3",
"react-dnd": "14.0.2",
"react-dnd-html5-backend": "14.0.0",
"react-dom": "17.0.2",
"react-helmet": "6.1.0",
"react-i18next": "11.8.12",
......@@ -66,6 +69,8 @@
"react-redux": "7.2.3",
"react-router-dom": "5.2.0",
"react-spinners": "0.10.6",
"react-table": "7.6.3",
"react-table-sticky": "1.1.3",
"react-toastify": "7.0.3",
"redux": "4.0.5",
"styled-components": "5.2.3",
......@@ -104,6 +109,7 @@
"@types/react-rangeslider": "2.2.3",
"@types/react-redux": "7.1.16",
"@types/react-router-dom": "5.1.7",
"@types/react-table": "7.0.29",
"@types/snowpack-env": "2.3.3",
"@types/styled-components": "5.1.9",
"@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">
<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" />
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
<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 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 @@
"close": "Close",
"colon": ": ",
"confirm": "Confirm",
"download-data": "Download data",
"download-data-format": "In {{format}}",
"download-image": "Download image",
"empty": "Nothing to display",
"error": "Error occurred",
"graph": "Graphs",
......@@ -13,16 +16,20 @@
"image": "Image",
"inactive": "Inactive",
"loading": "Please wait while loading data",
"maximize": "Maximize",
"minimize": "Minimize",
"more": "More",
"next-page": "Next Page",
"pr-curve": "PR Curve",
"previous-page": "Prev Page",
"restore": "Selection restore",
"roc-curve": "ROC Curve",
"run": "Run",
"running": "Running",
"runs": "Runs",
"sample": "Samples",
"scalar": "Scalars",
"scalar-value": "Value",
"search": "Search",
"search-empty": "Nothing found. Please try again with another word. <1/>Or you can <3>see all charts</3>.",
"search-result": "Search Result",
......@@ -46,6 +53,7 @@
"step": "Step",
"wall": "Wall Time"
},
"toggle-log-axis": "Logarithmic axis",
"total-page": "{{count}} page, jump to",
"total-page_plural": "{{count}} pages, jump to",
"unselected-empty": "Nothing selected. <1/>Please select display data from right side."
......
{
"download-image": "Download image",
"false-negatives": "FN",
"false-positives": "FP",
"maximize": "Maximize",
"minimize": "Minimize",
"precision": "Precision",
"recall": "Recall",
"restore": "Selection restore",
"threshold": "Threshold",
"time-display-type": "Time Display Type",
"true-negatives": "TN",
......
{
"download-image": "Download image",
"maximize": "Maximize",
"minimize": "Minimize",
"mode": "Mode",
"mode-value": {
"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 @@
"sample": "Sample",
"sample-rate": "Sample Rate",
"show-actual-size": "Show Actual Image Size",
"step": "Step",
"step-tip": "You can change step by pressing up & donw on your keyboard",
"text": "text"
}
{
"download-data": "Download data",
"download-data-format": "In {{format}}",
"download-image": "Download image",
"ignore-outliers": "Ignore outliers in chart scaling",
"max": "Max.",
"maximize": "Maximize",
"min": "Min.",
"minimize": "Minimize",
"restore": "Selection restore",
"show-most-value": "Show global extrema",
"smoothed": "Smoothed",
"smoothed-data-only": "Smoothed Data Only",
"smoothing": "Smoothing",
"toggle-log-axis": "Logarithmic axis",
"tooltip-sorting": "Tooltip Sorting",
"tooltip-sorting-value": {
"ascending": "Ascending",
......@@ -20,6 +13,5 @@
"descending": "Descending",
"nearest": "Nearest"
},
"value": "Value",
"x-axis": "X-Axis"
}
......@@ -4,6 +4,9 @@
"close": "关闭",
"colon": ":",
"confirm": "确定",
"download-data": "下载数据",
"download-data-format": "{{format}} 格式",
"download-image": "下载图片",
"empty": "暂无数据",
"error": "发生错误",
"graph": "网络结构",
......@@ -13,16 +16,20 @@
"image": "图像",
"inactive": "待使用",
"loading": "数据载入中,请稍等",
"maximize": "最大化",
"minimize": "最小化",
"more": "更多",
"next-page": "下一页",
"pr-curve": "PR曲线",
"previous-page": "上一页",
"restore": "还原图表框选",
"roc-curve": "ROC曲线",
"run": "运行",
"running": "运行中",
"runs": "数据流",
"sample": "样本数据",
"scalar": "标量数据",
"scalar-value": "Value",
"search": "搜索",
"search-empty": "没有找到您期望的内容,你可以尝试其他搜索词<1/>或者点击<3>查看全部图表</3>",
"search-result": "搜索结果",
......@@ -46,6 +53,7 @@
"step": "Step",
"wall": "Wall Time"
},
"toggle-log-axis": "切换对数坐标轴",
"total-page": "共 {{count}} 页,跳转至",
"total-page_plural": "共 {{count}} 页,跳转至",
"unselected-empty": "未选中任何数据<1/>请在右侧操作栏选择要展示的数据"
......
{
"download-image": "下载图片",
"false-negatives": "FN",
"false-positives": "FP",
"maximize": "最大化",
"minimize": "最小化",
"precision": "Precision",
"recall": "Recall",
"restore": "还原图表框选",
"threshold": "Threshold",
"time-display-type": "时间显示类型",
"true-negatives": "TN",
......
{
"download-image": "下载图片",
"maximize": "最大化",
"minimize": "最小化",
"mode": "直方图模式",
"mode-value": {
"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 @@
"sample": "样本",
"sample-rate": "采样率",
"show-actual-size": "按真实大小展示",
"step": "Step",
"step-tip": "您还可以通过键盘 ↑ ↓ 键,快速调节step哦~",
"text": "文本"
}
{
"download-data": "下载数据",
"download-data-format": "{{format}} 格式",
"download-image": "下载图片",
"ignore-outliers": "图表缩放时忽略极端值",
"max": "最大值",
"maximize": "最大化",
"min": "最小值",
"minimize": "最小化",
"restore": "还原图表框选",
"show-most-value": "显示最值",
"smoothed": "Smoothed",
"smoothed-data-only": "仅显示平滑后数据",
"smoothing": "平滑度",
"toggle-log-axis": "切换对数坐标轴",
"tooltip-sorting": "详情数据排序",
"tooltip-sorting-value": {
"ascending": "升序",
......@@ -20,6 +13,5 @@
"descending": "降序",
"nearest": "最近"
},
"value": "Value",
"x-axis": "X轴"
}
......@@ -28,6 +28,11 @@ export const AsideSection = styled.section`
margin-bottom: 0;
${transitionProps('border-color')}
}
& > & {
margin-left: 0;
margin-right: 0;
}
`;
const Wrapper = styled.div.attrs<{width: string | number}>(({width}) => ({
......@@ -59,6 +64,19 @@ const Wrapper = styled.div.attrs<{width: string | number}>(({width}) => ({
flex: none;
box-shadow: 0 -${rem(5)} ${rem(16)} 0 rgba(0, 0, 0, 0.03);
padding: ${rem(20)};
> ${AsideSection} {
margin-left: 0;
margin-right: 0;
&:first-child {
margin-top: 0;
}
&:last-child {
margin-bottom: 0;
}
}
}
> .aside-resize-bar-left,
......@@ -68,7 +86,7 @@ const Wrapper = styled.div.attrs<{width: string | number}>(({width}) => ({
height: 100%;
top: 0;
cursor: col-resize;
user-select: none;
touch-action: none;
&.aside-resize-bar-left {
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';
import RawIcon from '~/components/Icon';
import {colors} from '~/utils/theme';
import styled from 'styled-components';
import useClassNames from '~/hooks/useClassNames';
const height = em(36);
......@@ -128,14 +129,16 @@ const Button: FunctionComponent<ButtonProps & WithStyled> = ({
const buttonType = useMemo(() => type || 'default', [type]);
const classNames = useClassNames(className, {rounded, disabled, outline: buttonType === 'default' || outline}, [
className,
rounded,
disabled,
buttonType,
outline
]);
return (
<Wrapper
className={`${className ?? ''} ${rounded ? 'rounded' : ''} ${disabled ? 'disabled' : ''} ${
buttonType === 'default' || outline ? 'outline' : ''
}`}
type={buttonType}
onClick={click}
>
<Wrapper className={classNames} type={buttonType} onClick={click}>
{icon && <Icon type={icon}></Icon>}
{children}
</Wrapper>
......
......@@ -18,6 +18,7 @@ import React, {FunctionComponent} from 'react';
import {WithStyled, borderRadius, headerHeight, math, rem, sameBorder, size, transitionProps} from '~/utils/style';
import styled from 'styled-components';
import useClassNames from '~/hooks/useClassNames';
const Div = styled.div<{maximized?: boolean; divWidth?: string; divHeight?: string}>`
${props =>
......@@ -43,13 +44,10 @@ type ChartProps = {
};
const Chart: FunctionComponent<ChartProps & WithStyled> = ({maximized, width, height, className, children}) => {
const classNames = useClassNames({maximized}, className, [maximized, className]);
return (
<Div
maximized={maximized}
divWidth={width}
divHeight={height}
className={`${maximized ? 'maximized' : ''} ${className ?? ''}`}
>
<Div maximized={maximized} divWidth={width} divHeight={height} className={classNames}>
{children}
</Div>
);
......
......@@ -156,11 +156,11 @@ const ToggleChartToolbox: FunctionComponent<ToggleChartToolboxItem> = ({
}) => {
const [active, setActive] = useState(false);
const click = useCallback(() => {
onClick?.(!active);
setActive(a => {
onClick?.(!a);
return !a;
});
}, [onClick]);
}, [active, onClick]);
const toolboxIcon = useMemo(
() => <ChartToolboxIcon icon={icon} activeIcon={activeIcon} activeStatus={active} toggle onClick={click} />,
[icon, activeIcon, active, click]
......
......@@ -54,7 +54,7 @@ const Inner = styled.div<{checked?: boolean; size?: string; disabled?: boolean}>
props.disabled
? props.checked
? 'var(--text-lighter-color)'
: 'var(--text-lighter-color)'
: 'transparent'
: props.checked
? 'var(--primary-color)'
: 'var(--background-color)'};
......@@ -84,15 +84,15 @@ const Content = styled.div<{disabled?: boolean}>`
`;
type CheckboxProps = {
value?: boolean;
onChange?: (value: boolean) => unknown;
checked?: boolean;
onChange?: (checked: boolean) => unknown;
size?: 'small';
title?: string;
disabled?: boolean;
};
const Checkbox: FunctionComponent<CheckboxProps & WithStyled> = ({
value,
checked: value,
children,
size,
disabled,
......@@ -101,7 +101,7 @@ const Checkbox: FunctionComponent<CheckboxProps & WithStyled> = ({
onChange
}) => {
const [checked, setChecked] = useState(!!value);
useEffect(() => setChecked(!!value), [setChecked, value]);
useEffect(() => setChecked(!!value), [value]);
const onChangeInput = useCallback(
(e: React.ChangeEvent<HTMLInputElement>) => {
if (disabled) {
......
......@@ -215,19 +215,19 @@ const PRCurveChart: FunctionComponent<PRCurveChartProps> = ({type, runs, tag, ru
{
icon: 'maximize',
activeIcon: 'minimize',
tooltip: t('curves:maximize'),
activeTooltip: t('curves:minimize'),
tooltip: t('common:maximize'),
activeTooltip: t('common:minimize'),
toggle: true,
onClick: () => setMaximized(m => !m)
},
{
icon: 'restore-size',
tooltip: t('curves:restore'),
tooltip: t('common:restore'),
onClick: () => echart.current?.restore()
},
{
icon: 'download',
tooltip: t('curves:download-image'),
tooltip: t('common:download-image'),
onClick: () => echart.current?.saveAsImage()
}
]}
......
......@@ -15,7 +15,7 @@
*/
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 Properties from '~/components/GraphPage/Properties';
......@@ -58,10 +58,18 @@ const Dialog = styled.div`
> .modal-close {
flex: none;
${size(em(20, 18), em(20, 18))}
font-size: ${em(20, 18)};
text-align: center;
font-size: ${em(16, 18)};
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}) =
<Tab
list={reductions.map(value => ({value, label: t(`high-dimensional:reduction-value.${value}`)}))}
value={value}
variant="fullWidth"
appearance="underscore"
onChange={onChange}
/>
);
......
......@@ -289,14 +289,14 @@ const HistogramChart: FunctionComponent<HistogramChartProps> = ({run, tag, mode,
{
icon: 'maximize',
activeIcon: 'minimize',
tooltip: t('histogram:maximize'),
activeTooltip: t('histogram:minimize'),
tooltip: t('common:maximize'),
activeTooltip: t('common:minimize'),
toggle: true,
onClick: () => setMaximized(m => !m)
},
{
icon: 'download',
tooltip: t('histogram:download-image'),
tooltip: t('common:download-image'),
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.
*/
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';
import type {WithStyled} from '~/utils/style';
import styled from 'styled-components';
import useClassNames from '~/hooks/useClassNames';
const PUBLIC_PATH: string = import.meta.env.SNOWPACK_PUBLIC_PATH;
......@@ -40,8 +41,10 @@ type IconProps = {
const Icon: FunctionComponent<IconProps & WithStyled> = ({type, onClick, className}) => {
const Svg = useMemo(() => React.lazy(() => import(`${PUBLIC_PATH}/icons/${type}.js`)), [type]);
const classNames = useClassNames('vdl-icon', `icon-${type}`, className, [type, className]);
return (
<Wrapper className={`vdl-icon icon-${type} ${className ?? ''}`} onClick={() => onClick?.()}>
<Wrapper className={classNames} onClick={() => onClick?.()}>
<Suspense fallback="">
<Svg />
</Suspense>
......
......@@ -34,8 +34,8 @@ const StyledInput = styled.input<{rounded?: boolean}>`
caret-color: var(--text-color);
${transitionProps(['border-color', 'background-color', 'caret-color', 'color'])}
&:hover,
&:focus {
&:hover:not(:disabled),
&:focus:not(:disabled) {
border-color: var(--border-focused-color);
}
......@@ -43,6 +43,11 @@ const StyledInput = styled.input<{rounded?: boolean}>`
color: var(--text-lighter-color);
${transitionProps('color')}
}
&:disabled {
cursor: not-allowed;
color: var(--text-lighter-color);
}
`;
type CustomInputProps = {
......
......@@ -84,7 +84,16 @@ const LineChart = React.forwardRef<LineChartRef, LineChartProps & WithStyled>(
title: {
text: title ?? ''
},
series: data?.map(item =>
xAxis: {
splitLine: {
show: false
},
splitNumber: 5
},
yAxis: {
splitNumber: 4
},
series: data?.map((item, index) =>
defaultsDeep(
{
// show symbol if there is only one point
......@@ -92,6 +101,12 @@ const LineChart = React.forwardRef<LineChartRef, LineChartProps & WithStyled>(
type: 'line'
},
item,
{
lineStyle: {
color: color[index % color.length],
width: 1.5
}
},
series
)
)
......
......@@ -31,6 +31,7 @@ import logo from '~/assets/images/logo.svg';
import queryString from 'query-string';
import styled from 'styled-components';
import useAvailableComponents from '~/hooks/useAvailableComponents';
import useClassNames from '~/hooks/useClassNames';
import {useTranslation} from 'react-i18next';
const BASE_URI: string = import.meta.env.SNOWPACK_PUBLIC_BASE_URI;
......@@ -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...
const NavbarItem = React.forwardRef<HTMLDivElement, NavbarItemProps & {children?: React.ReactNode}>(
({path, active, showDropdownIcon, children}, ref) => {
const classNames = useClassNames('nav-text', {'dropdown-icon': showDropdownIcon}, [showDropdownIcon]);
if (path) {
return (
<NavItem active={active} ref={ref}>
<NavbarLink to={path} className="nav-link">
<span className={`nav-text ${showDropdownIcon ? 'dropdown-icon' : ''}`}>{children}</span>
<span className={classNames}>{children}</span>
</NavbarLink>
</NavItem>
);
......@@ -219,7 +222,7 @@ const NavbarItem = React.forwardRef<HTMLDivElement, NavbarItemProps & {children?
return (
<NavItem active={active} ref={ref}>
<span className={`nav-text ${showDropdownIcon ? 'dropdown-icon' : ''}`}>{children}</span>
<span className={classNames}>{children}</span>
</NavItem>
);
}
......
......@@ -160,7 +160,7 @@ const RunAside: FunctionComponent<RunAsideProps> = ({
placeholder={t('common:search-runs')}
rounded
/>
<Checkbox value={selectAll} onChange={toggleSelectAll}>
<Checkbox checked={selectAll} onChange={toggleSelectAll}>
{t('common:select-all')}
</Checkbox>
<div className="run-list">
......@@ -170,7 +170,7 @@ const RunAside: FunctionComponent<RunAsideProps> = ({
filteredRuns.map((run, index) => (
<div key={index}>
<Checkbox
value={selectedRuns?.map(r => r.label)?.includes(run.label)}
checked={selectedRuns?.map(r => r.label)?.includes(run.label)}
title={run.label}
onChange={value => setSelectedRuns(run, value)}
>
......
......@@ -317,7 +317,7 @@ const ImagePreviewer: FunctionComponent<ImagePreviewerProps> = ({
<Wrapper>
<Header>
<div className="step-slider">
<span>{t('sample:step')}</span>
<span>{t('common:time-mode.step')}</span>
<RangeSlider
className="slider"
min={0}
......
......@@ -110,7 +110,7 @@ const StepSlider: FunctionComponent<StepSliderProps> = ({onChange, onChangeCompl
<>
<Label>
<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')}>
<div className="step-buttons">
<a onClick={prevStep}>
......
......@@ -170,7 +170,7 @@ const Text: FunctionComponent<TextProps> = ({run, tag, step, wallTime, index}) =
<>
<span className="step">
<span>
{t('sample:step')} {step}
{t('common:time-mode.step')} {step}
</span>
</span>
<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;
......@@ -40,8 +40,10 @@ export const padding = em(10);
export const height = em(36);
const Wrapper = styled.div<{opened?: boolean}>`
height: ${height};
line-height: calc(${height} - 2px);
--height: ${height};
--padding: ${padding};
height: var(--height);
line-height: calc(var(--height) - 2px);
max-width: 100%;
display: inline-block;
position: relative;
......@@ -56,7 +58,7 @@ const Wrapper = styled.div<{opened?: boolean}>`
`;
const Trigger = styled.div<{selected?: boolean}>`
padding: ${padding};
padding: var(--padding);
display: inline-flex;
${size('100%')}
justify-content: space-between;
......@@ -78,12 +80,26 @@ const TriggerIcon = styled(Icon)<{opened?: boolean}>`
const Label = styled.span`
flex-grow: 1;
padding-right: ${em(10)};
line-height: 1;
${ellipsis()}
`;
const List = styled.div<{opened?: boolean; empty?: boolean}>`
const List = styled.div<{opened?: boolean; empty?: boolean; direction?: 'bottom' | 'top'}>`
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);
max-height: ${math(`4.35 * ${height} + 2 * ${padding}`)};
overflow-x: hidden;
......@@ -91,13 +107,10 @@ const List = styled.div<{opened?: boolean; empty?: boolean}>`
left: -1px;
padding: ${padding} 0;
border: inherit;
border-top-color: var(--border-color);
${borderRadiusShortHand('bottom', borderRadius)}
display: ${props => (props.opened ? 'block' : 'none')};
z-index: ${zIndexes.component};
line-height: 1;
background-color: inherit;
box-shadow: 0 5px 6px 0 rgba(0, 0, 0, 0.05);
${transitionProps(['border-color', 'color'])}
${props =>
props.empty
......@@ -168,6 +181,7 @@ export type SelectListItem<T> = {
export type SelectProps<T> = {
list?: (SelectListItem<T> | T)[];
placeholder?: string;
direction?: 'bottom' | 'top';
} & (
| {
value?: T;
......@@ -185,6 +199,7 @@ const Select = <T extends unknown>({
list: propList,
value: propValue,
placeholder,
direction,
multiple,
className,
onChange
......@@ -267,14 +282,14 @@ const Select = <T extends unknown>({
<Label>{label}</Label>
<TriggerIcon opened={isOpened} type="chevron-down" />
</Trigger>
<List opened={isOpened} empty={isListEmpty}>
<List className="list" opened={isOpened} empty={isListEmpty} direction={direction}>
{isListEmpty
? t('common:empty')
: list.map((item, index) => {
if (multiple) {
return (
<MultipleListItem
value={(value as T[]).includes(item.value)}
checked={(value as T[]).includes(item.value)}
key={index}
title={item.label}
disabled={item.disabled}
......
此差异已折叠。
......@@ -34,6 +34,7 @@ const Wrapper = styled.div`
th,
td {
margin: 0;
line-height: 1;
> span {
display: inline-block;
......@@ -46,11 +47,11 @@ const Wrapper = styled.div`
th {
font-size: 1.166666667em;
font-weight: bold;
padding: 0 0.285714286em;
padding: 0.15em 0.285714286em;
}
td {
padding: 0 0.333333333em;
padding: 0.15em 0.333333333em;
&.run-indicator > span {
${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);
}
......@@ -49,6 +49,9 @@ function useRequest<D = unknown, E extends Error = Error>(
const key = args[0];
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]);
useEffect(() => {
......
此差异已折叠。
......@@ -53,7 +53,7 @@ const ImageSample: FunctionComponent = () => {
loading={loading}
>
<AsideSection>
<Checkbox value={showActualSize} onChange={setShowActualSize}>
<Checkbox checked={showActualSize} onChange={setShowActualSize}>
{t('sample:show-actual-size')}
</Checkbox>
</AsideSection>
......
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册