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

Rewrite High-Dimensional (#869)

* feat: rewrite high-dimensional

* chore: update dependencies

* chore: update dependencies

* chore: rewrite scatter chart

* feat: t-sne for high-dimensional

* feat: umap for high-dimensional

* feat: add hover effects in high-dimensional

* feat: add hover label in high-dimensional chart

* chore: update dependencies

* feat: search & highlight in high-dimensional chart

* chore: update dependencies

* fix: resolve type

* chore: remove unused code
上级 1d65b86d
......@@ -38,21 +38,21 @@
"version": "yarn format && git add -A"
},
"devDependencies": {
"@typescript-eslint/eslint-plugin": "4.1.1",
"@typescript-eslint/parser": "4.1.1",
"eslint": "7.9.0",
"eslint-config-prettier": "6.11.0",
"@typescript-eslint/eslint-plugin": "4.10.0",
"@typescript-eslint/parser": "4.10.0",
"eslint": "7.15.0",
"eslint-config-prettier": "7.0.0",
"eslint-plugin-license-header": "0.2.0",
"eslint-plugin-prettier": "3.1.4",
"eslint-plugin-react": "7.20.6",
"eslint-plugin-react-hooks": "4.1.2",
"husky": "4.3.0",
"eslint-plugin-prettier": "3.3.0",
"eslint-plugin-react": "7.21.5",
"eslint-plugin-react-hooks": "4.2.0",
"husky": "4.3.6",
"lerna": "3.22.1",
"lint-staged": "10.4.0",
"prettier": "2.1.2",
"lint-staged": "10.5.3",
"prettier": "2.2.1",
"rimraf": "3.0.2",
"typescript": "4.0.2",
"yarn": "1.22.5"
"typescript": "4.0.5",
"yarn": "1.22.10"
},
"engines": {
"node": ">=10",
......
......@@ -35,17 +35,17 @@
],
"dependencies": {
"@visualdl/server": "2.0.9",
"open": "7.2.1",
"open": "7.3.0",
"ora": "5.1.0",
"pm2": "4.4.1",
"yargs": "16.0.3"
"pm2": "4.5.0",
"yargs": "16.2.0"
},
"devDependencies": {
"@types/node": "14.10.3",
"@types/yargs": "15.0.5",
"cross-env": "7.0.2",
"ts-node": "9.0.0",
"typescript": "4.0.2"
"@types/node": "14.14.14",
"@types/yargs": "15.0.12",
"cross-env": "7.0.3",
"ts-node": "9.1.1",
"typescript": "4.0.5"
},
"engines": {
"node": ">=12",
......
......@@ -19,7 +19,7 @@
const path = require('path');
const fs = require('fs/promises');
const ENV_INJECT = 'const env = window.__snowpack_env__ || {}; export default env;';
const ENV_INJECT = 'const env = globalThis.__snowpack_env__ || {}; export default env;';
const dest = path.resolve(__dirname, '../dist/__snowpack__');
const envFile = path.join(dest, 'env.js');
......
......@@ -20,7 +20,7 @@
const path = require('path');
const fs = require('fs-extra');
const root = path.dirname(require('enhanced-resolve').sync(__dirname, '@visualdl/netron'));
const root = path.dirname(require('enhanced-resolve').sync(__dirname, '@visualdl/netron') || '');
const pathname = '/netron';
const dist = path.resolve(__dirname, '../dist');
const dest = path.join(dist, pathname);
......
......@@ -34,82 +34,86 @@
"builder/environment.js"
],
"dependencies": {
"@tippyjs/react": "4.1.0",
"@tippyjs/react": "4.2.0",
"@visualdl/netron": "2.0.9",
"@visualdl/wasm": "2.0.9",
"bignumber.js": "9.0.0",
"bignumber.js": "9.0.1",
"d3": "6.3.1",
"d3-format": "2.0.0",
"echarts": "4.9.0",
"echarts-gl": "1.1.1",
"eventemitter3": "4.0.7",
"file-saver": "2.0.2",
"i18next": "19.7.0",
"file-saver": "2.0.5",
"i18next": "19.8.4",
"i18next-browser-languagedetector": "6.0.1",
"i18next-fetch-backend": "3.0.0",
"lodash": "4.17.20",
"mime-types": "2.1.27",
"moment": "2.28.0",
"moment": "2.29.1",
"nprogress": "0.2.0",
"polished": "3.6.6",
"query-string": "6.13.2",
"react": "16.13.1",
"react-dom": "16.13.1",
"polished": "4.0.5",
"query-string": "6.13.7",
"react": "17.0.1",
"react-dom": "17.0.1",
"react-helmet": "6.1.0",
"react-i18next": "11.7.2",
"react-i18next": "11.8.4",
"react-input-range": "1.3.0",
"react-is": "16.13.1",
"react-is": "17.0.1",
"react-rangeslider": "2.2.0",
"react-redux": "7.2.1",
"react-redux": "7.2.2",
"react-router-dom": "5.2.0",
"react-spinners": "0.9.0",
"react-toastify": "6.0.8",
"react-toastify": "6.2.0",
"redux": "4.0.5",
"styled-components": "5.2.0",
"swr": "0.3.0",
"tippy.js": "6.2.6"
"styled-components": "5.2.1",
"swr": "0.3.9",
"three": "0.123.0",
"tippy.js": "6.2.7",
"umap-js": "1.3.3"
},
"devDependencies": {
"@babel/core": "7.11.6",
"@babel/plugin-proposal-class-properties": "7.10.4",
"@babel/preset-env": "7.11.5",
"@babel/preset-react": "7.10.4",
"@baiducloud/sdk": "1.0.0-rc.22",
"@babel/core": "7.12.10",
"@babel/plugin-proposal-class-properties": "7.12.1",
"@babel/preset-env": "7.12.11",
"@babel/preset-react": "7.12.10",
"@baiducloud/sdk": "1.0.0-rc.25",
"@simbathesailor/use-what-changed": "0.1.25",
"@snowpack/app-scripts-react": "1.10.0",
"@snowpack/plugin-dotenv": "2.0.1",
"@snowpack/plugin-optimize": "0.2.1",
"@snowpack/plugin-run-script": "2.1.2",
"@svgr/core": "5.4.0",
"@testing-library/jest-dom": "5.11.4",
"@testing-library/react": "11.0.4",
"@types/d3-format": "1.3.1",
"@types/echarts": "4.6.5",
"@snowpack/app-scripts-react": "1.12.6",
"@snowpack/plugin-dotenv": "2.0.5",
"@snowpack/plugin-optimize": "0.2.10",
"@snowpack/plugin-run-script": "2.2.1",
"@svgr/core": "5.5.0",
"@testing-library/jest-dom": "5.11.6",
"@testing-library/react": "11.2.2",
"@types/d3": "6.2.0",
"@types/d3-format": "2.0.0",
"@types/echarts": "4.9.3",
"@types/file-saver": "2.0.1",
"@types/jest": "26.0.14",
"@types/loadable__component": "5.13.0",
"@types/lodash": "4.14.161",
"@types/jest": "26.0.19",
"@types/loadable__component": "5.13.1",
"@types/lodash": "4.14.165",
"@types/mime-types": "2.1.0",
"@types/nprogress": "0.2.0",
"@types/react": "16.9.49",
"@types/react-dom": "16.9.8",
"@types/react": "17.0.0",
"@types/react-dom": "17.0.0",
"@types/react-helmet": "6.1.0",
"@types/react-rangeslider": "2.2.3",
"@types/react-redux": "7.1.9",
"@types/react-router-dom": "5.1.5",
"@types/snowpack-env": "2.3.0",
"@types/styled-components": "5.1.3",
"@types/react-redux": "7.1.12",
"@types/react-router-dom": "5.1.6",
"@types/snowpack-env": "2.3.2",
"@types/styled-components": "5.1.5",
"@visualdl/mock": "2.0.9",
"babel-plugin-styled-components": "1.11.1",
"babel-plugin-styled-components": "1.12.0",
"dotenv": "8.2.0",
"enhanced-resolve": "4.3.0",
"enhanced-resolve": "5.4.0",
"express": "4.17.1",
"fs-extra": "9.0.1",
"html-minifier": "4.0.0",
"http-proxy-middleware": "1.0.5",
"jest": "26.4.2",
"snowpack": "2.11.1",
"typescript": "4.0.2",
"yargs": "16.0.3"
"http-proxy-middleware": "1.0.6",
"jest": "26.6.3",
"snowpack": "2.18.4",
"typescript": "4.0.5",
"yargs": "16.2.0"
},
"engines": {
"node": ">=14",
......
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="512" height="512" viewBox="0 0 512 512">
<path d="M0 201.147v-127.991l41.808 41.744c47.411-71.728 127.725-114.888 213.797-114.9 141.593 0 256.395 114.607 256.395 256s-114.803 256-256.396 256c-104.379 0.024-198.352-63.132-237.62-159.695-4.568-11.225 0.844-24.022 12.087-28.579s24.055 0.842 28.62 12.067c32.555 80.007 110.426 132.336 196.913 132.324 117.329 0 212.445-94.968 212.445-212.118s-95.116-212.118-212.446-212.118c-75.793 0-144.173 40.041-181.988 102.758l54.571 54.505h-128.188z"></path>
</svg>
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="512" height="512" viewBox="0 0 512 512">
<path d="M472.615 39.385h-78.769v-39.385h98.462c5.223 0 10.231 2.075 13.925 5.768s5.768 8.701 5.768 13.924v98.462h-39.385v-78.769zM472.615 393.846h39.385v98.462c0 10.875-8.817 19.692-19.692 19.692h-98.462v-39.385h78.769v-78.769zM118.154 472.615v39.385h-98.462c-5.223 0-10.232-2.075-13.925-5.768s-5.768-8.702-5.768-13.925v-98.462h39.385v78.769h78.769zM39.385 39.385v78.769h-39.385v-98.462c0-5.223 2.075-10.232 5.768-13.925s8.701-5.768 13.924-5.768h100.943v39.385h-81.251zM196.923 0.001h118.154v39.385h-118.154v-39.385zM196.923 472.615h118.154v39.385h-118.154v-39.385zM0.001 196.923h39.385v118.154h-39.385v-118.154zM472.615 196.923h39.385v118.154h-39.385v-118.154z"></path>
</svg>
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="512" height="512" viewBox="0 0 512 512">
<path d="M367.188 421.125c47.148 0 83.352-13.892 108.61-41.255 23.995-26.1 36.203-87.351 36.203-133.657 0-47.148-11.366-94.717-33.678-119.976-24.416-27.784-60.198-41.255-107.767-41.255h-107.767v336.142h104.399zM361.295 386.55h-63.2v-268.809h66.988c39.572 0 68.237 8.149 86.76 28.776 17.26 19.365 26.48 59.282 26.48 99.695 0 39.15-10.103 91.263-28.627 111.469-19.786 20.627-49.253 28.868-88.403 28.868z"></path>
<path d="M105.662 427.019c31.151 0 56.409-9.68 75.775-29.041 18.944-18.901 28.626-43.33 28.626-73.293 0-19.36-5.052-35.495-15.155-48.401-9.262-12.907-22.732-22.126-40.413-28.119 32.835-11.985 49.253-36.877 49.253-73.754 0-27.658-8.84-49.323-26.1-65.457-17.681-16.133-41.256-23.97-69.88-23.97s-51.779 8.758-69.46 26.736c-18.523 17.978-29.468 42.87-31.994 74.676h34.099c2.105-22.126 8.84-39.182 20.627-51.167 11.366-11.985 26.942-17.517 47.148-17.517 19.365 0 34.94 5.071 45.885 16.133 10.103 10.141 15.577 24.431 15.577 42.87s-5.472 32.728-15.996 42.87c-10.524 9.68-25.678 14.751-45.885 14.751h-23.154v29.501h24.416c21.049 0 37.044 5.071 48.832 16.134s17.681 26.276 17.681 46.097c0 19.36-6.315 35.495-18.102 48.401-13.049 13.368-30.31 20.283-51.779 20.283-18.944 0-34.519-5.993-47.148-17.057-14.733-13.368-22.733-33.189-23.574-59.004h-34.94c2.526 36.877 14.313 64.996 35.783 84.357 18.102 15.673 41.255 23.97 69.88 23.97z"></path>
</svg>
{
"2d": "2D",
"3d": "3D",
"3d-label": "Enable/disable 3D labels mode",
"component": "Component #{{index}}",
"continue": "Resume",
"data": "Data",
"data-dimension": "Types",
"data-path": "Data Path",
"dimension": "Dimension",
"display-all-label": "Display All Labels",
"pca": "PCA",
"reduction-method": "Reduction Method",
"tsne": "T-SNE"
"dimension-value": {
"2d": "2D",
"3d": "3D"
},
"iteration": "Iteration",
"learning-rate": "Learning rate",
"loading": {
"calculating": "Calculating...",
"fetching-metadata": "Fetching metadata...",
"fetching-tensor": "Fetching tensor...",
"parsing": "Parsing data...",
"reading-metadata": "Reading metadata...",
"reading-vector": "Reading tensor..."
},
"matched-result-count": "{{count}} matched.",
"neighbors": "Neighbors",
"pause": "Pause",
"perplexity": "Perplexity",
"points": "Points",
"reduction-value": {
"pca": "PCA",
"tsne": "T-SNE",
"umap": "UMAP"
},
"reset-zoom": "Reset zoom to fit all points",
"run": "Run",
"search-empty": "Nothing matched",
"select-color": "Color by",
"select-data": "Select Data",
"select-file": "Select File",
"select-label": "Label by",
"selection": "Bounding box selection",
"stop": "Stop",
"total-variance-described": "Total variance described",
"tsne": "T-SNE",
"upload": {
"step1": "Step 1: Load a TSV file of vectors.<1/> Example of 3 vectors with dimension 4:",
"step2": "Step 2 (optional): Load a TSV file of metadata.<1/>Example of 3 data points and 2 columns.<3/><4>Note: If there is more than one column, the first row will be parsed as column labels.</4>"
},
"upload-data": "Upload Data",
"upload-from-computer": "Load data from your computer"
}
{
"2d": "二维",
"3d": "三维",
"3d-label": "开启/关闭3D数据标签",
"component": "主成分{{index}}",
"continue": "继续",
"data": "数据",
"data-dimension": "数据类别",
"data-path": "数据路径",
"dimension": "维度",
"display-all-label": "展示所有标签",
"pca": "PCA",
"reduction-method": "降维方法",
"tsne": "T-SNE"
"dimension-value": {
"2d": "2D",
"3d": "3D"
},
"iteration": "迭代",
"learning-rate": "学习率",
"loading": {
"calculating": "计算中……",
"fetching-metadata": "获取元数据中……",
"fetching-tensor": "获取数据中……",
"parsing": "解析数据中……",
"reading-metadata": "读取元数据中……",
"reading-vector": "读取数据中……"
},
"matched-result-count": "匹配结果 {{count}}",
"neighbors": "相邻数据点数量",
"pause": "暂停",
"perplexity": "困惑度",
"points": "数据点",
"reduction-value": {
"pca": "PCA",
"tsne": "T-SNE",
"umap": "UMAP"
},
"reset-zoom": "重置大小设置",
"run": "运行",
"search-empty": "无搜索结果",
"select-color": "颜色分类方式",
"select-data": "选择可视化数据",
"select-file": "选择文件",
"select-label": "数据标签",
"selection": "框选数据",
"stop": "停止",
"total-variance-described": "主成分解释原变量总方差",
"tsne": "T-SNE",
"upload": {
"step1": "步骤一:上传向量的TSV文件<1/>以下是三个带有四个维度的向量的例子",
"step2": "步骤二(可选):上传Metadata的TSV文件<1/>以下是四列三个数据点的例子:<3/><4>注意:如果数据列数大于一,那么第一行数据会默认为列名</4>"
},
"upload-data": "上传数据",
"upload-from-computer": "从本地电脑上传数据"
}
......@@ -62,11 +62,11 @@ module.exports = {
}, {})
},
devOptions: {
out: 'dist',
hostname: process.env.HOST || 'localhost',
port
},
buildOptions: {
out: 'dist',
baseUrl: '/', // set it in post-build
clean: true
},
......
......@@ -16,12 +16,11 @@
// cSpell:words pageview inited
import React, {FunctionComponent, Suspense, useCallback, useEffect, useMemo, useState} from 'react';
import React, {FunctionComponent, Suspense, useCallback, useEffect, useMemo} from 'react';
import {Redirect, Route, BrowserRouter as Router, Switch, useLocation} from 'react-router-dom';
import {THEME, matchMedia} from '~/utils/theme';
import {headerHeight, position, size, zIndexes} from '~/utils/style';
import BodyLoading from '~/components/BodyLoading';
import ErrorBoundary from '~/components/ErrorBoundary';
import ErrorPage from '~/pages/error';
import {Helmet} from 'react-helmet';
......@@ -31,14 +30,12 @@ import {SWRConfig} from 'swr';
import {ToastContainer} from 'react-toastify';
import {actions} from '~/store';
import {fetcher} from '~/utils/fetch';
import init from '@visualdl/wasm';
import routes from '~/routes';
import styled from 'styled-components';
import {useDispatch} from 'react-redux';
import {useTranslation} from 'react-i18next';
const BASE_URI: string = import.meta.env.SNOWPACK_PUBLIC_BASE_URI;
const PUBLIC_PATH: string = import.meta.env.SNOWPACK_PUBLIC_PATH;
const Main = styled.main`
padding-top: ${headerHeight};
......@@ -85,16 +82,6 @@ const App: FunctionComponent = () => {
const dir = useMemo(() => (i18n.language ? i18n.dir(i18n.language) : ''), [i18n]);
const [inited, setInited] = useState(false);
useEffect(() => {
(async () => {
if (!inited) {
await init(`${PUBLIC_PATH}/wasm/visualdl.wasm`);
setInited(true);
}
})();
}, [inited]);
const dispatch = useDispatch();
const toggleTheme = useCallback(
......@@ -123,31 +110,27 @@ const App: FunctionComponent = () => {
revalidateOnReconnect: false
}}
>
{!inited ? (
<BodyLoading />
) : (
<Main>
<Router basename={BASE_URI || '/'}>
<Telemetry />
<Header>
<Navbar />
</Header>
<ErrorBoundary fallback={<ErrorPage />}>
<Suspense fallback={<Progress />}>
<Switch>
<Redirect exact from="/" to={defaultRoute?.path ?? '/index'} />
{routers.map(route => (
<Route key={route.id} path={route.path} component={route.component} />
))}
<Route path="*">
<ErrorPage title={t('errors:page-not-found')} />
</Route>
</Switch>
</Suspense>
</ErrorBoundary>
</Router>
</Main>
)}
<Main>
<Router basename={BASE_URI || '/'}>
<Telemetry />
<Header>
<Navbar />
</Header>
<ErrorBoundary fallback={<ErrorPage />}>
<Suspense fallback={<Progress />}>
<Switch>
<Redirect exact from="/" to={defaultRoute?.path ?? '/index'} />
{routers.map(route => (
<Route key={route.id} path={route.path} component={route.component} />
))}
<Route path="*">
<ErrorPage title={t('errors:page-not-found')} />
</Route>
</Switch>
</Suspense>
</ErrorBoundary>
</Router>
</Main>
<ToastContainer />
</SWRConfig>
</div>
......
......@@ -15,7 +15,7 @@
*/
import React, {FunctionComponent} from 'react';
import {position, primaryColor, size, transitionProps} from '~/utils/style';
import {position, primaryColor, rem, size, transitionProps, zIndexes} from '~/utils/style';
import HashLoader from 'react-spinners/HashLoader';
import styled from 'styled-components';
......@@ -25,17 +25,24 @@ const Wrapper = styled.div`
${position('fixed', 0, 0, 0, 0)}
background-color: var(--mask-color);
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
overscroll-behavior: none;
cursor: progress;
${transitionProps('background-color')}
z-index: ${zIndexes.component};
> .loading-detail {
margin-top: ${rem(20)};
}
`;
const BodyLoading: FunctionComponent = () => {
const BodyLoading: FunctionComponent = ({children}) => {
return (
<Wrapper>
<HashLoader size="60px" color={primaryColor} />
{children ? <div className="loading-detail">{children}</div> : null}
</Wrapper>
);
};
......
......@@ -14,8 +14,8 @@
* limitations under the License.
*/
import React, {FunctionComponent} from 'react';
import {WithStyled, borderRadius, css, ellipsis, em, half, sameBorder, transitionProps} from '~/utils/style';
import React, {FunctionComponent, useCallback, useMemo} from 'react';
import {WithStyled, borderRadius, ellipsis, em, half, transitionProps} from '~/utils/style';
import type {Icons} from '~/components/Icon';
import RawIcon from '~/components/Icon';
......@@ -24,30 +24,25 @@ import styled from 'styled-components';
const height = em(36);
const defaultColor = {
default: 'var(--border-color)',
focused: 'var(--border-focused-color)',
active: 'var(--border-active-color)'
const buttonColors = {
...colors,
default: {
default: 'var(--border-color)',
focused: 'var(--border-focused-color)',
active: 'var(--border-active-color)',
text: 'var(--text-color)'
} as const
} as const;
type colorTypes = keyof typeof colors;
type colorTypes = keyof typeof buttonColors;
const statusButtonColor: (
status: 'focused' | 'active'
) => (props: {disabled?: boolean; type?: colorTypes}) => ReturnType<typeof css> = status => ({disabled, type}) => css`
${disabled || type ? '' : sameBorder({color: defaultColor[status]})}
background-color: ${disabled ? '' : type ? colors[type][status] : 'transparent'};
`;
const Wrapper = styled.a<{type?: colorTypes; rounded?: boolean; disabled?: boolean}>`
const Wrapper = styled.a<{type: colorTypes}>`
height: ${height};
line-height: ${height};
border-radius: ${props => (props.rounded ? half(height) : borderRadius)};
${props => (props.type ? '' : sameBorder({color: defaultColor.default}))}
background-color: ${props => (props.type ? colors[props.type].default : 'transparent')};
color: ${props =>
props.disabled ? 'var(--text-lighter-color)' : props.type ? colors[props.type].text : 'var(--text-color)'};
cursor: ${props => (props.disabled ? 'not-allowed' : 'pointer')};
border-radius: ${borderRadius};
background-color: ${props => buttonColors[props.type].default};
color: ${props => buttonColors[props.type].text};
cursor: pointer;
display: inline-block;
vertical-align: top;
text-align: center;
......@@ -57,11 +52,47 @@ const Wrapper = styled.a<{type?: colorTypes; rounded?: boolean; disabled?: boole
&:hover,
&:focus {
${statusButtonColor('focused')}
background-color: ${props => buttonColors[props.type].focused};
}
&:active {
${statusButtonColor('active')}
background-color: ${props => buttonColors[props.type].active};
}
&.rounded {
border-radius: ${half(height)};
}
&.outline {
color: ${props => buttonColors[props.type][props.type === 'default' ? 'text' : 'default']};
background-color: transparent;
border: 1px solid ${props => buttonColors[props.type].default};
&:hover,
&:focus {
color: ${props => buttonColors[props.type][props.type === 'default' ? 'text' : 'focused']};
border-color: ${props => buttonColors[props.type].focused};
}
&:active {
color: ${props => buttonColors[props.type][props.type === 'default' ? 'text' : 'active']};
border-color: ${props => buttonColors[props.type].active};
}
}
&.disabled {
&,
&:hover,
&:focus,
&:active {
color: var(--text-lighter-color);
background-color: var(--border-color);
&.outline {
border-color: var(--border-color);
}
}
cursor: not-allowed;
}
`;
......@@ -71,6 +102,7 @@ const Icon = styled(RawIcon)`
type ButtonProps = {
rounded?: boolean;
outline?: boolean;
icon?: Icons;
type?: colorTypes;
disabled?: boolean;
......@@ -80,16 +112,34 @@ type ButtonProps = {
const Button: FunctionComponent<ButtonProps & WithStyled> = ({
disabled,
rounded,
outline,
icon,
type,
children,
className,
onClick
}) => (
<Wrapper className={className} onClick={onClick} type={type} rounded={rounded} disabled={disabled}>
{icon && <Icon type={icon}></Icon>}
{children}
</Wrapper>
);
}) => {
const click = useCallback(() => {
if (disabled) {
return;
}
onClick?.();
}, [disabled, onClick]);
const buttonType = useMemo(() => type || 'default', [type]);
return (
<Wrapper
className={`${className ?? ''} ${rounded ? 'rounded' : ''} ${disabled ? 'disabled' : ''} ${
buttonType === 'default' || outline ? 'outline' : ''
}`}
type={buttonType}
onClick={click}
>
{icon && <Icon type={icon}></Icon>}
{children}
</Wrapper>
);
};
export default Button;
......@@ -15,7 +15,7 @@
*/
import React, {FunctionComponent} from 'react';
import {contentHeight, contentMargin, headerHeight, position, transitionProps} from '~/utils/style';
import {WithStyled, contentHeight, contentMargin, headerHeight, position, transitionProps} from '~/utils/style';
import BodyLoading from '~/components/BodyLoading';
import styled from 'styled-components';
......@@ -42,11 +42,13 @@ const Aside = styled.aside`
type ContentProps = {
aside?: React.ReactNode;
leftAside?: React.ReactNode;
loading?: boolean;
};
const Content: FunctionComponent<ContentProps> = ({children, aside, loading}) => (
<Section>
const Content: FunctionComponent<ContentProps & WithStyled> = ({children, aside, leftAside, loading, className}) => (
<Section className={className}>
{leftAside && <Aside>{leftAside}</Aside>}
<Article>{children}</Article>
{aside && <Aside>{aside}</Aside>}
{loading && <BodyLoading />}
......
/**
* 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 {rem, sameBorder} from '~/utils/style';
import Icon from '~/components/Icon';
import Tippy from '@tippyjs/react';
import styled from 'styled-components';
import {useTranslation} from 'react-i18next';
const Operations = styled.div`
--operation-height: ${rem(36)};
display: flex;
align-items: center;
height: var(--operation-height);
${sameBorder({radius: 'calc(var(--operation-height) / 2)'})}
overflow: hidden;
> a {
cursor: pointer;
font-size: ${rem(16)};
line-height: calc(var(--operation-height) - 2px);
> span {
vertical-align: middle;
display: inline-block;
padding: 0 0.857142857rem;
height: ${rem(20)};
line-height: ${rem(20)};
> * {
vertical-align: middle;
}
}
& + a {
> span {
border-left: 1px solid var(--border-color);
}
}
&.three-d {
font-size: ${rem(20)};
}
&:hover {
color: var(--primary-focused-color);
}
&:active {
color: var(--primary-active-color);
}
}
`;
type ChartOperationsProps = {
onReset?: () => unknown;
};
const ChartOperations: FunctionComponent<ChartOperationsProps> = ({onReset}) => {
const {t} = useTranslation('high-dimensional');
return (
<Operations>
<Tippy content={t('high-dimensional:selection')} placement="bottom" theme="tooltip">
<a>
<span>
<Icon type="selection" />
</span>
</a>
</Tippy>
<Tippy content={t('high-dimensional:3d-label')} placement="bottom" theme="tooltip">
<a className="three-d">
<span>
<Icon type="three-d" />
</span>
</a>
</Tippy>
<Tippy content={t('high-dimensional:reset-zoom')} placement="bottom" theme="tooltip">
<a onClick={() => onReset?.()}>
<span>
<Icon type="reset" />
</span>
</a>
</Tippy>
</Operations>
);
};
export default ChartOperations;
/**
* 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 type {Dimension} from '~/resource/high-dimensional';
import RadioButton from '~/components/RadioButton';
import RadioGroup from '~/components/RadioGroup';
import {useTranslation} from 'react-i18next';
const dimensions: Dimension[] = ['2d', '3d'];
type DimensionSwitchProps = {
value: Dimension;
onChange?: (value: Dimension) => unknown;
};
const DimensionSwitch: FunctionComponent<DimensionSwitchProps> = ({value, onChange}) => {
const {t} = useTranslation('high-dimensional');
const [dimension, setDimension] = useState(value);
useEffect(() => setDimension(value), [value]);
const change = useCallback(
(val: Dimension) => {
setDimension(val);
onChange?.(val);
},
[onChange]
);
return (
<RadioGroup value={dimension} onChange={change}>
{dimensions.map(d => (
<RadioButton key={d} value={d}>
{t(`high-dimensional:dimension-value.${d}`)}
</RadioButton>
))}
</RadioGroup>
);
};
export default DimensionSwitch;
......@@ -14,120 +14,302 @@
* limitations under the License.
*/
import type {Dimension, Reduction} from '~/resource/high-dimensional';
import React, {FunctionComponent, useMemo} from 'react';
import {contentHeight, primaryColor, rem} from '~/utils/style';
import ScatterChart from '~/components/ScatterChart';
import {divide} from '~/resource/high-dimensional';
import type {high_dimensional_divide} from '@visualdl/wasm'; // eslint-disable-line @typescript-eslint/no-unused-vars
import queryString from 'query-string';
import type {PCAResult, Reduction, TSNEResult, UMAPResult} from '~/resource/high-dimensional';
import React, {useCallback, useEffect, useImperativeHandle, useLayoutEffect, useMemo, useRef, useState} from 'react';
import ScatterChart, {ScatterChartRef} from '~/components/ScatterChart';
import ChartOperations from '~/components/HighDimensionalPage/ChartOperations';
import type {InfoData} from '~/worker/high-dimensional/tsne';
import type {WithStyled} from '~/utils/style';
import {rem} from '~/utils/style';
import styled from 'styled-components';
import useHeavyWork from '~/hooks/useHeavyWork';
import {useRunningRequest} from '~/hooks/useRequest';
import {useTranslation} from 'react-i18next';
import wasm from '~/utils/wasm';
const divideWasm = () =>
wasm<typeof high_dimensional_divide>(
'high_dimensional_divide'
).then((high_dimensional_divide): typeof divide => params =>
high_dimensional_divide(params.points, params.labels, !!params.visibility, params.keyword ?? '')
);
// const divideWorker = () => new Worker('~/worker/high-dimensional/divide.worker.ts', {type: 'module'});
const StyledScatterChart = styled(ScatterChart)`
height: ${contentHeight};
import useWebAssembly from '~/hooks/useWebAssembly';
import useWorker from '~/hooks/useWorker';
function useComputeHighDimensional(
reduction: Reduction,
vectors: Float32Array,
dim: number,
is3D: boolean,
perplexity: number,
learningRate: number,
neighbors: number
) {
const pcaParams = useMemo(() => {
if (reduction === 'pca') {
return [Array.from(vectors), dim, 3] as const;
}
return [[], 0, 3];
}, [reduction, vectors, dim]);
const tsneInitParams = useRef({perplexity, epsilon: learningRate});
const tsneParams = useMemo(() => {
if (reduction === 'tsne') {
return {
input: vectors,
dim,
n: is3D ? 3 : 2,
...tsneInitParams.current
};
}
return {
input: new Float32Array(),
dim: 0,
n: is3D ? 3 : 2,
perplexity: 5
};
}, [reduction, vectors, dim, is3D]);
const umapParams = useMemo(() => {
if (reduction === 'umap') {
return {
input: vectors,
dim,
n: is3D ? 3 : 2,
neighbors
};
}
return {
input: new Float32Array(),
dim: 0,
n: is3D ? 3 : 2,
neighbors: 15
};
}, [reduction, vectors, dim, is3D, neighbors]);
const pcaResult = useWebAssembly<PCAResult>('high_dimensional_pca', pcaParams);
const tsneResult = useWorker<TSNEResult>('high-dimensional/tsne', tsneParams);
const umapResult = useWorker<UMAPResult>('high-dimensional/umap', umapParams);
if (reduction === 'pca') {
return pcaResult;
}
if (reduction === 'tsne') {
return tsneResult;
}
if (reduction === 'umap') {
return umapResult;
}
return null as never;
}
const Wrapper = styled.div`
height: 100%;
display: flex;
flex-direction: column;
justify-content: stretch;
align-items: stretch;
`;
const Empty = styled.div`
const Toolbar = styled.div`
display: flex;
justify-content: center;
justify-content: space-between;
align-items: center;
font-size: ${rem(20)};
height: ${contentHeight};
margin-bottom: ${rem(20)};
> .info {
color: var(--text-light-color);
> .sep {
display: inline-block;
width: 1px;
background-color: var(--border-color);
margin: 0 1em;
height: 1em;
vertical-align: middle;
}
}
`;
const label = {
show: true,
position: 'top',
formatter: (params: {data: {name: string; showing: boolean}}) => (params.data.showing ? params.data.name : '')
};
const Chart = styled.div`
width: 100%;
height: 100%;
overflow: hidden;
font-size: 0;
* {
outline: none;
}
`;
type Data = {
embedding: ([number, number] | [number, number, number])[];
type HighDimensionalChartProps = {
vectors: Float32Array;
labels: string[];
dim: number;
is3D: boolean;
reduction: Reduction;
perplexity: number;
learningRate: number;
neighbors: number;
highlightIndices?: number[];
onCalculate?: () => unknown;
onCalculated?: (data: PCAResult | TSNEResult | UMAPResult) => unknown;
onError?: (e: Error) => unknown;
};
type HighDimensionalChartProps = {
run: string;
tag: string;
running?: boolean;
labelVisibility?: boolean;
reduction: Reduction;
keyword: string;
dimension: Dimension;
export type HighDimensionalChartRef = {
pauseTSNE(): void;
resumeTSNE(): void;
rerunTSNE(): void;
rerunUMAP(): void;
};
const HighDimensionalChart: FunctionComponent<HighDimensionalChartProps> = ({
run,
tag,
running,
labelVisibility,
keyword,
reduction,
dimension
}) => {
const {t} = useTranslation('common');
const {data, error, loading} = useRunningRequest<Data>(
run && tag
? `/embedding/embedding?${queryString.stringify({
run,
tag,
dimension: Number.parseInt(dimension, 10),
reduction
})}`
: null,
!!running
);
const divideParams = useMemo(
() => ({
points: data?.embedding ?? [],
keyword,
labels: data?.labels ?? [],
visibility: labelVisibility
}),
[data, labelVisibility, keyword]
);
const points = useHeavyWork(divideWasm, null, divide, divideParams);
const chartData = useMemo(() => {
return [
{
name: 'highlighted',
data: points?.[0] ?? [],
label
},
{
name: 'others',
data: points?.[1] ?? [],
label,
color: primaryColor
const HighDimensionalChart = React.forwardRef<HighDimensionalChartRef, HighDimensionalChartProps & WithStyled>(
(
{
vectors,
labels,
dim,
is3D,
reduction,
perplexity,
learningRate,
neighbors,
highlightIndices,
onCalculate,
onCalculated,
onError,
className
},
ref
) => {
const {t} = useTranslation(['high-dimensional', 'common']);
const chartElement = useRef<HTMLDivElement>(null);
const chart = useRef<ScatterChartRef>(null);
const [width, setWidth] = useState(0);
const [height, setHeight] = useState(0);
const points = useMemo(() => Math.floor(vectors.length / dim), [vectors, dim]);
useLayoutEffect(() => {
const c = chartElement.current;
if (c) {
const observer = new ResizeObserver(() => {
const rect = c.getBoundingClientRect();
setWidth(rect.width);
setHeight(rect.height);
});
observer.observe(c);
return () => observer.unobserve(c);
}
];
}, [points]);
}, []);
if (!data && error) {
return <Empty>{t('common:error')}</Empty>;
}
const {data, error, worker} = useComputeHighDimensional(
reduction,
vectors,
dim,
is3D,
perplexity,
learningRate,
neighbors
);
if (!data && !loading) {
return <Empty>{t('common:empty')}</Empty>;
const iterationId = useRef<number | null>(null);
const iteration = useCallback(() => {
worker?.emit<InfoData>('INFO', {type: 'step'});
}, [worker]);
const nextIteration = useCallback(() => {
iterationId.current = requestAnimationFrame(iteration);
}, [iteration]);
const startIteration = useCallback(() => {
if (reduction === 'tsne') {
worker?.on('RESULT', nextIteration);
iteration();
}
}, [reduction, worker, nextIteration, iteration]);
const stopIteration = useCallback(() => {
if (reduction === 'tsne') {
if (iterationId.current) {
cancelAnimationFrame(iterationId.current);
iterationId.current = null;
}
worker?.off('RESULT', nextIteration);
}
}, [reduction, worker, nextIteration]);
const restartIteration = useCallback(() => {
if (reduction === 'tsne') {
stopIteration();
worker?.emit<InfoData>('INFO', {type: 'reset'});
worker?.once('RESULT', () => startIteration());
}
}, [reduction, startIteration, stopIteration, worker]);
useEffect(() => {
if (reduction === 'tsne') {
startIteration();
return () => {
stopIteration();
};
}
}, [reduction, startIteration, stopIteration]);
useEffect(() => {
if (reduction === 'tsne') {
worker?.emit<InfoData>('INFO', {
type: 'params',
data: {
perplexity,
epsilon: learningRate
}
});
}
}, [reduction, perplexity, learningRate, worker]);
const rerunUMAP = useCallback(() => {
if (reduction === 'umap') {
worker?.emit('INFO');
}
}, [reduction, worker]);
useEffect(() => {
if (error) {
onError?.(error);
} else if (data) {
onCalculated?.(data);
} else {
onCalculate?.();
}
}, [data, error, onCalculate, onCalculated, onError]);
useImperativeHandle(ref, () => ({
pauseTSNE: stopIteration,
resumeTSNE: startIteration,
rerunTSNE: restartIteration,
rerunUMAP
}));
return (
<Wrapper className={className}>
<Toolbar>
<div className="info">
{t('high-dimensional:points')}
{t('common:colon')}
{points}
<span className="sep"></span>
{t('high-dimensional:data-dimension')}
{t('common:colon')}
{dim}
</div>
<ChartOperations onReset={() => chart.current?.reset()} />
</Toolbar>
<Chart ref={chartElement}>
<ScatterChart
ref={chart}
width={width}
height={height}
data={data?.vectors ?? []}
labels={labels}
is3D={is3D}
rotate={reduction !== 'tsne'}
highlightIndices={highlightIndices ?? []}
/>
</Chart>
</Wrapper>
);
}
);
return <StyledScatterChart loading={loading} data={chartData} gl={dimension === '3d'} />;
};
HighDimensionalChart.displayName = 'HighDimensionalChart';
export default HighDimensionalChart;
/**
* 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, useEffect, useState} from 'react';
import Select, {SelectProps} from '~/components/Select';
import SearchInput from '~/components/SearchInput';
import {rem} from '~/utils/style';
import styled from 'styled-components';
import useSearchValue from '~/hooks/useSearchValue';
const Wrapper = styled.div`
width: 100%;
display: flex;
`;
const LabelSelect = styled<React.FunctionComponent<SelectProps<string>>>(Select)`
width: 45%;
min-width: ${rem(80)};
max-width: ${rem(200)};
border-top-right-radius: 0;
border-bottom-right-radius: 0;
`;
const LabelInput = styled(SearchInput)`
input {
border-left: none;
border-top-left-radius: 0;
border-bottom-left-radius: 0;
}
`;
type LabelSearchResult = {
labelBy: string | undefined;
value: string;
};
export type LabelSearchInputProps = {
labels: string[];
onChange?: (result: LabelSearchResult) => unknown;
};
const LabelSearchInput: FunctionComponent<LabelSearchInputProps> = ({labels, onChange}) => {
const [labelBy, setLabelBy] = useState<string | undefined>(labels[0] ?? undefined);
const [value, setValue] = useState('');
useEffect(() => {
if (labels.length) {
setLabelBy(label => {
if (label && labels.includes(label)) {
return label;
}
return labels[0];
});
} else {
setLabelBy(undefined);
}
}, [labels]);
const debouncedValue = useSearchValue(value);
useEffect(() => {
onChange?.({
labelBy,
value: debouncedValue
});
}, [labelBy, onChange, debouncedValue]);
return (
<Wrapper>
<LabelSelect list={labels} value={labelBy} onChange={setLabelBy} />
<LabelInput value={value} onChange={setValue} />
</Wrapper>
);
};
export default LabelSearchInput;
/**
* 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 {ellipsis, rem, transitionProps} from '~/utils/style';
import styled from 'styled-components';
import {useTranslation} from 'react-i18next';
const PUBLIC_PATH: string = import.meta.env.SNOWPACK_PUBLIC_PATH;
const Empty = styled.div`
width: 100%;
text-align: center;
font-size: ${rem(14)};
color: var(--text-lighter-color);
line-height: ${rem(20)};
height: auto;
padding: ${rem(170)} 0 ${rem(50)};
background-color: var(--background-color);
background-image: url(${`${PUBLIC_PATH}/images/empty.svg`});
background-repeat: no-repeat;
background-position: calc(50% + ${rem(12)}) ${rem(50)};
background-size: ${rem(140)} ${rem(122)};
${transitionProps(['color', 'background-color'])}
`;
const List = styled.ul`
list-style: none;
line-height: 2.3;
padding-left: 0;
margin: 0;
> li {
list-style: none;
> a {
cursor: pointer;
${ellipsis()}
display: block;
padding: 0 ${rem(20)};
${transitionProps('background-color')}
&:hover {
background-color: var(--background-focused-color);
}
}
}
`;
type LabelSearchResultProps = {
list: string[];
};
const LabelSearchResult: FunctionComponent<LabelSearchResultProps> = ({list}) => {
const {t} = useTranslation('high-dimensional');
if (!list.length) {
return <Empty>{t('high-dimensional:search-empty')}</Empty>;
}
return (
<List>
{list.map((label, index) => (
<li key={index}>
<a>{label}</a>
</li>
))}
</List>
);
};
export default LabelSearchResult;
/**
* 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 type {Dimension} from '~/resource/high-dimensional';
import Field from '~/components/Field';
import {format} from 'd3-format';
import styled from 'styled-components';
import {useTranslation} from 'react-i18next';
const formatRatio = format('.2%');
const Wrapper = styled(Field)`
line-height: 2.4;
`;
export type PCADetailProps = {
dimension: Dimension;
variance: number[];
};
const PCADetail: FunctionComponent<PCADetailProps> = ({dimension, variance}) => {
const {t} = useTranslation(['high-dimensional', 'common']);
const dim = useMemo(() => (dimension === '3d' ? 3 : 2), [dimension]);
const totalVariance = useMemo(() => variance.reduce((s, c) => s + c, 0), [variance]);
return (
<Wrapper>
{Array.from({length: dim}).map((_, index) => (
<div key={index}>
{t('high-dimensional:component', {index: index + 1})}
{t('common:colon')}
{variance[index] == null ? '--' : formatRatio(variance[index])}
</div>
))}
<div className="secondary">
{t('high-dimensional:total-variance-described')}
{t('common:colon')}
{formatRatio(totalVariance)}
</div>
</Wrapper>
);
};
export default PCADetail;
/**
* 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 {Reduction} from '~/resource/high-dimensional';
import Tab from '~/components/Tab';
import {useTranslation} from 'react-i18next';
const reductions: Reduction[] = ['tsne', 'pca', 'umap'];
type ReductionTabProps = {
value: Reduction;
onChange?: (value: Reduction) => unknown;
};
const ReductionTab: FunctionComponent<ReductionTabProps> = ({value, onChange}) => {
const {t} = useTranslation('high-dimensional');
return (
<Tab
list={reductions.map(value => ({value, label: t(`high-dimensional:reduction-value.${value}`)}))}
value={value}
onChange={onChange}
/>
);
};
export default ReductionTab;
/**
* 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, useState} from 'react';
import Button from '~/components/Button';
import Field from '~/components/Field';
import Slider from '~/components/Slider';
import {rem} from '~/utils/style';
import styled from 'styled-components';
import {useTranslation} from 'react-i18next';
const ButtonField = styled(Field)`
> *:not(:last-child) {
margin-right: ${rem(10)};
}
`;
export type TSNEDetailProps = {
iteration: number;
perplexity: number;
learningRate: number;
onChangePerplexity?: (perplexity: number) => void;
onChangeLearningRate?: (learningRate: number) => void;
onPause?: () => void;
onResume?: () => void;
onStop?: () => void;
onRerun?: () => void;
};
const TSNEDetail: FunctionComponent<TSNEDetailProps> = ({
iteration,
perplexity,
learningRate,
onChangePerplexity,
onChangeLearningRate,
onPause,
onResume,
onStop,
onRerun
}) => {
const {t} = useTranslation(['high-dimensional', 'common']);
const [localPerplexity, setPerplexity] = useState(perplexity);
const changePerplexity = useCallback(
(perplexity: number) => {
setPerplexity(perplexity);
onChangePerplexity?.(perplexity);
},
[onChangePerplexity]
);
const [localLearningRate, setLearningRate] = useState(learningRate);
const changeLearningRate = useCallback(
(learningRate: number) => {
setLearningRate(learningRate);
onChangeLearningRate?.(learningRate);
},
[onChangeLearningRate]
);
const [paused, setPaused] = useState(false);
const togglePaused = useCallback(
() =>
setPaused(p => {
if (p) {
onResume?.();
} else {
onPause?.();
}
return !p;
}),
[onResume, onPause]
);
const [stopped, setStopped] = useState(false);
const toggleStopped = useCallback(() => {
setPaused(false);
setStopped(s => {
if (s) {
onRerun?.();
} else {
onStop?.();
}
return !s;
});
}, [onRerun, onStop]);
return (
<>
<Field label={t('high-dimensional:perplexity')}>
<Slider min={2} max={100} step={1} value={localPerplexity} onChange={changePerplexity} />
</Field>
<Field label={t('high-dimensional:learning-rate')}>
<Slider
steps={[0.001, 0.01, 0.1, 1, 10, 100]}
value={localLearningRate}
onChange={changeLearningRate}
/>
</Field>
<ButtonField>
<Button type="primary" outline rounded onClick={toggleStopped}>
{stopped ? t('high-dimensional:run') : t('high-dimensional:stop')}
</Button>
<Button type="primary" outline rounded disabled={stopped} onClick={togglePaused}>
{paused ? t('high-dimensional:continue') : t('high-dimensional:pause')}
</Button>
</ButtonField>
<Field className="secondary">
{t('high-dimensional:iteration')}
{t('common:colon')}
{iteration}
</Field>
</>
);
};
export default TSNEDetail;
/**
* 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 Button from '~/components/Button';
import Field from '~/components/Field';
import Slider from '~/components/Slider';
import {useTranslation} from 'react-i18next';
export type UMAPDetailProps = {
neighbors: number;
onRun?: (neighbors: number) => void;
};
const UMAPDetail: FunctionComponent<UMAPDetailProps> = ({neighbors, onRun}) => {
const {t} = useTranslation('high-dimensional');
const [localNeighbors, setNeighbors] = useState(neighbors);
return (
<>
<Field label={t('high-dimensional:neighbors')}>
<Slider min={5} max={50} step={1} value={localNeighbors} onChange={setNeighbors} />
</Field>
<Field>
<Button type="primary" outline rounded onClick={() => onRun?.(localNeighbors)}>
{t('high-dimensional:run')}
</Button>
</Field>
</>
);
};
export default UMAPDetail;
/**
* 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 {Trans, useTranslation} from 'react-i18next';
import {borderRadius, em, rem, size, transitionProps, zIndexes} from '~/utils/style';
import Button from '~/components/Button';
import Icon from '~/components/Icon';
import styled from 'styled-components';
const Dialog = styled.div`
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
overscroll-behavior: none;
background-color: var(--dark-mask-color);
z-index: ${zIndexes.dialog};
${transitionProps('background-color')}
> .modal {
width: ${rem(700)};
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
box-shadow: 0 2px 20px 0 rgba(0, 0, 0, 0.08);
background-color: var(--background-color);
border-radius: ${borderRadius};
${transitionProps('background-color')}
> .modal-header {
padding: 0 ${em(40, 16)};
height: ${em(55, 16)};
line-height: ${em(55, 16)};
text-align: center;
font-size: ${em(16)};
font-weight: 700;
position: relative;
border-bottom: 1px solid var(--border-color);
> .modal-close {
position: absolute;
right: 0;
${size(em(55, 16), em(55, 16))}
line-height: ${em(55, 16)};
font-size: ${em(20, 16)};
text-align: center;
cursor: pointer;
color: var(--text-lighter-color);
}
}
> .modal-body {
margin: ${rem(40)} 0;
height: ${rem(314)};
}
}
`;
const STEP1_TSV_EXAMPLE = `0.1\\t0.2\\t0.5\\t0.9\n0.2\\t0.1\\t5.0\\t0.2\n0.4\\t0.1\\t7.0\\t0.8`;
const STEP2_TSV_EXAMPLE = `Pokémon\\tSpecies\nWartortle\\tTurtle\nVenusaur\\tSeed\nCharmeleon\\tFlame`;
const Uploader = styled.div`
width: 100%;
height: 100%;
display: flex;
align-items: stretch;
justify-content: space-between;
> .item {
flex: 1;
padding: 0 ${rem(40)};
display: flex;
flex-direction: column;
align-items: flex-start;
justify-content: space-between;
&:not(:last-child) {
border-right: 1px solid var(--border-color);
}
.desc {
height: 30%;
line-height: 1.7;
.tip {
color: var(--text-light-color);
}
}
pre {
height: 40%;
margin: 0;
padding: ${rem(30)} ${rem(40)};
color: var(--code-color);
background-color: var(--code-background-color);
}
}
`;
type UploadDialogProps = {
open?: boolean;
hasVector?: boolean;
onClose?: () => unknown;
onChangeVectorFile?: (file: File) => unknown;
onChangeMetadataFile?: (file: File) => unknown;
};
const UploadDialog: FunctionComponent<UploadDialogProps> = ({
open,
hasVector,
onClose,
onChangeVectorFile,
onChangeMetadataFile
}) => {
const {t} = useTranslation('high-dimensional');
const [show, setShow] = useState(!!open);
useEffect(() => setShow(!!open), [open]);
const close = useCallback(() => onClose?.(), [onClose]);
const vectorFileInput = useRef<HTMLInputElement>(null);
const metadataFileInput = useRef<HTMLInputElement>(null);
const changeVectorFile = useCallback(
(e: React.ChangeEvent<HTMLInputElement>) => {
const target = e.target;
if (target && target.files && target.files.length) {
onChangeVectorFile?.(target.files[0]);
}
},
[onChangeVectorFile]
);
const changeMetadataFile = useCallback(
(e: React.ChangeEvent<HTMLInputElement>) => {
const target = e.target;
if (target && target.files && target.files.length) {
onChangeMetadataFile?.(target.files[0]);
}
},
[onChangeMetadataFile]
);
const clickVectorFileInput = useCallback(() => {
if (vectorFileInput.current) {
vectorFileInput.current.value = '';
vectorFileInput.current.click();
}
}, []);
const clickMetadataFileInput = useCallback(() => {
if (metadataFileInput.current) {
metadataFileInput.current.value = '';
metadataFileInput.current.click();
}
}, []);
if (!show) {
return null;
}
return (
<Dialog>
<div className="modal">
<div className="modal-header">
{t('high-dimensional:upload-from-computer')}
<a className="modal-close" onClick={close}>
<Icon type="close" />
</a>
</div>
<div className="modal-body">
<Uploader>
<div className="item">
<div className="desc">
<Trans i18nKey="high-dimensional:upload.step1">
Step 1: Load a TSV file of vectors.
<br />
Example of 3 vectors with dimension 4:
</Trans>
</div>
<pre>{STEP1_TSV_EXAMPLE}</pre>
<Button type="primary" onClick={clickVectorFileInput}>
{t('high-dimensional:select-file')}
</Button>
</div>
<div className="item">
<div className="desc">
<Trans i18nKey="high-dimensional:upload.step2">
Step 2 (optional): Load a TSV file of metadata.
<br />
Example of 3 data points and 2 columns.
<br />
<span className="tip">
Note: If there is more than one column, the first row will be parsed as column
labels.
</span>
</Trans>
</div>
<pre>{STEP2_TSV_EXAMPLE}</pre>
<Button type="primary" disabled={!hasVector} onClick={clickMetadataFileInput}>
{t('high-dimensional:select-file')}
</Button>
</div>
</Uploader>
</div>
</div>
<input
ref={vectorFileInput}
type="file"
multiple={false}
onChange={changeVectorFile}
style={{
display: 'none'
}}
/>
<input
ref={metadataFileInput}
type="file"
multiple={false}
onChange={changeMetadataFile}
style={{
display: 'none'
}}
/>
</Dialog>
);
};
export default UploadDialog;
......@@ -31,7 +31,7 @@ const Wrapper = styled.div`
z-index: ${zIndexes.dialog};
height: 100vh;
width: 100vw;
background-color: var(--sample-preview-mask-color);
background-color: var(--dark-mask-color);
`;
const Header = styled.div`
......
......@@ -23,11 +23,11 @@ import GridLoader from 'react-spinners/GridLoader';
import type {Run} from '~/types';
import StepSlider from '~/components/SamplePage/StepSlider';
import {fetcher} from '~/utils/fetch';
import fileSaver from 'file-saver';
import {formatTime} from '~/utils';
import isEmpty from 'lodash/isEmpty';
import mime from 'mime-types';
import queryString from 'query-string';
import {saveAs} from 'file-saver';
import styled from 'styled-components';
import useRequest from '~/hooks/useRequest';
import {useRunningRequest} from '~/hooks/useRequest';
......@@ -268,7 +268,7 @@ const SampleChart: FunctionComponent<SampleChartProps> = ({
const download = useCallback(() => {
if (entityData) {
const ext = entityData.type ? mime.extension(entityData.type) : null;
saveAs(
fileSaver.saveAs(
entityData.data,
`${run.label}-${tag}-${steps[step]}-${wallTime.toString().replace(/\./, '_')}`.replace(
/[/\\?%*:|"<>]/g,
......
/**
* 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, useEffect, useMemo} from 'react';
import {WithStyled, position, primaryColor, size, transitionProps} from '~/utils/style';
import useECharts, {useChartTheme} from '~/hooks/useECharts';
import GridLoader from 'react-spinners/GridLoader';
import styled from 'styled-components';
const Wrapper = styled.div`
position: relative;
background-color: var(--background-color);
${transitionProps('background-color')}
> .echarts {
height: 100%;
}
> .loading {
${position('absolute', 0, null, null, 0)}
${size('100%')}
display: flex;
justify-content: center;
align-items: center;
}
`;
const SYMBOL_SIZE = 12;
const options2D = {
xAxis: {},
yAxis: {},
toolbox: {
show: true,
showTitle: false,
itemSize: 0,
feature: {
dataZoom: {},
restore: {},
saveAsImage: {}
}
}
};
const options3D = {
grid3D: {},
xAxis3D: {},
yAxis3D: {},
zAxis3D: {}
};
const series2D = {
symbolSize: SYMBOL_SIZE,
type: 'scatter'
};
const series3D = {
symbolSize: SYMBOL_SIZE,
type: 'scatter3D'
};
type ScatterChartProps = {
loading?: boolean;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
data?: Record<string, any>[];
gl?: boolean;
};
const ScatterChart: FunctionComponent<ScatterChartProps & WithStyled> = ({data, loading, gl, className}) => {
const {ref, echart, wrapper} = useECharts<HTMLDivElement>({
loading,
gl,
autoFit: true
});
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const {tooltip, ...theme} = useChartTheme(gl);
const chartOptions = useMemo(
() => ({
...(gl ? options3D : options2D),
...theme,
series:
data?.map(series => ({
...(gl ? series3D : series2D),
...series
})) ?? []
}),
[gl, data, theme]
);
useEffect(() => {
echart?.setOption(chartOptions);
}, [chartOptions, echart]);
return (
<Wrapper ref={wrapper} className={className}>
{!echart && (
<div className="loading">
<GridLoader color={primaryColor} size="10px" />
</div>
)}
<div className="echarts" ref={ref}></div>
</Wrapper>
);
};
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.
*/
import Chart, {ScatterChartOptions as ChartOptions} from './ScatterChart';
import React, {useEffect, useImperativeHandle, useRef} from 'react';
import type {Point3D} from './types';
import type {WithStyled} from '~/utils/style';
import styled from 'styled-components';
import {themes} from '~/utils/theme';
import useTheme from '~/hooks/useTheme';
const Wrapper = styled.div<{dark?: boolean}>`
position: relative;
filter: ${props =>
props.dark ? 'invert(99%) sepia(7%) saturate(5670%) hue-rotate(204deg) brightness(96%) contrast(79%)' : ''};
`;
export type ScatterChartProps = {
width: number;
height: number;
data: Point3D[];
labels: string[];
is3D: boolean;
rotate?: boolean;
highlightIndices: number[];
};
export type ScatterChartRef = {
reset(): void;
};
const ScatterChart = React.forwardRef<ScatterChartRef, ScatterChartProps & WithStyled>(
({width, height, data, labels, is3D, rotate, highlightIndices, className}, ref) => {
const theme = useTheme();
const element = useRef<HTMLDivElement>(null);
const chart = useRef<Chart | null>(null);
const options = useRef<ChartOptions>({width, height, is3D, background: themes.light.backgroundColor});
useEffect(() => {
if (element.current) {
chart.current = new Chart(element.current, options.current);
return () => {
chart.current?.dispose();
};
}
}, []);
useEffect(() => {
chart.current?.setDimension(is3D);
if (is3D) {
if (rotate) {
chart.current?.startRotate();
} else {
chart.current?.stopRotate();
}
}
}, [is3D, rotate]);
useEffect(() => {
chart.current?.setData(data);
chart.current?.setLabels(labels);
}, [data, labels]);
useEffect(() => {
chart.current?.setHighLightIndices(highlightIndices);
}, [highlightIndices]);
useEffect(() => {
chart.current?.setSize(width, height);
}, [width, height]);
useImperativeHandle(ref, () => ({
reset: () => {
chart.current?.reset();
}
}));
return <Wrapper className={className} ref={element} dark={theme === 'dark'} />;
}
);
ScatterChart.displayName = 'ScatterChart';
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 roboto
type ScatterChartLabelOptions = {
width: number;
height: number;
};
type LabelRenderContext = {
text: string;
fontSize: number;
fillColor: string;
strokeColor: string;
opacity: number;
x: number;
y: number;
};
function convertToRGBA(color: string, opacity: number) {
if (color.startsWith('rgb')) {
const rgba = color.replace(/^rgba?\(|\)$/g, '');
const [r, g, b, a = '1'] = rgba.split(',').map(c => c.trim());
return `rgba(${r}, ${g}, ${b}, ${Number.parseFloat(a.trim()) * opacity})`;
}
let hex = color.replace('#', '');
if (hex.length === 3) {
hex = `${hex[0]}${hex[0]}${hex[1]}${hex[1]}${hex[2]}${hex[2]}`;
}
const r = parseInt(hex.substring(0, 2), 16);
const g = parseInt(hex.substring(2, 4), 16);
const b = parseInt(hex.substring(4, 6), 16);
return `rgba(${r}, ${g}, ${b}, ${opacity})`;
}
export default class ScatterChartLabel {
private readonly container: HTMLElement;
private canvas: HTMLCanvasElement | null;
width: number;
height: number;
constructor(container: HTMLElement, options: ScatterChartLabelOptions) {
this.container = container;
this.width = options.width;
this.height = options.height;
this.canvas = this.initCanvas();
this.container.appendChild(this.canvas);
}
private initCanvas() {
const canvas = document.createElement('canvas');
const dpr = window.devicePixelRatio || 1;
canvas.width = this.width * dpr;
canvas.height = this.height * dpr;
canvas.style.width = `${this.width}px`;
canvas.style.height = `${this.height}px`;
canvas.style.position = 'absolute';
canvas.style.top = '0';
canvas.style.left = '0';
canvas.style.pointerEvents = 'none';
return canvas;
}
render(labels: LabelRenderContext[]) {
this.clear();
const ctx = this.canvas?.getContext('2d');
if (ctx) {
ctx.miterLimit = 2;
ctx.textBaseline = 'middle';
labels.forEach(label => {
if (!label.text) {
return;
}
ctx.fillStyle = convertToRGBA(label.fillColor, label.opacity);
ctx.strokeStyle = convertToRGBA(label.strokeColor, label.opacity);
ctx.font = `${label.fontSize}px roboto`;
ctx.lineWidth = 3;
ctx.strokeText(label.text, label.x, label.y);
ctx.lineWidth = 6;
ctx.fillText(label.text, label.x, label.y);
});
}
}
clear() {
const ctx = this.canvas?.getContext('2d');
if (ctx) {
const dpr = window.devicePixelRatio || 1;
ctx.clearRect(0, 0, this.width * dpr, this.height * dpr);
}
}
setSize(width: number, height: number) {
this.width = width;
this.height = height;
if (this.canvas) {
const dpr = window.devicePixelRatio || 1;
this.canvas.width = this.width * dpr;
this.canvas.height = this.height * dpr;
this.canvas.style.width = `${this.width}px`;
this.canvas.style.height = `${this.height}px`;
}
}
dispose() {
if (this.canvas) {
this.container.removeChild(this.canvas);
this.canvas = null;
}
}
}
......@@ -14,16 +14,8 @@
* limitations under the License.
*/
export default {
runs: ['train', 'test'],
tags: [
['layer2/biases/summaries/mean', 'test/1234', 'another'],
[
'layer2/biases/summaries/mean',
'layer2/biases/summaries/accuracy',
'layer2/biases/summaries/cost',
'test/431',
'others'
]
]
};
import ScatterChart from './Component';
export type {ScatterChartProps, ScatterChartRef} from './Component';
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.
*/
export type Point2D = [number, number];
export type Point3D = [number, number, number];
......@@ -169,13 +169,12 @@ const Select = <T extends unknown>({
const [isOpened, setIsOpened] = useState(false);
const toggleOpened = useCallback(() => setIsOpened(!isOpened), [isOpened]);
const setIsOpenedFalse = useCallback(() => setIsOpened(false), []);
const closeDropdown = useCallback(() => setIsOpened(false), []);
const [value, setValue] = useState(multiple ? (Array.isArray(propValue) ? propValue : []) : propValue);
useEffect(() => setValue(multiple ? (Array.isArray(propValue) ? propValue : []) : propValue), [
multiple,
propValue,
setValue
propValue
]);
const isSelected = useMemo(() => !!(multiple ? (value as T[]) && (value as T[]).length !== 0 : (value as T)), [
......@@ -186,9 +185,9 @@ const Select = <T extends unknown>({
(mutateValue: T) => {
setValue(mutateValue);
(onChange as OnSingleChange<T>)?.(mutateValue);
setIsOpenedFalse();
closeDropdown();
},
[setIsOpenedFalse, onChange]
[closeDropdown, onChange]
);
const changeMultipleValue = useCallback(
(mutateValue: T, checked: boolean) => {
......@@ -208,7 +207,7 @@ const Select = <T extends unknown>({
[value, onChange]
);
const ref = useClickOutside<HTMLDivElement>(setIsOpenedFalse);
const ref = useClickOutside<HTMLDivElement>(closeDropdown);
const list = useMemo<SelectListItem<T>[]>(
() =>
......
......@@ -14,7 +14,7 @@
* limitations under the License.
*/
import React, {FunctionComponent, useCallback, useState} from 'react';
import React, {FunctionComponent, useCallback, useMemo, useState} from 'react';
import {height, padding} from '~/components/Input';
import {rem, size, transitionProps} from '~/utils/style';
......@@ -53,66 +53,124 @@ const FullWidthRangeSlider = styled(RangeSlider)`
`;
type SliderProps = {
min: number;
max: number;
step: number;
min?: number;
max?: number;
step?: number;
steps?: number[];
value: number;
onChange?: (value: number) => unknown;
onChangeComplete?: (value: number) => unknown;
};
const Slider: FunctionComponent<SliderProps> = ({onChange, onChangeComplete, value, min, max, step}) => {
const Slider: FunctionComponent<SliderProps> = ({onChange, onChangeComplete, value, min, max, step, steps}) => {
const sortedSteps = useMemo(() => steps?.sort() ?? [], [steps]);
const fixedMin = useMemo(() => (sortedSteps.length ? 0 : min ?? 0), [min, sortedSteps]);
const fixedMax = useMemo(() => (sortedSteps.length ? sortedSteps.length - 1 : max ?? 1), [max, sortedSteps]);
const fixedStep = useMemo(() => (sortedSteps.length ? 1 : step ?? 1), [step, sortedSteps]);
const fixNumber = useCallback(
(v: number) =>
new BigNumber(v).dividedBy(step).integerValue(BigNumber.ROUND_HALF_UP).multipliedBy(step).toNumber(),
[step]
new BigNumber(v)
.dividedBy(fixedStep)
.integerValue(BigNumber.ROUND_HALF_UP)
.multipliedBy(fixedStep)
.toNumber(),
[fixedStep]
);
const actualValueByInput = useCallback(
(v: number) => {
if (sortedSteps.length) {
let r = Number.NaN;
let d = Number.POSITIVE_INFINITY;
for (let i = 0; i < sortedSteps.length; i++) {
const c = Math.abs(sortedSteps[i] - v);
if (d > c) {
d = c;
r = sortedSteps[i];
}
}
return r;
}
return fixNumber(v);
},
[fixNumber, sortedSteps]
);
const sliderValueByInput = useCallback(
(v: number) => {
if (sortedSteps.length) {
let r = -1;
let d = Number.POSITIVE_INFINITY;
for (let i = 0; i < sortedSteps.length; i++) {
const c = Math.abs(sortedSteps[i] - v);
if (d > c) {
d = c;
r = i;
}
}
return r;
}
return fixNumber(v);
},
[fixNumber, sortedSteps]
);
const actualValueBySlider = useCallback(
(v: number) => {
if (sortedSteps.length) {
return sortedSteps[v];
}
return v;
},
[sortedSteps]
);
const [sliderValue, setSliderValue] = useState(fixNumber(value));
const [inputValue, setInputValue] = useState(sliderValue + '');
const [sliderValue, setSliderValue] = useState(sliderValueByInput(value));
const [inputValue, setInputValue] = useState(actualValueByInput(value) + '');
const changeSliderValue = useCallback(
(value: number) => {
const v = fixNumber(value);
setInputValue(v + '');
(v: number) => {
setSliderValue(v);
onChange?.(v);
const actualValue = actualValueBySlider(v);
setInputValue(actualValue + '');
onChange?.(actualValue);
},
[fixNumber, onChange]
[actualValueBySlider, onChange]
);
const changeSliderValueComplete = useCallback(() => {
onChangeComplete?.(actualValueBySlider(sliderValue));
}, [sliderValue, onChangeComplete, actualValueBySlider]);
const changeInputValue = useCallback(
(value: string) => {
setInputValue(value);
(stringValue: string) => {
setInputValue(stringValue);
const v = Number.parseFloat(value);
const v = Number.parseFloat(stringValue);
if (v < min || v > max || Number.isNaN(v)) {
if (v < fixedMin || v > fixedMax || Number.isNaN(v)) {
return;
}
const result = fixNumber(v);
setSliderValue(result);
onChange?.(result);
onChangeComplete?.(result);
setSliderValue(sliderValueByInput(v));
const actualValue = actualValueByInput(v);
onChange?.(actualValue);
onChangeComplete?.(actualValue);
},
[onChange, onChangeComplete, min, max, fixNumber]
[fixedMin, fixedMax, sliderValueByInput, actualValueByInput, onChange, onChangeComplete]
);
const confirmInput = useCallback(() => {
setInputValue(sliderValue + '');
}, [sliderValue]);
setInputValue(actualValueBySlider(sliderValue) + '');
}, [actualValueBySlider, sliderValue]);
return (
<Wrapper>
<FullWidthRangeSlider
min={min}
max={max}
step={step}
min={fixedMin}
max={fixedMax}
step={fixedStep}
value={sliderValue}
onChange={changeSliderValue}
onChangeComplete={() => onChangeComplete?.(sliderValue)}
onChangeComplete={changeSliderValueComplete}
/>
<Input
type="text"
......
......@@ -39,13 +39,7 @@ const Tooltip = styled.div`
${transitionProps(['color', 'background-color'])}
`;
type renderItem = NonNullable<EChartOption.SeriesCustom['renderItem']>;
type renderItemArguments = NonNullable<renderItem['arguments']>;
type RenderItem = (
// eslint-disable-next-line @typescript-eslint/no-explicit-any
params: any,
api: Required<NonNullable<renderItemArguments['api']>>
) => NonNullable<renderItem['return']>;
type RenderItem = EChartOption.SeriesCustom.RenderItem;
type GetValue = (i: number) => number;
type GetCoord = (p: [number, number]) => [number, number];
......@@ -126,11 +120,11 @@ const StackChart = React.forwardRef<StackChartRef, StackChartProps & WithStyled>
return {
type: 'polygon',
silent: true,
z: api.value(1),
z: api.value?.(1),
shape: {
points
},
style: api.style({
style: api.style?.({
stroke: chart.xAxis.axisLine.lineStyle.color,
lineWidth: 1
})
......
/**
* Copyright 2020 Baidu Inc. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React, {FunctionComponent, useCallback, useEffect, useState} from 'react';
import {em, rem, transitionProps} from '~/utils/style';
import styled from 'styled-components';
const Wrapper = styled.div`
display: flex;
align-items: stretch;
justify-content: space-between;
> a {
cursor: pointer;
display: block;
font-size: ${rem(16)};
border-bottom: 2px solid transparent;
${transitionProps(['border-color', 'color'])}
&:not(:last-child) {
margin-right: ${em(20)};
}
&.active {
color: var(--primary-color);
border-bottom-color: var(--primary-color);
}
&:hover {
color: var(--primary-color);
}
}
`;
type TabProps<T> = {
list?: {
value: T;
label: string;
}[];
value?: T;
onChange?: (value: T) => unknown;
};
const Tab = <T extends unknown>({list, value, onChange}: TabProps<T>): ReturnType<FunctionComponent> => {
const [selected, setSelected] = useState<T | undefined>(value);
useEffect(() => setSelected(value), [value]);
const change = useCallback(
(v: T) => {
if (selected !== v) {
setSelected(v);
onChange?.(v);
}
},
[selected, onChange]
);
return (
<Wrapper>
{list?.map((item, index) => (
<a
key={index}
className={(selected === item.value && 'active') || ''}
onClick={() => change(item.value)}
>
{item.label}
</a>
))}
</Wrapper>
);
};
export default Tab;
......@@ -21,7 +21,7 @@ import {position, primaryColor, size} from '~/utils/style';
import type {ECharts} from 'echarts';
import {dataURL2Blob} from '~/utils/image';
import {saveAs} from 'file-saver';
import fileSaver from 'file-saver';
import styled from 'styled-components';
import {themes} from '~/utils/theme';
import useTheme from '~/hooks/useTheme';
......@@ -131,7 +131,7 @@ const useECharts = <T extends HTMLElement, W extends HTMLElement = HTMLDivElemen
(filename?: string) => {
if (echart) {
const blob = dataURL2Blob(echart.getDataURL({type: 'png', pixelRatio: 2, backgroundColor: '#FFF'}));
saveAs(blob, `${filename?.replace(/[/\\?%*:|"<>]/g, '_') || 'chart'}.png`);
fileSaver.saveAs(blob, `${filename?.replace(/[/\\?%*:|"<>]/g, '_') || 'chart'}.png`);
}
},
[echart]
......
/**
* 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 * as funcs from '@visualdl/wasm';
import {useMemo} from 'react';
import useWorker from '~/hooks/useWorker';
type FuncNames = Exclude<keyof typeof funcs, 'default'>;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const useWebAssembly = <D, P = any, E extends Error = Error>(name: FuncNames, params: P) => {
const p = useMemo(() => ({name, params}), [name, params]);
return useWorker<D, typeof p, E>('wasm', p);
};
export default useWebAssembly;
/**
* 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 {useEffect, useMemo, useState} from 'react';
import type {InitializeData} from '~/worker';
import {WebWorker} from '~/worker';
const PUBLIC_PATH: string = import.meta.env.SNOWPACK_PUBLIC_PATH;
type WorkerResult<D, E extends Error> = {
data?: D;
error?: E;
worker?: WebWorker;
};
const useWorker = <D, P = unknown, E extends Error = Error>(name: string, params: P): WorkerResult<D, E> => {
const p = useMemo(() => params, [params]);
const [result, setResult] = useState<WorkerResult<D, E>>({});
useEffect(() => {
const worker = new WebWorker(`${PUBLIC_PATH}/_dist_/worker/${name}.js`, {type: 'module'});
worker.emit<InitializeData>('INITIALIZE', {env: import.meta.env});
worker.on('INITIALIZED', () => {
setResult({worker});
worker.emit('RUN', p);
});
worker.on<D>('RESULT', data => setResult({data, worker}));
worker.on<E>('ERROR', error => setResult({error, worker}));
return () => {
worker.terminate();
};
}, [name, p]);
return result;
};
export default useWorker;
......@@ -14,46 +14,21 @@
* limitations under the License.
*/
import type {Point} from './types';
export type {
Dimension,
Reduction,
PcaParams,
PCAResult,
Vectors,
ParseParams,
ParseResult,
TSNEParams,
TSNEResult,
UMAPParams,
UMAPResult
} from './types';
export type {Dimension, Reduction, Point} from './types';
export {parseFromBlob, parseFromString, ParserError} from './parser';
const dividePoints = (points: Point[], keyword?: string) => {
if (!keyword) {
return [[], points];
}
const matched: Point[] = [];
const missing: Point[] = [];
points.forEach(point => {
if (point.name.includes(keyword)) {
matched.push(point);
return;
}
missing.push(point);
});
return [matched, missing];
};
const combineLabel = (points: Point['value'][], labels: string[], visibility?: boolean) =>
points.map((value, i) => {
const name = labels[i] || '';
return {
name,
showing: !!visibility,
value
};
});
export const divide = ({
points,
keyword,
labels,
visibility
}: {
points: Point['value'][];
keyword?: string;
labels: string[];
visibility?: boolean;
}) => dividePoints(combineLabel(points, labels, visibility), keyword);
export {default as tSNE} from './tsne';
export {default as UMAP} from './umap';
/**
* 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 {MetadataResult, ParseFromBlobParams, ParseFromStringParams, ParseResult, VectorResult} from './types';
import {safeSplit} from '~/utils';
const INDEX_METADATA_FIELD = '__index__';
const DEFAULT_METADATA_FIELD = '__metadata__';
const PARSER_ERROR_CODES = {
NUMBER_MISMATCH: Symbol('NUMBER_MISMATCH'),
TENSER_EMPTY: Symbol('TENSER_EMPTY'),
METADATA_EMPTY: Symbol('METADATA_EMPTY'),
SHAPE_MISMATCH: Symbol('SHAPE_MISMATCH')
} as const;
type ParserErrorCode = string | number | symbol;
export class ParserError extends Error {
static CODES = PARSER_ERROR_CODES;
readonly code: ParserErrorCode;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
readonly data: any;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
constructor(message: string | undefined, code: ParserErrorCode, data?: any) {
super(message);
this.code = code;
this.data = data;
}
}
function split<T = string>(str: string, processer?: (item: string) => T): T[][] {
return safeSplit(str, '\n')
.map(r => safeSplit(r, '\t').map(n => (processer ? processer(n) : n) as T))
.filter(r => r.length);
}
function alignItems<T>(data: T[][], dimension: number, defaultValue: T): T[][] {
return data.map(row => {
const length = row.length;
if (length > dimension) {
return row.slice(0, dimension);
}
if (length < dimension) {
return [...row, ...Array.from<T>({length: dimension - length}).fill(defaultValue)];
}
return row;
});
}
function parseVectors(str: string): VectorResult {
if (!str) {
throw new ParserError('Tenser file is empty', ParserError.CODES.TENSER_EMPTY);
}
let vectors = split(str, Number.parseFloat);
// TODO: sampling
const dimension = Math.min(...vectors.map(vector => vector.length));
vectors = alignItems(vectors, dimension, 0);
return {
dimension,
count: vectors.length,
vectors: new Float32Array(vectors.flat())
};
}
function parseMetadata(str: string): MetadataResult {
if (!str) {
throw new ParserError('Metadata file is empty', ParserError.CODES.METADATA_EMPTY);
}
let metadata = split(str);
// dimension is larger then 0
const dimension = metadata[0].length;
let labels = [DEFAULT_METADATA_FIELD];
metadata = alignItems(metadata, dimension, '');
if (dimension > 1) {
// metadata is larger then 1
labels = metadata.shift() as string[];
}
return {
dimension,
labels,
metadata
};
}
function genMetadataAndLabels(metadata: string, count: number) {
if (metadata) {
const data = parseMetadata(metadata);
const metadataCount = data.metadata.length;
if (count !== metadataCount) {
throw new ParserError(
`Number of tensors (${count}) do not match the number of lines in metadata (${metadataCount}).`,
ParserError.CODES.NUMBER_MISMATCH,
{
vectors: count,
metadata: metadataCount
}
);
}
return {
labels: data.labels,
metadata: data.metadata
};
}
return {
labels: [INDEX_METADATA_FIELD],
metadata: Array.from({length: count}, (_, i) => [`${i}`])
};
}
export function parseFromString({vectors: v, metadata: m}: ParseFromStringParams): ParseResult {
const result: ParseResult = {
dimension: 0,
vectors: new Float32Array(),
labels: [],
metadata: []
};
if (v) {
const {dimension, vectors, count} = parseVectors(v);
result.dimension = dimension;
result.vectors = vectors;
Object.assign(result, genMetadataAndLabels(m, count));
}
return result;
}
export async function parseFromBlob({shape, vectors: v, metadata: m}: ParseFromBlobParams): Promise<ParseResult> {
const [count, dimension] = shape;
const vectors = new Float32Array(await v.arrayBuffer());
if (count * dimension !== vectors.length) {
throw new ParserError('Size of tensor does not match.', ParserError.CODES.SHAPE_MISMATCH);
}
return {
dimension,
vectors,
...genMetadataAndLabels(m, count)
};
}
/**
* 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.
*/
/**
* This is a fork of the Karpathy's TSNE.js (original license below).
*
* The MIT License (MIT)
*
* Copyright (c) 2015 Andrej Karpathy
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
/**
* The algorithm was originally described in this paper:
*
* L.J.P. van der Maaten and G.E. Hinton.
* Visualizing High-Dimensional Data Using t-SNE. Journal of Machine Learning Research
* 9(Nov):2579-2605, 2008.
*
* You can find the PDF [here](http://jmlr.csail.mit.edu/papers/volume9/vandermaaten08a/vandermaaten08a.pdf).
*/
// cSpell:words Maaten Andrej Karpathy Karpathy's Guass randn xtod premult dists gainid
export type tSNEOptions = {
perplexity?: number;
epsilon?: number;
dimension: number;
};
class GuassRandom {
private returnV = false;
private vVal = 0.0;
get value(): number {
if (this.returnV) {
this.returnV = false;
return this.vVal;
}
const u = 2 * Math.random() - 1;
const v = 2 * Math.random() - 1;
const r = u * u + v * v;
if (r === 0 || r > 1) {
return this.value;
}
const c = Math.sqrt((-2 * Math.log(r)) / r);
this.vVal = v * c;
this.returnV = true;
return u * c;
}
}
export default class tSNE {
readonly dimension: number;
epsilon = 10;
perplexity = 30;
private Random = new GuassRandom();
private data = new Float32Array();
private P: Float32Array = new Float32Array();
private Y: number[][] = [];
private gains: number[][] = [];
private yStep: number[][] = [];
private D = 0;
private N = 0;
private iter = 0;
get solution() {
return this.Y;
}
get step() {
return this.iter;
}
constructor(options: tSNEOptions) {
this.dimension = options.dimension;
this.perplexity = options.perplexity ?? this.perplexity;
this.epsilon = options.epsilon ?? this.epsilon;
}
private L2(x1: Float32Array, x2: Float32Array) {
if (x1.length !== x2.length) {
throw new Error('Cannot compare vectors with different length');
}
const D = x1.length;
let d = 0;
for (let i = 0; i < D; i++) {
const x1i = x1[i];
const x2i = x2[i];
d += (x1i - x2i) * (x1i - x2i);
}
return d;
}
private randn2d(s?: number) {
const uses = s != null;
const x: number[][] = [];
for (let i = 0; i < this.N; i++) {
const xHere: number[] = [];
for (let j = 0; j < this.dimension; j++) {
if (uses) {
xHere.push(s as number);
} else {
xHere.push(this.Random.value * 1e-4);
}
}
x.push(xHere);
}
return x;
}
private sliceDataRow(row: number) {
return this.data.slice(row * this.D, (row + 1) * this.D);
}
private xtod() {
const dist = new Float32Array(this.N * this.N);
for (let i = 0; i < this.N; i++) {
for (let j = 0; j < this.N; j++) {
const d = this.L2(this.sliceDataRow(i), this.sliceDataRow(j));
dist[i * this.N + j] = d;
dist[j * this.N + i] = d;
}
}
return dist;
}
private d2p(D: Float32Array, tol = 1e-4) {
const Nf = Math.sqrt(D.length);
const N = Math.floor(Nf);
if (Nf !== N) {
throw new Error('Distance is not a square matrix');
}
const hTarget = Math.log(this.perplexity);
const P = new Float32Array(N * N);
const pRow = new Float32Array(N);
for (let i = 0; i < N; i++) {
let betaMin = Number.NEGATIVE_INFINITY;
let betaMax = Number.POSITIVE_INFINITY;
let beta = 1;
const maxTries = 50;
let num = 0;
while (num < maxTries) {
let pSum = 0.0;
for (let j = 0; j < N; j++) {
let pj = Math.exp(-D[i * N + j] * beta);
if (i === j) {
pj = 0;
}
pRow[j] = pj;
pSum += pj;
}
let hHere = 0.0;
for (let j = 0; j < N; j++) {
const pj = pSum === 0 ? 0 : pRow[j] / pSum;
pRow[j] = pj;
if (pj > 1e-7) {
hHere -= pj * Math.log(pj);
}
}
if (hHere > hTarget) {
betaMin = beta;
if (betaMax === Number.POSITIVE_INFINITY) {
beta *= 2;
} else {
beta = (beta + betaMax) / 2;
}
} else {
betaMax = beta;
if (betaMin === Number.NEGATIVE_INFINITY) {
beta /= 2;
} else {
beta = (beta + betaMin) / 2;
}
}
num++;
if (Math.abs(hHere - hTarget) < tol) {
break;
}
}
for (let j = 0; j < N; j++) {
P[i * N + j] = pRow[j];
}
}
const pOut = new Float32Array(N * N);
const N2 = N * 2;
for (let i = 0; i < N; i++) {
for (let j = 0; j < N; j++) {
pOut[i * N + j] = Math.max((P[i * N + j] + P[j * N + i]) / N2, Number.EPSILON);
}
}
return pOut;
}
private costGrad() {
const Y = this.Y;
const N = this.N;
const dim = this.dimension;
const P = this.P;
const NN = N * N;
const pMul = this.iter < 100 ? 4 : 1;
const Qu = new Float32Array(NN);
let qSum = 0.0;
for (let i = 0; i < N; i++) {
for (let j = i + 1; j < N; j++) {
let dSum = 0.0;
for (let d = 0; d < dim; d++) {
const dHere = Y[i][d] - Y[j][d];
dSum += dHere * dHere;
}
const qu = 1.0 / (1.0 + dSum);
Qu[i * N + j] = qu;
Qu[j * N + i] = qu;
qSum += 2 * qu;
}
}
const Q = new Float32Array(NN);
for (let q = 0; q < NN; q++) {
Q[q] = Math.max(Qu[q] / qSum, Number.EPSILON);
}
let cost = 0.0;
const grad: number[][] = [];
for (let i = 0; i < N; i++) {
const gSum = Array.from<number>({length: dim}).fill(0.0);
for (let j = 0; j < N; j++) {
cost += -P[i * N + j] * Math.log(Q[i * N + j]);
const premult = 4 * (pMul * P[i * N + j] - Q[i * N + j]) * Qu[i * N + j];
for (let d = 0; d < dim; d++) {
gSum[d] += premult * (Y[i][d] - Y[j][d]);
}
}
grad.push(gSum);
}
return {cost, grad};
}
setData(data: Float32Array, dimension: number) {
if (data.length && dimension && data.length % dimension !== 0) {
throw Error('Wrong data shape');
}
if (data.length === 0 || dimension === 0) {
return;
}
this.data = data;
this.D = dimension;
this.N = data.length / dimension;
const dists = this.xtod();
this.P = this.d2p(dists);
this.Y = this.randn2d();
this.gains = this.randn2d(1.0);
this.yStep = this.randn2d(0.0);
this.iter = 0;
}
setPerplexity(perplexity: number) {
this.perplexity = perplexity;
}
setEpsilon(epsilon: number) {
this.epsilon = epsilon;
}
run() {
this.iter++;
const N = this.N;
const {cost, grad} = this.costGrad();
const yMean = new Float32Array(this.dimension);
for (let i = 0; i < N; i++) {
for (let d = 0; d < this.dimension; d++) {
const gid = grad[i][d];
const sid = this.yStep[i][d];
const gainid = this.gains[i][d];
let newGain = Math.sign(gid) === Math.sign(sid) ? gainid * 0.8 : gainid + 0.2;
if (newGain < 0.01) {
newGain = 0.01;
}
this.gains[i][d] = newGain;
const momVal = this.iter < 250 ? 0.5 : 0.8;
const newSid = momVal * sid - this.epsilon * newGain * grad[i][d];
this.yStep[i][d] = newSid;
this.Y[i][d] += newSid;
yMean[d] += this.Y[i][d];
}
}
for (let i = 0; i < N; i++) {
for (let d = 0; d < this.dimension; d++) {
this.Y[i][d] -= yMean[d] / N;
}
}
return cost;
}
}
......@@ -15,10 +15,84 @@
*/
export type Dimension = '2d' | '3d';
export type Reduction = 'pca' | 'tsne';
export type Reduction = 'pca' | 'tsne' | 'umap';
export type Point = {
name: string;
value: [number, number] | [number, number, number];
showing: boolean;
export type Vectors = [number, number, number][];
export type VectorResult = {
dimension: number;
count: number;
vectors: Float32Array;
};
export type MetadataResult = {
dimension: number;
labels: string[];
metadata: string[][];
};
export type ParseFromStringParams = {
vectors: string;
metadata: string;
};
export type ParseFromBlobParams = {
shape: [number, number];
vectors: Blob;
metadata: string;
};
export type ParseParams =
| {
from: 'blob';
params: ParseFromBlobParams;
}
| {
from: 'string';
params: ParseFromStringParams;
}
| null;
export type ParseResult = {
dimension: number;
vectors: Float32Array;
labels: string[];
metadata: string[][];
};
export type PcaParams = {
input: number[];
dim: number;
n: number;
};
export type PCAResult = {
vectors: Vectors;
variance: number[];
};
export type TSNEParams = {
input: Float32Array;
dim: number;
n: number;
perplexity?: number;
epsilon?: number;
};
export type TSNEResult = {
vectors: Vectors;
step: number;
};
export type UMAPParams = {
input: Float32Array;
dim: number;
n: number;
neighbors: number;
};
export type UMAPResult = {
vectors: Vectors;
epoch: number;
nEpochs: number;
};
......@@ -14,42 +14,30 @@
* limitations under the License.
*/
import {Request} from 'express';
import {UMAP} from 'umap-js';
export default (req: Request) => {
const {dimension, run} = req.query;
if (dimension === '3') {
return {
embedding: [
[10.0, 8.04, 3],
[8.0, 6.95, 4],
[13.0, 7.58, 1],
[9.0, 8.81, 3],
[11.0, 8.33, 5],
[14.0, 9.96, 6],
[6.0, 7.24, 1],
[4.0, 4.26, 2],
[12.0, 10.84, 6],
[7.0, 4.8, 3],
[5.0, 5.68, 3]
],
labels: [`${run}-yellow`, 'blue', 'red', 'king', 'queen', 'man', 'women', 'kid', 'adult', 'light', 'dark']
};
export default (
nComponents: number,
nNeighbors: number,
input: Float32Array,
dim: number,
epochCallback?: (epoch: number, nEpochs: number) => unknown
) => {
if (input.length === 0) {
return;
}
const umap = new UMAP({nComponents, nNeighbors});
const X = [];
for (let i = 0; i < input.length; i += dim) {
X.push(Array.from(input.slice(i, i + dim)));
}
const nEpochs = umap.initializeFit(X);
for (let i = 0; i < nEpochs; i++) {
epochCallback?.(i, nEpochs);
umap.step();
}
return {
embedding: [
[10.0, 8.04],
[8.0, 6.95],
[13.0, 7.58],
[9.0, 8.81],
[11.0, 8.33],
[14.0, 9.96],
[6.0, 7.24],
[4.0, 4.26],
[12.0, 10.84],
[7.0, 4.8],
[5.0, 5.68]
],
labels: [`${run}-yellow`, 'blue', 'red', 'king', 'queen', 'man', 'women', 'kid', 'adult', 'light', 'dark']
embedding: umap.getEmbedding(),
nEpochs
};
};
......@@ -80,8 +80,9 @@ function logErrorAndReturnT(e: unknown) {
}
export function fetcher(url: string, options?: RequestInit): Promise<BlobResponse>;
export function fetcher(url: string, options?: RequestInit): Promise<string>;
export function fetcher<T = unknown>(url: string, options?: RequestInit): Promise<T>;
export async function fetcher<T = unknown>(url: string, options?: RequestInit): Promise<BlobResponse | T> {
export async function fetcher<T = unknown>(url: string, options?: RequestInit): Promise<BlobResponse | string | T> {
let res: Response;
try {
res = await fetch(API_URL + url, addApiToken(options));
......@@ -95,8 +96,9 @@ export async function fetcher<T = unknown>(url: string, options?: RequestInit):
throw new Error(t([`errors:response-error.${res.status}`, 'errors:response-error.unknown']));
}
let response: Data<T> | T;
if (res.headers.get('content-type')?.includes('application/json')) {
const contentType = res.headers.get('content-type') ?? '';
if (contentType.includes('application/json')) {
let response: Data<T> | T;
try {
response = await res.json();
} catch (e) {
......@@ -112,6 +114,15 @@ export async function fetcher<T = unknown>(url: string, options?: RequestInit):
}
}
return response;
} else if (contentType.startsWith('text/')) {
let response: string;
try {
response = await res.text();
} catch (e) {
const t = await logErrorAndReturnT(e);
throw new Error(t('errors:parse-error'));
}
return response;
} else {
let data: Blob;
try {
......
......@@ -42,3 +42,5 @@ export const quantile = (values: number[], p: number) => {
export const distance = (p1: [number, number], p2: [number, number]): number =>
Math.sqrt((p1[0] - p2[0]) ** 2 + (p1[1] - p2[1]) ** 2);
export const safeSplit = (s: string, d: string) => (s.length ? s.split(d) : []);
......@@ -74,6 +74,7 @@ export const sameBorder = (
width = '1px' as
| string
| number
| true
| {width?: string | number; type?: string; color?: string; radius?: string | boolean},
type = 'solid',
color = 'var(--border-color)',
......@@ -84,6 +85,9 @@ export const sameBorder = (
color = width.color ?? 'var(--border-color)';
radius = width.radius === true ? borderRadius : width.radius;
width = width.width ?? '1px';
} else if (width === true) {
width = '1px';
radius = true;
}
return Object.assign(
{},
......
......@@ -79,8 +79,7 @@ export const themes = {
tooltipBackgroundColor: 'rgba(0, 0, 0, 0.6)',
progressBarColor: '#fff',
maskColor: 'rgba(255, 255, 255, 0.8)',
samplePreviewMaskColor: 'rgba(0, 0, 0, 0.5)',
darkMaskColor: 'rgba(0, 0, 0, 0.5)',
graphUploaderBackgroundColor: '#f9f9f9',
graphUploaderActiveBackgroundColor: '#f2f6ff',
......@@ -121,8 +120,7 @@ export const themes = {
tooltipBackgroundColor: '#292929',
progressBarColor: '#fff',
maskColor: 'rgba(0, 0, 0, 0.8)',
samplePreviewMaskColor: 'rgba(0, 0, 0, 0.8)',
darkMaskColor: 'rgba(0, 0, 0, 0.8)',
graphUploaderBackgroundColor: '#262629',
graphUploaderActiveBackgroundColor: '#303033',
......
/**
* 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 {parseFromBlob, parseFromString} from '~/resource/high-dimensional';
import type {ParseParams} from '~/resource/high-dimensional';
import {WorkerSelf} from '~/worker';
const workerSelf = new WorkerSelf();
workerSelf.emit('INITIALIZED');
workerSelf.on<ParseParams>('RUN', async data => {
if (data) {
if (data.from === 'string') {
return workerSelf.emit('RESULT', parseFromString(data.params));
}
if (data.from === 'blob') {
return workerSelf.emit('RESULT', await parseFromBlob(data.params));
}
}
workerSelf.emit('RESULT', null);
});
/**
* 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 {TSNEParams, TSNEResult} from '~/resource/high-dimensional';
import {WorkerSelf} from '~/worker';
import {tSNE} from '~/resource/high-dimensional';
import type {tSNEOptions} from '~/resource/high-dimensional/tsne';
type InfoStepData = {
type: 'step';
};
type InfoResetData = {
type: 'reset';
};
type InfoParamsData = {
type: 'params';
data: Partial<Omit<tSNEOptions, 'dimension'>>;
};
export type InfoData = InfoStepData | InfoResetData | InfoParamsData;
const workerSelf = new WorkerSelf();
workerSelf.emit('INITIALIZED');
workerSelf.on<TSNEParams>('RUN', data => {
const t_sne = new tSNE({
dimension: data.n,
perplexity: data.perplexity,
epsilon: data.epsilon
});
const reset = () => {
t_sne.setData(data.input, data.dim);
return workerSelf.emit<TSNEResult>('RESULT', {
vectors: t_sne.solution as [number, number, number][],
step: t_sne.step
});
};
reset();
workerSelf.on<InfoData>('INFO', infoData => {
const type = infoData.type;
switch (type) {
case 'step': {
t_sne.run();
return workerSelf.emit<TSNEResult>('RESULT', {
vectors: t_sne.solution as [number, number, number][],
step: t_sne.step
});
}
case 'reset': {
return reset();
}
case 'params': {
const data = (infoData as InfoParamsData).data;
if (data?.perplexity != null) {
t_sne.setPerplexity(data.perplexity);
}
if (data?.epsilon != null) {
t_sne.setEpsilon(data.epsilon);
}
return;
}
default:
return null as never;
}
});
});
/**
* 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 {UMAPParams, UMAPResult} from '~/resource/high-dimensional';
import {UMAP} from '~/resource/high-dimensional';
import {WorkerSelf} from '~/worker';
const workerSelf = new WorkerSelf();
workerSelf.emit('INITIALIZED');
workerSelf.on<UMAPParams>('RUN', data => {
const result = UMAP(data.n, data.neighbors, data.input, data.dim);
if (result) {
workerSelf.emit<UMAPResult>('RESULT', {
vectors: result.embedding as [number, number, number][],
epoch: result.nEpochs,
nEpochs: result.nEpochs
});
}
});
workerSelf.on('INFO', () => {
workerSelf.emit('INITIALIZED');
});
此差异已折叠。
/**
* 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.
*/
type WorkerMessageType<T extends string, D = void> = {
type: T;
data: D;
};
export type InitializeData = {
env: Record<string, string>;
};
type InitializeMessage = WorkerMessageType<'INITIALIZE', InitializeData>;
type InitializedMessage = WorkerMessageType<'INITIALIZED'>;
type RunMessage<T> = WorkerMessageType<'RUN', T>;
type ResultMessage<T> = WorkerMessageType<'RESULT', T>;
type InfoMessage<T> = WorkerMessageType<'INFO', T>;
type ErrorMessage<E extends Error = Error> = WorkerMessageType<'ERROR', E>;
export type WorkerMessage<T> =
| InitializeMessage
| InitializedMessage
| RunMessage<T>
| ResultMessage<T>
| InfoMessage<T>
| ErrorMessage;
export type WorkerMessageEvent<T> = MessageEvent<WorkerMessage<T>>;
/**
* 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 funcs from '@visualdl/wasm';
import type {InitializeData} from '~/worker';
import {WorkerSelf} from '~/worker';
import initWasm from '@visualdl/wasm';
const workerSelf = new WorkerSelf();
type FuncNames = Exclude<keyof typeof funcs, 'default'>;
async function init(env: Record<string, string>) {
const PUBLIC_PATH = env.SNOWPACK_PUBLIC_PATH;
await initWasm(`${PUBLIC_PATH}/wasm/visualdl.wasm`);
workerSelf.emit('INITIALIZED');
workerSelf.on<{name: FuncNames; params: unknown[]}>('RUN', ({name, params}) => {
try {
// eslint-disable-next-line @typescript-eslint/ban-types
const result = (funcs[name] as Function)(...params);
workerSelf.emit('RESULT', result);
} catch (e) {
if (e.message !== 'unreachable') {
throw e;
}
}
});
}
workerSelf.on<InitializeData>('INITIALIZE', ({env}) => {
init(env);
});
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册