/** * Copyright 2020 Baidu Inc. All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import Aside, {AsideSection} from '~/components/Aside'; import type { Dimension, LabelMetadata, PCAResult, ParseParams, ParseResult, Reduction, Shape, TSNEResult, UMAPResult } from '~/resource/high-dimensional'; import HighDimensionalChart, {HighDimensionalChartRef} from '~/components/HighDimensionalPage/HighDimensionalChart'; import LabelSearchInput, {LabelSearchInputProps} from '~/components/HighDimensionalPage/LabelSearchInput'; import React, {FunctionComponent, useCallback, useEffect, useMemo, useRef, useState} from 'react'; import type {SelectListItem, SelectProps} from '~/components/Select'; import type {BlobResponse} from '~/utils/fetch'; import BodyLoading from '~/components/BodyLoading'; import Button from '~/components/Button'; import Content from '~/components/Content'; import DimensionSwitch from '~/components/HighDimensionalPage/DimensionSwitch'; import Error from '~/components/Error'; import Field from '~/components/Field'; import LabelSearchResult from '~/components/HighDimensionalPage/LabelSearchResult'; import {LabelType} from '~/resource/high-dimensional'; import PCADetail from '~/components/HighDimensionalPage/PCADetail'; import ReductionTab from '~/components/HighDimensionalPage/ReductionTab'; import ScatterChart from '~/components/ScatterChart/ScatterChart'; import Select from '~/components/Select'; import TSNEDetail from '~/components/HighDimensionalPage/TSNEDetail'; import Title from '~/components/Title'; import UMAPDetail from '~/components/HighDimensionalPage/UMAPDetail'; import UploadDialog from '~/components/HighDimensionalPage/UploadDialog'; import queryString from 'query-string'; import {rem} from '~/utils/style'; import styled from 'styled-components'; import {toast} from 'react-toastify'; import useRequest from '~/hooks/useRequest'; import {useTranslation} from 'react-i18next'; import useWorker from '~/hooks/useWorker'; const MODE = import.meta.env.MODE; const MAX_COUNT: Record = { pca: 50000, tsne: 10000, umap: 5000 } as const; const MAX_DIMENSION: Record = { pca: 200, tsne: undefined, umap: undefined }; const AsideTitle = styled.div` font-size: ${rem(16)}; line-height: ${rem(16)}; font-weight: 700; margin-bottom: ${rem(20)}; `; // styled-components cannot infer type of a component... // And I don't want to write a type guard // eslint-disable-next-line @typescript-eslint/no-explicit-any const FullWidthSelect = styled>>(Select)` width: 100%; `; const FullWidthButton = styled(Button)` width: 100%; `; const HDAside = styled(Aside)` .secondary { color: var(--text-light-color); } `; const RightAside = styled(HDAside)` border-left: 1px solid var(--border-color); `; const LeftAside = styled(HDAside)` border-right: 1px solid var(--border-color); ${AsideSection} { border-bottom: none; } > .aside-top > .search-result { margin-top: 0; margin-left: 0; margin-right: 0; flex: auto; overflow: hidden auto; } `; const HDContent = styled(Content)` background-color: var(--background-color); `; type EmbeddingInfo = { name: string; shape: [number, number]; path?: string; }; const HighDimensional: FunctionComponent = () => { const {t} = useTranslation(['high-dimensional', 'common']); const chart = useRef(null); const {data: list, loading: loadingList} = useRequest('/embedding/list'); const embeddingList = useMemo(() => list?.map(item => ({value: item.name, label: item.name, ...item})) ?? [], [ list ]); const [selectedEmbeddingName, setSelectedEmbeddingName] = useState(); const selectedEmbedding = useMemo( () => embeddingList.find(embedding => embedding.value === selectedEmbeddingName), [embeddingList, selectedEmbeddingName] ); useEffect(() => { setSelectedEmbeddingName(embeddingList[0]?.value ?? undefined); }, [embeddingList]); const {data: tensorData, loading: loadingTensor} = useRequest( selectedEmbeddingName ? `/embedding/tensor?${queryString.stringify({name: selectedEmbeddingName})}` : null ); const {data: metadataData, loading: loadingMetadata} = useRequest( selectedEmbeddingName ? `/embedding/metadata?${queryString.stringify({name: selectedEmbeddingName})}` : null ); const [uploadModal, setUploadModal] = useState(false); const [loading, setLoading] = useState(false); const [loadingPhase, setLoadingPhase] = useState(''); useEffect(() => { if (!loading) { setLoadingPhase(''); } }, [loading]); useEffect(() => { if (loadingPhase) { setLoading(true); } }, [loadingPhase]); useEffect(() => { if (loadingTensor) { setLoading(true); setLoadingPhase('fetching-tensor'); } }, [loadingTensor]); useEffect(() => { if (loadingMetadata) { setLoading(true); setLoadingPhase('fetching-metadata'); } }, [loadingMetadata]); const [vectorFile, setVectorFile] = useState(null); const [metadataFile, setMetadataFile] = useState(null); const changeVectorFile = useCallback((file: File) => { setVectorFile(file); setMetadataFile(null); }, []); const [vectorContent, setVectorContent] = useState(''); const [metadataContent, setMetadataContent] = useState(''); const [vectors, setVectors] = useState(new Float32Array()); const [labelList, setLabelList] = useState([]); const labelByList = useMemo[]>( () => labelList.map(({label}, index) => ({label, value: index})), [labelList] ); const [labelByIndex, setLabelByIndex] = useState(0); const colorByList = useMemo[]>( () => [ { label: t('high-dimensional:no-color-map'), value: -1 }, ...labelList.map((item, index) => ({ label: item.label, value: index, disabled: item.type === LabelType.Null || (item.type === LabelType.Category && item.categories.length > ScatterChart.CATEGORY_COLOR_MAP.length) })) ], [labelList, t] ); const [colorByIndex, setColorByIndex] = useState(-1); const [metadata, setMetadata] = useState([]); // dimension of data const [dim, setDim] = useState(0); const [rawShape, setRawShape] = useState([0, 0]); const selectedMetadata = useMemo(() => metadata[labelByIndex], [labelByIndex, metadata]); const selectedColor = useMemo( () => colorByIndex === -1 ? null : { ...labelList[colorByIndex], labels: metadata[colorByIndex] }, [colorByIndex, labelList, metadata] ); // dimension of display const [dimension, setDimension] = useState('3d'); const [reduction, setReduction] = useState('pca'); const is3D = useMemo(() => dimension === '3d', [dimension]); const readFile = useCallback( (phase: string, file: File | null, setter: React.Dispatch>) => { if (file) { setLoading(true); setLoadingPhase(phase); const reader = new FileReader(); reader.readAsText(file, 'utf-8'); reader.onload = () => { setter(content => { const result = reader.result as string; if (content === result) { setLoading(false); } return result; }); }; } else { setter(''); } }, [] ); useEffect(() => readFile('reading-vector', vectorFile, setVectorContent), [vectorFile, readFile]); useEffect(() => readFile('reading-metadata', metadataFile, setMetadataContent), [metadataFile, readFile]); useEffect(() => setVectorFile(null), [selectedEmbeddingName]); const showError = useCallback((e: Error) => { toast(e.message, { position: toast.POSITION.TOP_CENTER, type: toast.TYPE.ERROR }); if (MODE !== 'production') { // eslint-disable-next-line no-console console.error(e); } setLoading(false); }, []); const params = useMemo(() => { const maxValues = { maxCount: MAX_COUNT[reduction], maxDimension: MAX_DIMENSION[reduction] }; if (vectorContent) { return { from: 'string', params: { vectors: vectorContent, metadata: metadataContent, ...maxValues } }; } if (selectedEmbedding && tensorData) { return { from: 'blob', params: { shape: selectedEmbedding.shape, vectors: tensorData.data, metadata: metadataData ?? '', ...maxValues } }; } return null; }, [reduction, vectorContent, selectedEmbedding, tensorData, metadataContent, metadataData]); const result = useWorker('high-dimensional/parse-data', params); useEffect(() => { const {error, data} = result; if (error) { showError(error); } else if (data) { setRawShape(data.rawShape); setDim(data.dimension); setVectors(data.vectors); setLabelList(data.labels); setLabelByIndex(0); setColorByIndex(-1); setMetadata(data.metadata); } else if (data !== null) { setLoadingPhase('parsing'); } }, [result, showError]); const hasVector = useMemo(() => dim !== 0, [dim]); const dataPath = useMemo(() => (vectorFile ? vectorFile.name : selectedEmbedding?.path ?? ''), [ vectorFile, selectedEmbedding ]); const [perplexity, setPerplexity] = useState(5); const [learningRate, setLearningRate] = useState(10); const [neighbors, setNeighbors] = useState(15); const runUMAP = useCallback((n: number) => { setNeighbors(n); chart.current?.rerunUMAP(); }, []); const [data, setData] = useState(); const calculate = useCallback(() => { setData(undefined); setLoadingPhase('calculating'); }, []); const calculated = useCallback((data: PCAResult | TSNEResult | UMAPResult) => { setData(data); setLoading(false); }, []); const [searchResult, setSearchResult] = useState>['0']>({ labelIndex: undefined, value: '' }); const searchedResult = useMemo(() => { if (searchResult.labelIndex == null || searchResult.value === '') { return { indices: [], metadata: [] }; } const metadataList = metadata[searchResult.labelIndex]; const metadataResult: string[] = []; const vectorsIndices: number[] = []; for (let i = 0; i < metadataList.length; i++) { if (metadataList[i].includes(searchResult.value)) { metadataResult.push(metadataList[i]); vectorsIndices.push(i); } } // const vectorsResult = new Float32Array(vectorsIndices.length * dim); // for (let i = 0; i < vectorsIndices.length; i++) { // vectorsResult.set(vectors.subarray(vectorsIndices[i] * dim, vectorsIndices[i] * dim + dim), i * dim); // } return { indices: vectorsIndices, metadata: metadataResult }; }, [metadata, searchResult.labelIndex, searchResult.value]); const [hoveredIndices, setHoveredIndices] = useState([]); const hoverSearchResult = useCallback( (index?: number) => setHoveredIndices(index == null ? [] : [searchedResult.indices[index]]), [searchedResult.indices] ); const detail = useMemo(() => { switch (reduction) { case 'pca': return ( ); case 'tsne': return ( ); case 'umap': return ; default: return null as never; } }, [reduction, dimension, data, perplexity, learningRate, is3D, neighbors, runUMAP]); const aside = useMemo( () => ( {t('high-dimensional:data')} setUploadModal(true)}> {t('high-dimensional:upload-data')} {dataPath && (
{t('high-dimensional:data-path')} {t('common:colon')} {dataPath}
)}
{detail}
), [ t, embeddingList, selectedEmbeddingName, labelByList, labelByIndex, colorByList, colorByIndex, dataPath, reduction, dimension, detail ] ); const leftAside = useMemo( () => ( {searchResult.value !== '' && ( {t('high-dimensional:matched-result-count', { count: searchedResult.metadata.length })} )} ), [hoverSearchResult, labelByList, searchResult.value, searchedResult.metadata, t] ); return ( <> {t('common:high-dimensional')} {loading || loadingList ? {t(`high-dimensional:loading.${loadingPhase}`)} : null} {hasVector ? ( ) : ( )} setUploadModal(false)} onChangeVectorFile={changeVectorFile} onChangeMetadataFile={setMetadataFile} /> ); }; export default HighDimensional;