/** * 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, 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 Select, {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 PCADetail from '~/components/HighDimensionalPage/PCADetail'; import ReductionTab from '~/components/HighDimensionalPage/ReductionTab'; 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)}; `; 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 [labels, setLabels] = useState([]); const [labelBy, setLabelBy] = useState(); const [metadata, setMetadata] = useState([]); // dimension of data const [dim, setDim] = useState(0); const [rawShape, setRawShape] = useState([0, 0]); const getLabelByLabels = useCallback( (value: string | undefined) => { if (value != null) { const labelIndex = labels.indexOf(value); if (labelIndex !== -1) { return metadata.map(row => row[labelIndex]); } } return []; }, [labels, metadata] ); const labelByLabels = useMemo(() => getLabelByLabels(labelBy), [getLabelByLabels, labelBy]); // 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); setLabels(data.labels); setLabelBy(data.labels[0]); 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']>({ labelBy: undefined, value: '' }); const searchedResult = useMemo(() => { if (searchResult.labelBy == null || searchResult.value === '') { return { indices: [], metadata: [] }; } const labelByLabels = getLabelByLabels(searchResult.labelBy); const metadataResult: string[] = []; const vectorsIndices: number[] = []; for (let i = 0; i < labelByLabels.length; i++) { if (labelByLabels[i].includes(searchResult.value)) { metadataResult.push(labelByLabels[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 }; }, [getLabelByLabels, searchResult.labelBy, 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, dataPath, reduction, dimension, labels, labelBy, embeddingList, selectedEmbeddingName, detail] ); const leftAside = useMemo( () => ( {searchResult.value !== '' && ( {t('high-dimensional:matched-result-count', { count: searchedResult.metadata.length })} )} ), [hoverSearchResult, labels, 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;