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

3D Labels in High-dimensional page (#947)

* chore: update dependencies

* chore: bump typescript to 4.2

* fix: fix chaos of theme when system theme changed while theme is not set to auto

* feat: 3d label in high-dimensional page

* fix: label index error when data has a large size

* fix: minor bugs fix

* chore: update dependencies
上级 fe2a1063
...@@ -35,11 +35,7 @@ module.exports = { ...@@ -35,11 +35,7 @@ module.exports = {
overrides: [ overrides: [
{ {
files: ['packages/cli/**/*', 'packages/mock/**/*', 'packages/demo/**/*', 'packages/server/**/*'], files: ['packages/cli/**/*', 'packages/mock/**/*', 'packages/demo/**/*', 'packages/server/**/*'],
extends: [ extends: ['plugin:@typescript-eslint/recommended', 'plugin:prettier/recommended'],
'plugin:@typescript-eslint/recommended',
'prettier/@typescript-eslint',
'plugin:prettier/recommended'
],
parser: '@typescript-eslint/parser', parser: '@typescript-eslint/parser',
rules: { rules: {
'@typescript-eslint/explicit-function-return-type': 'off', '@typescript-eslint/explicit-function-return-type': 'off',
...@@ -53,7 +49,6 @@ module.exports = { ...@@ -53,7 +49,6 @@ module.exports = {
'plugin:@typescript-eslint/recommended', 'plugin:@typescript-eslint/recommended',
'plugin:react/recommended', 'plugin:react/recommended',
'plugin:react-hooks/recommended', 'plugin:react-hooks/recommended',
'prettier/@typescript-eslint',
'plugin:prettier/recommended' 'plugin:prettier/recommended'
], ],
parser: '@typescript-eslint/parser', parser: '@typescript-eslint/parser',
......
...@@ -38,19 +38,19 @@ ...@@ -38,19 +38,19 @@
"version": "yarn format && git add -A" "version": "yarn format && git add -A"
}, },
"devDependencies": { "devDependencies": {
"@typescript-eslint/eslint-plugin": "4.14.1", "@typescript-eslint/eslint-plugin": "4.21.0",
"@typescript-eslint/parser": "4.14.1", "@typescript-eslint/parser": "4.21.0",
"eslint": "7.18.0", "eslint": "7.23.0",
"eslint-config-prettier": "7.2.0", "eslint-config-prettier": "8.1.0",
"eslint-plugin-license-header": "0.2.0", "eslint-plugin-license-header": "0.2.0",
"eslint-plugin-prettier": "3.3.1", "eslint-plugin-prettier": "3.3.1",
"eslint-plugin-react": "7.22.0", "eslint-plugin-react": "7.23.1",
"eslint-plugin-react-hooks": "4.2.0", "eslint-plugin-react-hooks": "4.2.0",
"lerna": "3.22.1", "lerna": "4.0.0",
"lint-staged": "10.5.3", "lint-staged": "10.5.4",
"prettier": "2.2.1", "prettier": "2.2.1",
"rimraf": "3.0.2", "rimraf": "3.0.2",
"typescript": "4.0.5", "typescript": "4.2.3",
"yarn": "1.22.10" "yarn": "1.22.10"
}, },
"engines": { "engines": {
......
...@@ -35,17 +35,17 @@ ...@@ -35,17 +35,17 @@
], ],
"dependencies": { "dependencies": {
"@visualdl/server": "2.1.5", "@visualdl/server": "2.1.5",
"open": "7.3.1", "open": "8.0.5",
"ora": "5.3.0", "ora": "5.4.0",
"pm2": "4.5.1", "pm2": "4.5.6",
"yargs": "16.2.0" "yargs": "16.2.0"
}, },
"devDependencies": { "devDependencies": {
"@types/node": "14.14.22", "@types/node": "14.14.37",
"@types/yargs": "15.0.12", "@types/yargs": "16.0.1",
"cross-env": "7.0.3", "cross-env": "7.0.3",
"ts-node": "9.1.1", "ts-node": "9.1.1",
"typescript": "4.0.5" "typescript": "4.2.3"
}, },
"engines": { "engines": {
"node": ">=12", "node": ">=12",
......
...@@ -34,78 +34,79 @@ ...@@ -34,78 +34,79 @@
"builder/environment.js" "builder/environment.js"
], ],
"dependencies": { "dependencies": {
"@tippyjs/react": "4.2.0", "@tippyjs/react": "4.2.5",
"@visualdl/netron": "2.1.5", "@visualdl/netron": "2.1.5",
"@visualdl/wasm": "2.1.5", "@visualdl/wasm": "2.1.5",
"bignumber.js": "9.0.1", "bignumber.js": "9.0.1",
"d3": "6.5.0", "d3": "6.6.2",
"d3-format": "2.0.0", "d3-format": "2.0.0",
"echarts": "4.9.0", "echarts": "4.9.0",
"echarts-gl": "1.1.2", "echarts-gl": "1.1.2",
"eventemitter3": "4.0.7", "eventemitter3": "4.0.7",
"file-saver": "2.0.5", "file-saver": "2.0.5",
"i18next": "19.8.4", "i18next": "20.2.1",
"i18next-browser-languagedetector": "6.0.1", "i18next-browser-languagedetector": "6.1.0",
"i18next-fetch-backend": "3.0.0", "i18next-fetch-backend": "3.0.0",
"jszip": "3.5.0", "jszip": "3.6.0",
"lodash": "4.17.20", "lodash": "4.17.21",
"mime-types": "2.1.28", "mime-types": "2.1.30",
"moment": "2.29.1", "moment": "2.29.1",
"nprogress": "0.2.0", "nprogress": "0.2.0",
"numeric": "1.2.6", "numeric": "1.2.6",
"polished": "4.1.0", "polished": "4.1.1",
"query-string": "6.13.8", "query-string": "7.0.0",
"react": "17.0.1", "react": "17.0.2",
"react-content-loader": "6.0.1", "react-content-loader": "6.0.3",
"react-dom": "17.0.1", "react-dom": "17.0.2",
"react-helmet": "6.1.0", "react-helmet": "6.1.0",
"react-i18next": "11.8.5", "react-i18next": "11.8.12",
"react-input-range": "1.3.0", "react-input-range": "1.3.0",
"react-is": "17.0.1", "react-is": "17.0.2",
"react-rangeslider": "2.2.0", "react-rangeslider": "2.2.0",
"react-redux": "7.2.2", "react-redux": "7.2.3",
"react-router-dom": "5.2.0", "react-router-dom": "5.2.0",
"react-spinners": "0.10.4", "react-spinners": "0.10.6",
"react-toastify": "7.0.1", "react-toastify": "7.0.3",
"redux": "4.0.5", "redux": "4.0.5",
"styled-components": "5.2.1", "styled-components": "5.2.3",
"swr": "0.4.0", "swr": "0.5.5",
"three": "0.125.1", "three": "0.127.0",
"tippy.js": "6.2.7", "tippy.js": "6.3.1",
"umap-js": "1.3.3" "umap-js": "1.3.3"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "7.12.10", "@babel/core": "7.13.14",
"@babel/plugin-proposal-class-properties": "7.12.1", "@babel/plugin-proposal-class-properties": "7.13.0",
"@babel/preset-env": "7.12.11", "@babel/preset-env": "7.13.12",
"@babel/preset-react": "7.12.10", "@babel/preset-react": "7.13.13",
"@baiducloud/sdk": "1.0.0-rc.25", "@baiducloud/sdk": "1.0.0-rc.28",
"@simbathesailor/use-what-changed": "0.1.25", "@simbathesailor/use-what-changed": "2.0.0",
"@snowpack/app-scripts-react": "1.12.6", "@snowpack/app-scripts-react": "1.12.6",
"@snowpack/plugin-dotenv": "2.0.5", "@snowpack/plugin-dotenv": "2.0.5",
"@snowpack/plugin-optimize": "0.2.10", "@snowpack/plugin-optimize": "0.2.10",
"@snowpack/plugin-run-script": "2.3.0", "@snowpack/plugin-run-script": "2.3.0",
"@svgr/core": "5.5.0", "@svgr/core": "5.5.0",
"@testing-library/jest-dom": "5.11.9", "@testing-library/jest-dom": "5.11.10",
"@testing-library/react": "11.2.3", "@testing-library/react": "11.2.6",
"@types/d3": "6.3.0", "@types/d3": "6.3.0",
"@types/d3-format": "2.0.0", "@types/d3-format": "2.0.0",
"@types/echarts": "4.9.3", "@types/echarts": "4.9.7",
"@types/file-saver": "2.0.1", "@types/file-saver": "2.0.1",
"@types/jest": "26.0.20", "@types/jest": "26.0.22",
"@types/loadable__component": "5.13.1", "@types/loadable__component": "5.13.3",
"@types/lodash": "4.14.168", "@types/lodash": "4.14.168",
"@types/mime-types": "2.1.0", "@types/mime-types": "2.1.0",
"@types/nprogress": "0.2.0", "@types/nprogress": "0.2.0",
"@types/numeric": "1.2.1", "@types/numeric": "1.2.1",
"@types/react": "17.0.0", "@types/react": "17.0.3",
"@types/react-dom": "17.0.0", "@types/react-dom": "17.0.3",
"@types/react-helmet": "6.1.0", "@types/react-helmet": "6.1.0",
"@types/react-rangeslider": "2.2.3", "@types/react-rangeslider": "2.2.3",
"@types/react-redux": "7.1.16", "@types/react-redux": "7.1.16",
"@types/react-router-dom": "5.1.7", "@types/react-router-dom": "5.1.7",
"@types/snowpack-env": "2.3.3", "@types/snowpack-env": "2.3.3",
"@types/styled-components": "5.1.7", "@types/styled-components": "5.1.9",
"@types/three": "0.127.0",
"@visualdl/mock": "2.1.5", "@visualdl/mock": "2.1.5",
"babel-plugin-styled-components": "1.12.0", "babel-plugin-styled-components": "1.12.0",
"dotenv": "8.2.0", "dotenv": "8.2.0",
...@@ -113,10 +114,10 @@ ...@@ -113,10 +114,10 @@
"express": "4.17.1", "express": "4.17.1",
"fs-extra": "9.1.0", "fs-extra": "9.1.0",
"html-minifier": "4.0.0", "html-minifier": "4.0.0",
"http-proxy-middleware": "1.0.6", "http-proxy-middleware": "1.1.0",
"jest": "26.6.3", "jest": "26.6.3",
"snowpack": "2.18.5", "snowpack": "2.18.5",
"typescript": "4.0.5", "typescript": "4.2.3",
"yargs": "16.2.0" "yargs": "16.2.0"
}, },
"engines": { "engines": {
......
...@@ -19,7 +19,9 @@ ...@@ -19,7 +19,9 @@
import React, {FunctionComponent, Suspense, useCallback, useEffect, useMemo} from 'react'; import React, {FunctionComponent, Suspense, useCallback, useEffect, useMemo} from 'react';
import {Redirect, Route, BrowserRouter as Router, Switch, useLocation} from 'react-router-dom'; import {Redirect, Route, BrowserRouter as Router, Switch, useLocation} from 'react-router-dom';
import {THEME, matchMedia} from '~/utils/theme'; import {THEME, matchMedia} from '~/utils/theme';
import {actions, selectors} from '~/store';
import {headerHeight, position, size, zIndexes} from '~/utils/style'; import {headerHeight, position, size, zIndexes} from '~/utils/style';
import {useDispatch, useSelector} from 'react-redux';
import ErrorBoundary from '~/components/ErrorBoundary'; import ErrorBoundary from '~/components/ErrorBoundary';
import ErrorPage from '~/pages/error'; import ErrorPage from '~/pages/error';
...@@ -28,11 +30,9 @@ import NProgress from 'nprogress'; ...@@ -28,11 +30,9 @@ import NProgress from 'nprogress';
import Navbar from '~/components/Navbar'; import Navbar from '~/components/Navbar';
import {SWRConfig} from 'swr'; import {SWRConfig} from 'swr';
import {ToastContainer} from 'react-toastify'; import {ToastContainer} from 'react-toastify';
import {actions} from '~/store';
import {fetcher} from '~/utils/fetch'; import {fetcher} from '~/utils/fetch';
import routes from '~/routes'; import routes from '~/routes';
import styled from 'styled-components'; import styled from 'styled-components';
import {useDispatch} from 'react-redux';
import {useTranslation} from 'react-i18next'; import {useTranslation} from 'react-i18next';
const BASE_URI: string = import.meta.env.SNOWPACK_PUBLIC_BASE_URI; const BASE_URI: string = import.meta.env.SNOWPACK_PUBLIC_BASE_URI;
...@@ -83,10 +83,15 @@ const App: FunctionComponent = () => { ...@@ -83,10 +83,15 @@ const App: FunctionComponent = () => {
const dir = useMemo(() => (i18n.language ? i18n.dir(i18n.language) : ''), [i18n]); const dir = useMemo(() => (i18n.language ? i18n.dir(i18n.language) : ''), [i18n]);
const dispatch = useDispatch(); const dispatch = useDispatch();
const selectedTheme = useSelector(selectors.theme.selected);
const toggleTheme = useCallback( const toggleTheme = useCallback(
(e: MediaQueryListEvent) => dispatch(actions.theme.setTheme(e.matches ? 'dark' : 'light')), (e: MediaQueryListEvent) => {
[dispatch] if (selectedTheme === 'auto') {
dispatch(actions.theme.setTheme(e.matches ? 'dark' : 'light'));
}
},
[dispatch, selectedTheme]
); );
useEffect(() => { useEffect(() => {
......
...@@ -21,6 +21,7 @@ import Field from '~/components/Field'; ...@@ -21,6 +21,7 @@ import Field from '~/components/Field';
import RangeSlider from '~/components/RangeSlider'; import RangeSlider from '~/components/RangeSlider';
import type {Run} from '~/resource/curves'; import type {Run} from '~/resource/curves';
import {TimeType} from '~/resource/curves'; import {TimeType} from '~/resource/curves';
import type {UseTranslationResponse} from 'react-i18next';
import {format} from 'd3-format'; import {format} from 'd3-format';
import {formatTime} from '~/utils'; import {formatTime} from '~/utils';
import styled from 'styled-components'; import styled from 'styled-components';
...@@ -65,11 +66,12 @@ const typeMap = { ...@@ -65,11 +66,12 @@ const typeMap = {
[TimeType.Step]: 'steps' [TimeType.Step]: 'steps'
} as const; } as const;
// TODO: react-i18next add a workaround to suppress an error in ts4.1, but it causes our types broken...
const formatter = { const formatter = {
[TimeType.WallTime]: (wallTime: number, {i18n}: ReturnType<typeof useTranslation>) => [TimeType.WallTime]: (wallTime: number, {i18n}: UseTranslationResponse<'common'>) =>
formatTime(wallTime, i18n.language), formatTime(wallTime, i18n.language),
[TimeType.Relative]: (relative: number) => `${relativeFormatter(relative)} ms`, [TimeType.Relative]: (relative: number) => `${relativeFormatter(relative)} ms`,
[TimeType.Step]: (step: number, {t}: ReturnType<typeof useTranslation>) => `${t('common:time-mode.step')} ${step}` [TimeType.Step]: (step: number, {t}: UseTranslationResponse<'common'>) => `${t('common:time-mode.step')} ${step}`
} as const; } as const;
type StepSliderProps = { type StepSliderProps = {
......
...@@ -69,10 +69,11 @@ const Operations = styled.div` ...@@ -69,10 +69,11 @@ const Operations = styled.div`
`; `;
type ChartOperationsProps = { type ChartOperationsProps = {
onToggleLabelCloud?: () => unknown;
onReset?: () => unknown; onReset?: () => unknown;
}; };
const ChartOperations: FunctionComponent<ChartOperationsProps> = ({onReset}) => { const ChartOperations: FunctionComponent<ChartOperationsProps> = ({onToggleLabelCloud, onReset}) => {
const {t} = useTranslation('high-dimensional'); const {t} = useTranslation('high-dimensional');
return ( return (
...@@ -83,14 +84,14 @@ const ChartOperations: FunctionComponent<ChartOperationsProps> = ({onReset}) => ...@@ -83,14 +84,14 @@ const ChartOperations: FunctionComponent<ChartOperationsProps> = ({onReset}) =>
<Icon type="selection" /> <Icon type="selection" />
</span> </span>
</a> </a>
</Tippy> </Tippy> */}
<Tippy content={t('high-dimensional:3d-label')} placement="bottom" theme="tooltip"> <Tippy content={t('high-dimensional:3d-label')} placement="bottom" theme="tooltip">
<a className="three-d"> <a className="three-d" onClick={() => onToggleLabelCloud?.()}>
<span> <span>
<Icon type="three-d" /> <Icon type="three-d" />
</span> </span>
</a> </a>
</Tippy> */} </Tippy>
<Tippy content={t('high-dimensional:reset-zoom')} placement="bottom" theme="tooltip"> <Tippy content={t('high-dimensional:reset-zoom')} placement="bottom" theme="tooltip">
<a onClick={() => onReset?.()}> <a onClick={() => onReset?.()}>
<span> <span>
......
...@@ -16,10 +16,11 @@ ...@@ -16,10 +16,11 @@
import type {CalculateParams, CalculateResult, Reduction, Shape} from '~/resource/high-dimensional'; import type {CalculateParams, CalculateResult, Reduction, Shape} from '~/resource/high-dimensional';
import React, {useCallback, useEffect, useImperativeHandle, useLayoutEffect, useMemo, useRef, useState} from 'react'; import React, {useCallback, useEffect, useImperativeHandle, useLayoutEffect, useMemo, useRef, useState} from 'react';
import ScatterChart, {ScatterChartRef} from '~/components/ScatterChart'; import type {ScatterChartProps, ScatterChartRef} from '~/components/ScatterChart';
import ChartOperations from '~/components/HighDimensionalPage/ChartOperations'; import ChartOperations from '~/components/HighDimensionalPage/ChartOperations';
import type {InfoData} from '~/worker/high-dimensional/calculate'; import type {InfoData} from '~/worker/high-dimensional/calculate';
import ScatterChart from '~/components/ScatterChart';
import type {WithStyled} from '~/utils/style'; import type {WithStyled} from '~/utils/style';
import {rem} from '~/utils/style'; import {rem} from '~/utils/style';
import styled from 'styled-components'; import styled from 'styled-components';
...@@ -116,6 +117,10 @@ const HighDimensionalChart = React.forwardRef<HighDimensionalChartRef, HighDimen ...@@ -116,6 +117,10 @@ const HighDimensionalChart = React.forwardRef<HighDimensionalChartRef, HighDimen
const [width, setWidth] = useState(0); const [width, setWidth] = useState(0);
const [height, setHeight] = useState(0); const [height, setHeight] = useState(0);
const [showLabelCloud, setShowLabelCloud] = useState(false);
const chartType = useMemo<ScatterChartProps['type']>(() => (showLabelCloud ? 'labels' : 'points'), [
showLabelCloud
]);
useLayoutEffect(() => { useLayoutEffect(() => {
const c = chartElement.current; const c = chartElement.current;
...@@ -259,7 +264,10 @@ const HighDimensionalChart = React.forwardRef<HighDimensionalChartRef, HighDimen ...@@ -259,7 +264,10 @@ const HighDimensionalChart = React.forwardRef<HighDimensionalChartRef, HighDimen
{t('common:colon')} {t('common:colon')}
{shape[1]} {shape[1]}
</div> </div>
<ChartOperations onReset={() => chart.current?.reset()} /> <ChartOperations
onToggleLabelCloud={() => setShowLabelCloud(s => !s)}
onReset={() => chart.current?.reset()}
/>
</Toolbar> </Toolbar>
<Chart ref={chartElement}> <Chart ref={chartElement}>
<ScatterChart <ScatterChart
...@@ -272,6 +280,7 @@ const HighDimensionalChart = React.forwardRef<HighDimensionalChartRef, HighDimen ...@@ -272,6 +280,7 @@ const HighDimensionalChart = React.forwardRef<HighDimensionalChartRef, HighDimen
rotate={reduction !== 'tsne'} rotate={reduction !== 'tsne'}
focusedIndices={focusedIndices} focusedIndices={focusedIndices}
highlightIndices={highlightIndices} highlightIndices={highlightIndices}
type={chartType}
/> />
</Chart> </Chart>
</Wrapper> </Wrapper>
......
...@@ -29,7 +29,6 @@ import {Chart as ChartLoader} from '~/components/Loader/ChartPage'; ...@@ -29,7 +29,6 @@ import {Chart as ChartLoader} from '~/components/Loader/ChartPage';
import ChartToolbox from '~/components/ChartToolbox'; import ChartToolbox from '~/components/ChartToolbox';
import type {Run} from '~/types'; import type {Run} from '~/types';
import {distance} from '~/utils'; import {distance} from '~/utils';
import {fetcher} from '~/utils/fetch';
import {format} from 'd3-format'; import {format} from 'd3-format';
import minBy from 'lodash/minBy'; import minBy from 'lodash/minBy';
import queryString from 'query-string'; import queryString from 'query-string';
...@@ -95,7 +94,6 @@ const HistogramChart: FunctionComponent<HistogramChartProps> = ({run, tag, mode, ...@@ -95,7 +94,6 @@ const HistogramChart: FunctionComponent<HistogramChartProps> = ({run, tag, mode,
const {data: dataset, error, loading} = useRunningRequest<HistogramData>( const {data: dataset, error, loading} = useRunningRequest<HistogramData>(
`/histogram/list?${queryString.stringify({run: run.label, tag})}`, `/histogram/list?${queryString.stringify({run: run.label, tag})}`,
!!running, !!running,
fetcher,
{ {
refreshInterval: 60 * 1000 refreshInterval: 60 * 1000
} }
......
...@@ -23,7 +23,6 @@ import ChartToolbox from '~/components/ChartToolbox'; ...@@ -23,7 +23,6 @@ import ChartToolbox from '~/components/ChartToolbox';
import GridLoader from 'react-spinners/GridLoader'; import GridLoader from 'react-spinners/GridLoader';
import type {Run} from '~/types'; import type {Run} from '~/types';
import StepSlider from '~/components/SamplePage/StepSlider'; import StepSlider from '~/components/SamplePage/StepSlider';
import {fetcher} from '~/utils/fetch';
import {formatTime} from '~/utils'; import {formatTime} from '~/utils';
import isEmpty from 'lodash/isEmpty'; import isEmpty from 'lodash/isEmpty';
import mime from 'mime-types'; import mime from 'mime-types';
...@@ -256,13 +255,9 @@ const SampleChart: FunctionComponent<SampleChartProps> = ({ ...@@ -256,13 +255,9 @@ const SampleChart: FunctionComponent<SampleChartProps> = ({
} }
}, []); }, []);
const {data: entityData, error: entityError, loading: entityLoading} = useRequest<BlobResponse>( const {data: entityData, error: entityError, loading: entityLoading} = useRequest<BlobResponse>(src ?? null, {
src ?? null, dedupingInterval: 5 * 60 * 1000
fetcher, });
{
dedupingInterval: 5 * 60 * 1000
}
);
const download = useCallback(() => { const download = useCallback(() => {
if (entityData) { if (entityData) {
......
...@@ -22,7 +22,6 @@ import useRequest, {useRunningRequest} from '~/hooks/useRequest'; ...@@ -22,7 +22,6 @@ import useRequest, {useRunningRequest} from '~/hooks/useRequest';
import ContentLoader from '~/components/Loader/ContentLoader'; import ContentLoader from '~/components/Loader/ContentLoader';
import Icon from '~/components/Icon'; import Icon from '~/components/Icon';
import {TextChart as TextChartLoader} from '~/components/Loader/ChartPage'; import {TextChart as TextChartLoader} from '~/components/Loader/ChartPage';
import {fetcher} from '~/utils/fetch';
import {getEntityUrl} from './SampleChart'; import {getEntityUrl} from './SampleChart';
import queryString from 'query-string'; import queryString from 'query-string';
import styled from 'styled-components'; import styled from 'styled-components';
...@@ -163,7 +162,7 @@ type TextProps = { ...@@ -163,7 +162,7 @@ type TextProps = {
const Text: FunctionComponent<TextProps> = ({run, tag, step, wallTime, index}) => { const Text: FunctionComponent<TextProps> = ({run, tag, step, wallTime, index}) => {
const {t} = useTranslation('sample'); const {t} = useTranslation('sample');
const {data: text, error, loading} = useRequest<string>(getEntityUrl('text', index, run, tag, wallTime), fetcher, { const {data: text, error, loading} = useRequest<string>(getEntityUrl('text', index, run, tag, wallTime), {
dedupingInterval: 5 * 60 * 1000 dedupingInterval: 5 * 60 * 1000
}); });
......
...@@ -14,10 +14,13 @@ ...@@ -14,10 +14,13 @@
* limitations under the License. * limitations under the License.
*/ */
import Chart, {ScatterChartOptions as ChartOptions} from './ScatterChart';
import React, {useEffect, useImperativeHandle, useRef} from 'react'; import React, {useEffect, useImperativeHandle, useRef} from 'react';
import type Chart from './ScatterChart';
import type {ScatterChartOptions as ChartOptions} from './ScatterChart';
import LabelChart from './Labels';
import type {Point3D} from './types'; import type {Point3D} from './types';
import PointChart from './Points';
import type {WithStyled} from '~/utils/style'; import type {WithStyled} from '~/utils/style';
import styled from 'styled-components'; import styled from 'styled-components';
import {themes} from '~/utils/theme'; import {themes} from '~/utils/theme';
...@@ -38,6 +41,7 @@ export type ScatterChartProps = { ...@@ -38,6 +41,7 @@ export type ScatterChartProps = {
rotate?: boolean; rotate?: boolean;
focusedIndices?: number[]; focusedIndices?: number[];
highlightIndices?: number[]; highlightIndices?: number[];
type: 'points' | 'labels';
}; };
export type ScatterChartRef = { export type ScatterChartRef = {
...@@ -45,7 +49,7 @@ export type ScatterChartRef = { ...@@ -45,7 +49,7 @@ export type ScatterChartRef = {
}; };
const ScatterChart = React.forwardRef<ScatterChartRef, ScatterChartProps & WithStyled>( const ScatterChart = React.forwardRef<ScatterChartRef, ScatterChartProps & WithStyled>(
({width, height, data, labels, is3D, rotate, focusedIndices, highlightIndices, className}, ref) => { ({width, height, data, labels, is3D, rotate, focusedIndices, highlightIndices, type, className}, ref) => {
const theme = useTheme(); const theme = useTheme();
const element = useRef<HTMLDivElement>(null); const element = useRef<HTMLDivElement>(null);
...@@ -54,14 +58,21 @@ const ScatterChart = React.forwardRef<ScatterChartRef, ScatterChartProps & WithS ...@@ -54,14 +58,21 @@ const ScatterChart = React.forwardRef<ScatterChartRef, ScatterChartProps & WithS
useEffect(() => { useEffect(() => {
if (element.current) { if (element.current) {
chart.current = new Chart(element.current, options.current); if (type === 'points') {
chart.current = new PointChart(element.current, options.current);
} else if (type === 'labels') {
chart.current = new LabelChart(element.current, options.current);
} else {
chart.current = null;
}
return () => { return () => {
chart.current?.dispose(); chart.current?.dispose();
}; };
} }
}, []); }, [type]);
useEffect(() => { useEffect(() => {
options.current.is3D = is3D;
chart.current?.setDimension(is3D); chart.current?.setDimension(is3D);
if (is3D) { if (is3D) {
if (rotate) { if (rotate) {
...@@ -73,21 +84,22 @@ const ScatterChart = React.forwardRef<ScatterChartRef, ScatterChartProps & WithS ...@@ -73,21 +84,22 @@ const ScatterChart = React.forwardRef<ScatterChartRef, ScatterChartProps & WithS
}, [is3D, rotate]); }, [is3D, rotate]);
useEffect(() => { useEffect(() => {
chart.current?.setData(data); chart.current?.setData(data, labels);
chart.current?.setLabels(labels); }, [data, labels, type]);
}, [data, labels]);
useEffect(() => { useEffect(() => {
chart.current?.setFocusedPointIndices(focusedIndices ?? []); chart.current?.setFocusedPointIndices(focusedIndices ?? []);
}, [focusedIndices]); }, [focusedIndices, type]);
useEffect(() => { useEffect(() => {
chart.current?.setHighLightIndices(highlightIndices ?? []); chart.current?.setHighLightIndices(highlightIndices ?? []);
}, [highlightIndices]); }, [highlightIndices, type]);
useEffect(() => { useEffect(() => {
options.current.width = width;
options.current.height = height;
chart.current?.setSize(width, height); chart.current?.setSize(width, height);
}, [width, height]); }, [width, height, type]);
useImperativeHandle(ref, () => ({ useImperativeHandle(ref, () => ({
reset: () => { reset: () => {
......
/**
* 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 mipmaps
import * as THREE from 'three';
import type {Point2D} from '../types';
import ScatterChart from '../ScatterChart';
import fragmentShader from './fragment.glsl';
import vertexShader from './vertex.glsl';
const CANVAS_MAX_WIDTH = 16384;
const CANVAS_MAX_HEIGHT = 16384;
const VERTEX_COUNT_PER_LABEL = 6;
type Position2D = [Point2D, Point2D];
export default class LabelScatterChart extends ScatterChart {
static readonly LABEL_COLOR = new THREE.Color(0x000000);
static readonly LABEL_BACKGROUND_COLOR_DEFAULT = new THREE.Color(0xffffff);
static readonly LABEL_BACKGROUND_COLOR_HOVER = new THREE.Color(0x2932e1);
static readonly LABEL_BACKGROUND_COLOR_HIGHLIGHT = new THREE.Color(0x2932e1);
static readonly LABEL_BACKGROUND_COLOR_FOCUS = new THREE.Color(0x2932e1);
static readonly LABEL_FONT = 'roboto';
static readonly LABEL_FONT_SIZE = 20;
static readonly LABEL_PADDING = [3, 5] as const;
protected blending: THREE.Blending = THREE.NormalBlending;
protected vertexShader: string = vertexShader;
protected fragmentShader: string = fragmentShader;
private glyphTexture: THREE.CanvasTexture | null = null;
private mesh: THREE.Mesh | null = null;
private textWidthInGlyphTexture: number[] = [];
private textPositionInGlyphTexture: Position2D[] = [];
get object() {
return this.mesh;
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
protected createShaderUniforms(picking: boolean): Record<string, THREE.IUniform<any>> {
return {
glyphTexture: {value: this.glyphTexture},
picking: {value: picking}
};
}
private convertVertexes() {
const count = this.dataCount;
const vertexes = new Float32Array(count * VERTEX_COUNT_PER_LABEL * 3);
for (let i = 0; i < count; i++) {
for (let j = 0; j < VERTEX_COUNT_PER_LABEL; j++) {
for (let k = 0; k < 3; k++) {
vertexes[i * VERTEX_COUNT_PER_LABEL * 3 + j * 3 + k] = this.positions[i * 3 + k];
}
}
}
return vertexes;
}
private convertPickingColors() {
if (this.pickingColors) {
const count = this.dataCount;
const colors = new Float32Array(count * VERTEX_COUNT_PER_LABEL * 3);
for (let i = 0; i < count; i++) {
for (let j = 0; j < VERTEX_COUNT_PER_LABEL; j++) {
for (let k = 0; k < 3; k++) {
colors[i * VERTEX_COUNT_PER_LABEL * 3 + j * 3 + k] = this.pickingColors[i * 3 + k];
}
}
}
return colors;
}
return null;
}
private convertPositionsToLabelPosition() {
const count = this.dataCount;
const vertexes = new Float32Array(count * VERTEX_COUNT_PER_LABEL * 2);
const scaleFactor = 1 / (700 * LabelScatterChart.CUBE_LENGTH);
const height = (LabelScatterChart.LABEL_FONT_SIZE + 2 * LabelScatterChart.LABEL_PADDING[0]) * scaleFactor;
for (let i = 0; i < count; i++) {
const vi = i * VERTEX_COUNT_PER_LABEL * 2;
const width = this.textWidthInGlyphTexture[i] * scaleFactor;
const x1 = -width;
const y1 = -height;
const x2 = width;
const y2 = height;
vertexes[vi] = x1;
vertexes[vi + 1] = y1;
vertexes[vi + 2] = x2;
vertexes[vi + 3] = y1;
vertexes[vi + 4] = x1;
vertexes[vi + 5] = y2;
vertexes[vi + 6] = x1;
vertexes[vi + 7] = y2;
vertexes[vi + 8] = x2;
vertexes[vi + 9] = y1;
vertexes[vi + 10] = x2;
vertexes[vi + 11] = y2;
}
return vertexes;
}
private convertGlyphTexturePositionsToUV() {
const count = this.dataCount;
const uv = new Float32Array(count * VERTEX_COUNT_PER_LABEL * 2);
for (let i = 0; i < count; i++) {
const vi = i * VERTEX_COUNT_PER_LABEL * 2;
let x1 = 0;
let y1 = 0;
let x2 = 1;
let y2 = 1;
if (this.textPositionInGlyphTexture[i]) {
const [topLeft, bottomRight] = this.textPositionInGlyphTexture[i];
x1 = topLeft[0];
y1 = 1 - topLeft[1];
x2 = bottomRight[0];
y2 = 1 - bottomRight[1];
}
uv[vi] = x1;
uv[vi + 1] = y2;
uv[vi + 2] = x2;
uv[vi + 3] = y2;
uv[vi + 4] = x1;
uv[vi + 5] = y1;
uv[vi + 6] = x1;
uv[vi + 7] = y1;
uv[vi + 8] = x2;
uv[vi + 9] = y2;
uv[vi + 10] = x2;
uv[vi + 11] = y1;
}
return uv;
}
private convertVertexColors() {
const count = this.dataCount;
const colors = new Float32Array(count * VERTEX_COUNT_PER_LABEL * 3);
for (let i = 0; i < count; i++) {
let color: THREE.Color;
if (this.hoveredDataIndices.includes(i)) {
color = LabelScatterChart.LABEL_BACKGROUND_COLOR_HOVER;
} else if (this.focusedDataIndices.includes(i)) {
color = LabelScatterChart.LABEL_BACKGROUND_COLOR_FOCUS;
} else if (this.highLightDataIndices.includes(i)) {
color = LabelScatterChart.LABEL_BACKGROUND_COLOR_HIGHLIGHT;
} else {
color = LabelScatterChart.LABEL_BACKGROUND_COLOR_DEFAULT;
}
for (let j = 0; j < VERTEX_COUNT_PER_LABEL; j++) {
colors[i * VERTEX_COUNT_PER_LABEL * 3 + j * 3] = color.r;
colors[i * VERTEX_COUNT_PER_LABEL * 3 + j * 3 + 1] = color.g;
colors[i * VERTEX_COUNT_PER_LABEL * 3 + j * 3 + 2] = color.b;
}
}
return colors;
}
private createGlyphTexture() {
const dpr = window.devicePixelRatio;
const labelCount = this.labels.length;
const fontSize = LabelScatterChart.LABEL_FONT_SIZE * dpr;
const font = `bold ${fontSize}px roboto`;
const [vPadding, hPadding] = LabelScatterChart.LABEL_PADDING;
const canvas = document.createElement('canvas');
canvas.width = CANVAS_MAX_WIDTH;
canvas.height = fontSize;
const ctx = canvas.getContext('2d');
let canvasWidth = 0;
let canvasHeight = fontSize + 2 * vPadding;
const positions: Position2D[] = [];
const textWidths: number[] = [];
if (ctx) {
ctx.font = font;
ctx.fillStyle = LabelScatterChart.LABEL_COLOR.getStyle();
ctx.textAlign = 'start';
ctx.textBaseline = 'top';
let x = hPadding;
let y = vPadding;
for (let i = 0; i < labelCount; i++) {
const label = this.labels[i];
const index = this.labels.indexOf(label);
if (index >= 0 && i !== index) {
textWidths.push(textWidths[index]);
// deep copy position
positions.push([
[positions[index][0][0], positions[index][0][1]],
[positions[index][1][0], positions[index][1][1]]
]);
continue;
}
const textWidth = Math.ceil(ctx.measureText(label).width);
textWidths.push(Math.floor(textWidth / dpr) + 2 * hPadding);
const deltaX = textWidth + hPadding;
const deltaY = fontSize + vPadding;
if (x + deltaX > CANVAS_MAX_WIDTH) {
x = hPadding;
y += deltaY;
if (y > CANVAS_MAX_HEIGHT) {
throw new Error('Texture too large!');
}
canvasHeight = y + deltaY;
}
positions.push([
[x - hPadding, y - vPadding],
[x + deltaX, y + deltaY]
]);
x += deltaX;
if (canvasWidth < x) {
canvasWidth = x;
}
}
canvas.width = canvasWidth;
canvas.height = canvasHeight;
ctx.fillStyle = '#fff';
ctx.fillRect(0, 0, canvasWidth, canvasHeight);
ctx.font = font;
ctx.fillStyle = LabelScatterChart.LABEL_COLOR.getStyle();
ctx.textAlign = 'start';
ctx.textBaseline = 'top';
for (let i = 0; i < labelCount; i++) {
const label = this.labels[i];
const index = this.labels.indexOf(label);
if (index >= 0 && i !== index) {
continue;
}
const [position] = positions[i];
ctx.fillText(label, position[0] + hPadding, position[1] + vPadding);
}
positions.forEach(position => {
position[0][0] /= canvasWidth;
position[0][1] /= canvasHeight;
position[1][0] /= canvasWidth;
position[1][1] /= canvasHeight;
});
}
const texture = new THREE.CanvasTexture(canvas);
texture.needsUpdate = true;
texture.minFilter = THREE.LinearFilter;
texture.generateMipmaps = false;
texture.flipY = true;
this.glyphTexture = texture;
this.textWidthInGlyphTexture = textWidths;
this.textPositionInGlyphTexture = positions;
}
private setLabelPosition(position: Float32Array) {
this.setGeometryAttribute('labelPosition', position, 2);
}
private setTextureUV(uv: Float32Array) {
this.setGeometryAttribute('uv', uv, 2);
}
protected onRender() {
this.colors = this.convertVertexColors();
}
protected onSetSize() {
// nothing to do
}
protected onDataSet() {
this.createGlyphTexture();
const uv = this.convertGlyphTexturePositionsToUV();
this.setTextureUV(uv);
const labelPosition = this.convertPositionsToLabelPosition();
this.setLabelPosition(labelPosition);
const vertexes = this.convertVertexes();
this.setPosition(vertexes);
this.pickingColors = this.convertPickingColors();
this.createMaterial();
if (this.material) {
this.mesh = new THREE.Mesh(this.geometry, this.material);
this.mesh.frustumCulled = false;
}
}
protected onDispose() {
this.glyphTexture?.dispose();
}
}
/**
* 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 default /* glsl */ `
uniform sampler2D glyphTexture;
uniform bool picking;
varying vec2 vUv;
varying vec3 vColor;
void main() {
if (picking) {
gl_FragColor = vec4(vColor, 1.0);
} else {
vec4 fromTexture = texture(glyphTexture, vUv);
gl_FragColor = vec4(vColor, 1.0) * fromTexture;
}
}
`;
/**
* 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 LabelScatter from './Labels';
export default LabelScatter;
/**
* 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 {ShaderChunk} from 'three';
export default /* glsl */ `
attribute vec2 labelPosition;
attribute vec3 color;
varying vec2 vUv;
varying vec3 vColor;
${ShaderChunk['common']}
void main() {
vUv = uv;
vColor = color;
vec4 vRight = vec4(modelViewMatrix[0][0], modelViewMatrix[1][0], modelViewMatrix[2][0], 0);
vec4 vUp = vec4(modelViewMatrix[0][1], modelViewMatrix[1][1], modelViewMatrix[2][1], 0);
vec4 vAt = -vec4(modelViewMatrix[0][2], modelViewMatrix[1][2], modelViewMatrix[2][2], 0);
mat4 pointToCamera = mat4(vRight, vUp, vAt, vec4(0, 0, 0, 1));
vec4 posRotated = pointToCamera * vec4(labelPosition, 0, 1);
vec4 mvPosition = modelViewMatrix * (vec4(position, 0) + posRotated);
gl_Position = projectionMatrix * mvPosition;
}
`;
/**
* 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 THREE from 'three';
import type {Point2D} from '../types';
import ScatterChart from '../ScatterChart';
import ScatterChartLabel from '../ScatterChartLabel';
import type {ScatterChartOptions} from '../ScatterChart';
import fragmentShader from './fragment.glsl';
import vertexShader from './vertex.glsl';
export default class PointScatter extends ScatterChart {
static readonly NUM_POINTS_FOG_THRESHOLD = 5000;
static readonly POINT_COLOR_DEFAULT = new THREE.Color(0x7e7e7e);
static readonly POINT_COLOR_HOVER = new THREE.Color(0x2932e1);
static readonly POINT_COLOR_HIGHLIGHT = new THREE.Color(0x2932e1);
static readonly POINT_COLOR_FOCUS = new THREE.Color(0x2932e1);
static readonly POINT_SCALE_DEFAULT = 1.0;
static readonly POINT_SCALE_HOVER = 1.2;
static readonly POINT_SCALE_HIGHLIGHT = 1.0;
static readonly POINT_SCALE_FOCUS = 1.2;
protected blending: THREE.Blending = THREE.MultiplyBlending;
protected depth = false;
protected vertexShader: string = vertexShader;
protected fragmentShader: string = fragmentShader;
private scaleFactors: Float32Array | null = null;
private points: THREE.Points | null = null;
private label: ScatterChartLabel;
get object() {
return this.points;
}
constructor(container: HTMLElement, options: ScatterChartOptions) {
super(container, options);
this.label = new ScatterChartLabel(this.container, {
width: this.width,
height: this.height
});
this.fog = this.initFog();
}
private initFog() {
return new THREE.Fog(this.background);
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
protected createShaderUniforms(): Record<string, THREE.IUniform<any>> {
const fog = this.scene.fog as THREE.Fog | null;
return {
pointSize: {value: 200 / Math.log(this.dataCount) / Math.log(8) / (this.is3D ? 1 : 1.5)},
sizeAttenuation: {value: this.is3D},
fogColor: {value: fog?.color},
fogNear: {value: fog?.near},
fogFar: {value: fog?.far}
};
}
private setPointsScaleFactor(scaleFactors: Float32Array) {
this.setGeometryAttribute('scaleFactor', scaleFactors, 1);
}
private convertPointsColor() {
const count = this.dataCount;
const colors = new Float32Array(count * 3);
let dst = 0;
for (let i = 0; i < count; i++) {
if (this.hoveredDataIndices.includes(i)) {
colors[dst++] = PointScatter.POINT_COLOR_HOVER.r;
colors[dst++] = PointScatter.POINT_COLOR_HOVER.g;
colors[dst++] = PointScatter.POINT_COLOR_HOVER.b;
} else if (this.focusedDataIndices.includes(i)) {
colors[dst++] = PointScatter.POINT_COLOR_FOCUS.r;
colors[dst++] = PointScatter.POINT_COLOR_FOCUS.g;
colors[dst++] = PointScatter.POINT_COLOR_FOCUS.b;
} else if (this.highLightDataIndices.includes(i)) {
colors[dst++] = PointScatter.POINT_COLOR_HIGHLIGHT.r;
colors[dst++] = PointScatter.POINT_COLOR_HIGHLIGHT.g;
colors[dst++] = PointScatter.POINT_COLOR_HIGHLIGHT.b;
} else {
colors[dst++] = PointScatter.POINT_COLOR_DEFAULT.r;
colors[dst++] = PointScatter.POINT_COLOR_DEFAULT.g;
colors[dst++] = PointScatter.POINT_COLOR_DEFAULT.b;
}
}
return colors;
}
private convertPointsScaleFactor() {
const count = this.dataCount;
const scaleFactor = new Float32Array(count);
for (let i = 0; i < count; i++) {
if (this.hoveredDataIndices.includes(i)) {
scaleFactor[i] = PointScatter.POINT_SCALE_HOVER;
} else if (this.focusedDataIndices.includes(i)) {
scaleFactor[i] = PointScatter.POINT_SCALE_FOCUS;
} else if (this.highLightDataIndices.includes(i)) {
scaleFactor[i] = PointScatter.POINT_SCALE_HIGHLIGHT;
} else {
scaleFactor[i] = PointScatter.POINT_SCALE_DEFAULT;
}
}
return scaleFactor;
}
private updateHoveredLabels() {
if (!this.camera || !this.positions.length) {
return;
}
const indices = this.focusedDataIndices.length ? this.focusedDataIndices : this.hoveredDataIndices;
if (!indices.length) {
this.label.clear();
return;
}
const dpr = window.devicePixelRatio || 1;
const w = this.width;
const h = this.height;
const labels = indices.map(index => {
const pi = index * 3;
const point = new THREE.Vector3(this.positions[pi], this.positions[pi + 1], this.positions[pi + 2]);
const pv = new THREE.Vector3().copy(point).project(this.camera);
const coordinate: Point2D = [((pv.x + 1) / 2) * w * dpr, -(((pv.y - 1) / 2) * h) * dpr];
return {
text: this.labels[index] ?? '',
fontSize: 40,
fillColor: '#000',
strokeColor: '#fff',
opacity: 1,
x: coordinate[0] + 4,
y: coordinate[1]
};
});
this.label.render(labels);
}
private updateFog() {
const fog = this.fog;
if (fog) {
fog.color = new THREE.Color(this.background);
if (this.is3D && this.positions.length) {
const cameraPos = this.camera.position;
const cameraTarget = this.controls.target;
let shortestDist = Number.POSITIVE_INFINITY;
let furthestDist = 0;
const camToTarget = new THREE.Vector3().copy(cameraTarget).sub(cameraPos);
const camPlaneNormal = new THREE.Vector3().copy(camToTarget).normalize();
const n = this.positions.length / 3;
let src = 0;
const p = new THREE.Vector3();
const camToPoint = new THREE.Vector3();
for (let i = 0; i < n; i++) {
p.x = this.positions[src++];
p.y = this.positions[src++];
p.z = this.positions[src++];
camToPoint.copy(p).sub(cameraPos);
const dist = camPlaneNormal.dot(camToPoint);
if (dist < 0) {
continue;
}
furthestDist = dist > furthestDist ? dist : furthestDist;
shortestDist = dist < shortestDist ? dist : shortestDist;
}
const multiplier =
2 - Math.min(n, PointScatter.NUM_POINTS_FOG_THRESHOLD) / PointScatter.NUM_POINTS_FOG_THRESHOLD;
fog.near = shortestDist;
fog.far = furthestDist * multiplier;
} else {
fog.near = Number.POSITIVE_INFINITY;
fog.far = Number.POSITIVE_INFINITY;
}
if (this.points) {
const material = this.points.material as THREE.ShaderMaterial;
material.uniforms.fogColor.value = fog.color;
material.uniforms.fogNear.value = fog.near;
material.uniforms.fogFar.value = fog.far;
}
this.scene.fog = fog;
}
}
protected onRender() {
this.colors = this.convertPointsColor();
this.updateFog();
this.updateHoveredLabels();
this.scaleFactors = this.convertPointsScaleFactor();
this.setPointsScaleFactor(this.scaleFactors);
}
protected onSetSize(width: number, height: number) {
this.label.setSize(width, height);
}
protected onDataSet() {
this.setPosition(this.positions);
this.createMaterial();
if (this.material) {
this.points = new THREE.Points(this.geometry, this.material);
this.points.frustumCulled = false;
}
}
protected onDispose() {
this.label.dispose();
}
}
/**
* 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 coord
import {ShaderChunk} from 'three';
export default /* glsl */ `
varying vec3 vColor;
${ShaderChunk['common']}
${ShaderChunk['fog_pars_fragment']}
void main() {
float r = distance(gl_PointCoord, vec2(0.5, 0.5));
if (r < 0.5) {
gl_FragColor = vec4(vColor, 1.0);
} else {
discard;
}
${ShaderChunk['fog_fragment']}
}
`;
/**
* 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 PointScatter from './Points';
export default PointScatter;
/**
* 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 {ShaderChunk} from 'three';
const MIN_POINT_SIZE = 5;
export default /* glsl */ `
attribute vec3 color;
attribute float scaleFactor;
uniform bool sizeAttenuation;
uniform float pointSize;
varying vec3 vColor;
${ShaderChunk['fog_pars_vertex']}
void main() {
vColor = color;
vec4 mvPosition = modelViewMatrix * vec4(position, 1.0);
gl_Position = projectionMatrix * mvPosition;
float outputPointSize = pointSize;
if (sizeAttenuation) {
outputPointSize = -pointSize / mvPosition.z;
} else {
const float PI = 3.1415926535897932384626433832795;
const float minScale = 0.1; // minimum scaling factor
const float outSpeed = 2.0; // shrink speed when zooming out
const float outNorm = (1.0 - minScale) / atan(outSpeed);
const float maxScale = 15.0; // maximum scaling factor
const float inSpeed = 0.02; // enlarge speed when zooming in
const float zoomOffset = 0.3; // offset zoom pivot
float zoom = projectionMatrix[0][0] + zoomOffset; // zoom pivot
float scale = zoom < 1.0 ? 1.0 + outNorm * atan(outSpeed * (zoom - 1.0)) :
1.0 + 2.0 / PI * (maxScale - 1.0) * atan(inSpeed * (zoom - 1.0));
outputPointSize = pointSize * scale;
}
gl_PointSize = max(outputPointSize * scaleFactor, ${MIN_POINT_SIZE.toFixed(1)});
${ShaderChunk['fog_vertex']}
}
`;
...@@ -18,7 +18,6 @@ import routes, {Pages, Route} from '~/routes'; ...@@ -18,7 +18,6 @@ import routes, {Pages, Route} from '~/routes';
import {useCallback, useEffect, useState} from 'react'; import {useCallback, useEffect, useState} from 'react';
import ee from '~/utils/event'; import ee from '~/utils/event';
import {fetcher} from '~/utils/fetch';
import useRequest from '~/hooks/useRequest'; import useRequest from '~/hooks/useRequest';
export const navMap = { export const navMap = {
...@@ -36,7 +35,7 @@ export const navMap = { ...@@ -36,7 +35,7 @@ export const navMap = {
const useNavItems = () => { const useNavItems = () => {
const [components, setComponents] = useState<Route[]>([]); const [components, setComponents] = useState<Route[]>([]);
const {data, loading, error, mutate} = useRequest<(keyof typeof navMap)[]>('/components', fetcher, { const {data, loading, error, mutate} = useRequest<(keyof typeof navMap)[]>('/components', {
refreshInterval: components.length ? 61 * 1000 : 15 * 1000, refreshInterval: components.length ? 61 * 1000 : 15 * 1000,
dedupingInterval: 14 * 1000, dedupingInterval: 14 * 1000,
errorRetryInterval: 15 * 1000, errorRetryInterval: 15 * 1000,
......
...@@ -14,31 +14,41 @@ ...@@ -14,31 +14,41 @@
* limitations under the License. * limitations under the License.
*/ */
import type {ConfigInterface, keyInterface, responseInterface} from 'swr'; import type {Key, SWRConfiguration, SWRResponse} from 'swr';
import {useEffect, useMemo} from 'react'; import {useEffect, useMemo} from 'react';
import type {Fetcher} from 'swr/dist/types';
import ee from '~/utils/event'; import ee from '~/utils/event';
import type {fetcherFn} from 'swr/dist/types';
import {toast} from 'react-toastify'; import {toast} from 'react-toastify';
import useSWR from 'swr'; import useSWR from 'swr';
type Response<D, E> = responseInterface<D, E> & { type RequestConfig<D, E> = SWRConfiguration<D, E, Fetcher<D>>;
type RunningRequestConfig<D, E> = Omit<RequestConfig<D, E>, 'dedupingInterval' | 'errorRetryInterval'>;
type Response<D, E> = SWRResponse<D, E> & {
loading: boolean; loading: boolean;
}; };
function useRequest<D = unknown, E extends Error = Error>(key: keyInterface): Response<D, E>; function useRequest<D = unknown, E extends Error = Error>(key: Key): Response<D, E>;
function useRequest<D = unknown, E extends Error = Error>(key: keyInterface, fetcher?: fetcherFn<D>): Response<D, E>; function useRequest<D = unknown, E extends Error = Error>(key: Key, fetcher: Fetcher<D> | null): Response<D, E>;
function useRequest<D = unknown, E extends Error = Error>( function useRequest<D = unknown, E extends Error = Error>(
key: keyInterface, key: Key,
fetcher?: fetcherFn<D>, config: RequestConfig<D, E> | undefined
config?: ConfigInterface<D, E, fetcherFn<D>>
): Response<D, E>; ): Response<D, E>;
function useRequest<D = unknown, E extends Error = Error>( function useRequest<D = unknown, E extends Error = Error>(
key: keyInterface, key: Key,
fetcher?: fetcherFn<D>, fetcher: Fetcher<D> | null,
config?: ConfigInterface<D, E, fetcherFn<D>> config: RequestConfig<D, E> | undefined
): Response<D, E>;
function useRequest<D = unknown, E extends Error = Error>(
...args:
| readonly [Key]
| readonly [Key, Fetcher<D> | null]
| readonly [Key, RequestConfig<D, E> | undefined]
| readonly [Key, Fetcher<D> | null, RequestConfig<D, E> | undefined]
): Response<D, E> { ): Response<D, E> {
const {data, error, ...other} = useSWR<D, E>(key, fetcher, config); const key = args[0];
const {data, error, ...other} = useSWR<D, E>(...args);
const loading = useMemo(() => !!key && data === void 0 && !error, [key, data, error]); const loading = useMemo(() => !!key && data === void 0 && !error, [key, data, error]);
useEffect(() => { useEffect(() => {
...@@ -53,35 +63,64 @@ function useRequest<D = unknown, E extends Error = Error>( ...@@ -53,35 +63,64 @@ function useRequest<D = unknown, E extends Error = Error>(
return {data, error, loading, ...other}; return {data, error, loading, ...other};
} }
function useRunningRequest<D = unknown, E extends Error = Error>(key: keyInterface, running: boolean): Response<D, E>; function useRunningRequest<D = unknown, E extends Error = Error>(key: Key, running: boolean): Response<D, E>;
function useRunningRequest<D = unknown, E extends Error = Error>( function useRunningRequest<D = unknown, E extends Error = Error>(
key: keyInterface, key: Key,
running: boolean, running: boolean,
fetcher?: fetcherFn<D> fetcher: Fetcher<D> | null
): Response<D, E>; ): Response<D, E>;
function useRunningRequest<D = unknown, E extends Error = Error>( function useRunningRequest<D = unknown, E extends Error = Error>(
key: keyInterface, key: Key,
running: boolean, running: boolean,
fetcher?: fetcherFn<D>, config: RunningRequestConfig<D, E> | undefined
config?: Omit<ConfigInterface<D, E, fetcherFn<D>>, 'dedupingInterval' | 'errorRetryInterval'>
): Response<D, E>; ): Response<D, E>;
function useRunningRequest<D = unknown, E extends Error = Error>( function useRunningRequest<D = unknown, E extends Error = Error>(
key: keyInterface, key: Key,
running: boolean, running: boolean,
fetcher?: fetcherFn<D>, fetcher: Fetcher<D> | null,
config?: Omit<ConfigInterface<D, E, fetcherFn<D>>, 'dedupingInterval' | 'errorRetryInterval'> config: RunningRequestConfig<D, E> | undefined
): Response<D, E>;
function useRunningRequest<D = unknown, E extends Error = Error>(
...args:
| readonly [Key, boolean]
| readonly [Key, boolean, Fetcher<D> | null]
| readonly [Key, boolean, RequestConfig<D, E> | undefined]
| readonly [Key, boolean, Fetcher<D> | null, RequestConfig<D, E> | undefined]
) { ) {
const c = useMemo<ConfigInterface<D, E, fetcherFn<D>>>( const [key, running, ...options] = args;
let fetcher: Fetcher<D> | null | undefined = undefined;
let config: RequestConfig<D, E> | undefined = undefined;
if (options.length > 1) {
fetcher = options[0] as Fetcher<D> | null;
config = options[1];
} else if (options[0] != null) {
if (typeof options[0] === 'object') {
config = options[0];
} else if (typeof options[0] === 'function') {
fetcher = options[0];
}
}
const c = useMemo<SWRConfiguration<D, E, Fetcher<D>>>(
() => ({ () => ({
...config, ...config,
refreshInterval: running ? config?.refreshInterval ?? 15 * 1000 : 0, refreshInterval: running ? config?.refreshInterval ?? 15 * 1000 : 0,
dedupingInterval: config?.refreshInterval ?? 15 * 1000, dedupingInterval: config?.refreshInterval ?? 15 * 1000,
errorRetryInterval: config?.refreshInterval ?? 15 * 1000 errorRetryInterval: config?.refreshInterval ?? 15 * 1000
}), }),
[running, config] [config, running]
); );
const {mutate, ...others} = useRequest(key, fetcher, c); const requestArgs = useMemo<
readonly [Key, RequestConfig<D, E>] | readonly [Key, Fetcher<D> | null, RequestConfig<D, E>]
>(() => {
if (fetcher === undefined) {
return [key, c];
}
return [key, fetcher, c];
}, [key, fetcher, c]);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const {mutate, ...others} = (useRequest as any)(...requestArgs);
// revalidate immediately when running is set to true // revalidate immediately when running is set to true
useEffect(() => { useEffect(() => {
......
/**
* 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-i18next';
// refer to `react-i18next/ts4.1/index.d.ts`
// https://github.com/i18next/react-i18next/blob/master/example/react-typescript4.1/namespaces/%40types/react-i18next/index.d.ts
declare module 'react-i18next' {
// eslint-disable-next-line @typescript-eslint/ban-types
type DefaultResources = {};
// eslint-disable-next-line @typescript-eslint/no-empty-interface
interface Resources extends DefaultResources {}
}
...@@ -34,17 +34,17 @@ ...@@ -34,17 +34,17 @@
"devDependencies": { "devDependencies": {
"@types/express": "4.17.11", "@types/express": "4.17.11",
"@types/mkdirp": "1.0.1", "@types/mkdirp": "1.0.1",
"@types/node": "14.14.22", "@types/node": "14.14.37",
"@types/node-fetch": "2.5.8", "@types/node-fetch": "2.5.9",
"@types/rimraf": "3.0.0", "@types/rimraf": "3.0.0",
"cpy-cli": "3.1.1", "cpy-cli": "3.1.1",
"get-port": "5.1.1", "get-port": "5.1.1",
"mime-types": "2.1.28", "mime-types": "2.1.30",
"mkdirp": "1.0.4", "mkdirp": "1.0.4",
"node-fetch": "2.6.1", "node-fetch": "2.6.1",
"rimraf": "3.0.2", "rimraf": "3.0.2",
"ts-node": "9.1.1", "ts-node": "9.1.1",
"typescript": "4.0.5" "typescript": "4.2.3"
}, },
"peerDependencies": { "peerDependencies": {
"express": "^4.17.1" "express": "^4.17.1"
......
...@@ -35,18 +35,18 @@ ...@@ -35,18 +35,18 @@
}, },
"dependencies": { "dependencies": {
"express": "4.17.1", "express": "4.17.1",
"faker": "5.2.0", "faker": "5.5.2",
"isomorphic-unfetch": "3.1.0", "isomorphic-unfetch": "3.1.0",
"mime-types": "2.1.28" "mime-types": "2.1.30"
}, },
"devDependencies": { "devDependencies": {
"@types/express": "4.17.11", "@types/express": "4.17.11",
"@types/faker": "5.1.5", "@types/faker": "5.5.0",
"@types/node": "14.14.22", "@types/node": "14.14.37",
"cpy-cli": "3.1.1", "cpy-cli": "3.1.1",
"rimraf": "3.0.2", "rimraf": "3.0.2",
"ts-node": "9.1.1", "ts-node": "9.1.1",
"typescript": "4.0.5" "typescript": "4.2.3"
}, },
"engines": { "engines": {
"node": ">=12", "node": ">=12",
......
...@@ -35,24 +35,24 @@ ...@@ -35,24 +35,24 @@
"dagre": "0.8.5", "dagre": "0.8.5",
"flatbuffers": "1.12.0", "flatbuffers": "1.12.0",
"long": "4.0.0", "long": "4.0.0",
"marked": "1.2.8", "marked": "2.0.1",
"netron": "PeterPanZH/netron", "netron": "PeterPanZH/netron",
"pako": "1.0.11" "pako": "1.0.11"
}, },
"devDependencies": { "devDependencies": {
"autoprefixer": "10.2.3", "autoprefixer": "10.2.5",
"copy-webpack-plugin": "7.0.0", "copy-webpack-plugin": "8.1.1",
"css-loader": "5.0.1", "css-loader": "5.2.0",
"html-webpack-plugin": "4.5.1", "html-webpack-plugin": "5.3.1",
"mini-css-extract-plugin": "1.3.4", "mini-css-extract-plugin": "1.4.0",
"postcss": "8.2.4", "postcss": "8.2.9",
"postcss-loader": "4.2.0", "postcss-loader": "5.2.0",
"rimraf": "3.0.2", "rimraf": "3.0.2",
"sass": "1.32.5", "sass": "1.32.8",
"sass-loader": "10.1.1", "sass-loader": "11.0.1",
"terser": "5.5.1", "terser": "5.6.1",
"webpack": "5.18.0", "webpack": "5.30.0",
"webpack-cli": "4.4.0" "webpack-cli": "4.6.0"
}, },
"engines": { "engines": {
"node": ">=12", "node": ">=12",
......
...@@ -41,18 +41,18 @@ ...@@ -41,18 +41,18 @@
"dotenv": "8.2.0", "dotenv": "8.2.0",
"enhanced-resolve": "5.7.0", "enhanced-resolve": "5.7.0",
"express": "4.17.1", "express": "4.17.1",
"http-proxy-middleware": "1.0.6", "http-proxy-middleware": "1.1.0",
"pm2": "4.5.1" "pm2": "4.5.6"
}, },
"devDependencies": { "devDependencies": {
"@types/enhanced-resolve": "3.0.6", "@types/enhanced-resolve": "3.0.6",
"@types/express": "4.17.11", "@types/express": "4.17.11",
"@types/node": "14.14.22", "@types/node": "14.14.37",
"@visualdl/mock": "2.1.5", "@visualdl/mock": "2.1.5",
"cross-env": "7.0.3", "cross-env": "7.0.3",
"nodemon": "2.0.7", "nodemon": "2.0.7",
"ts-node": "9.1.1", "ts-node": "9.1.1",
"typescript": "4.0.5" "typescript": "4.2.3"
}, },
"optionalDependencies": { "optionalDependencies": {
"@visualdl/demo": "2.1.5" "@visualdl/demo": "2.1.5"
......
此差异已折叠。
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册