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

feat: color map of high-dimensional page (#950)

* feat: show more menu in navbar

* feat: able to drag and change run sidebar width

* feat: color map of high-dimensional page

* fix: disable color map when category is more than color size
上级 6da5a64d
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
<path d="M30.957 32h-4.769l-3.725-9.723h-13.411l-3.539 9.723h-4.471l12.294-32h4.545l13.076 32zM21.196 18.887l-5.625-14.826-5.364 14.826h10.989z"></path>
</svg>
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="512" height="512" viewBox="0 0 512 512"> <svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
<path d="M179.2 256l230.4-219.429-38.4-36.572-268.8 256 268.8 256 38.4-36.572z"></path> <path d="M11.2 16l14.4-13.714-2.4-2.286-16.8 16 16.8 16 2.4-2.286z"></path>
</svg> </svg>
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="512" height="512" viewBox="0 0 512 512"> <svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
<path d="M332.8 256l-230.4-219.429 38.4-36.572 268.8 256-268.8 256-38.4-36.572z"></path> <path d="M20.8 16l-14.4 13.714 2.4 2.286 16.8-16-16.8-16-2.4 2.286z"></path>
</svg> </svg>
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="512" height="512" viewBox="0 0 512 512"> <svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
<path d="M256 179.2l-219.429 230.4-36.572-38.4 256-268.8 256 268.8-36.572 38.4z"></path> <path d="M16 11.2l-13.714 14.4-2.286-2.4 16-16.8 16 16.8-2.286 2.4z"></path>
</svg> </svg>
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="512" height="512" viewBox="0 0 512 512"> <svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
<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> <path d="M0 12.572v-7.999l2.613 2.609c2.963-4.483 7.983-7.181 13.362-7.181 8.85 0 16.025 7.163 16.025 16s-7.175 16-16.025 16c-6.524 0.001-12.397-3.946-14.851-9.981-0.285-0.702 0.053-1.501 0.755-1.786s1.503 0.053 1.789 0.754c2.035 5 6.902 8.271 12.307 8.27 7.333 0 13.278-5.936 13.278-13.257s-5.945-13.257-13.278-13.257c-4.737 0-9.011 2.503-11.374 6.422l3.411 3.407h-8.012z"></path>
</svg> </svg>
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="512" height="512" viewBox="0 0 512 512"> <svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
<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> <path d="M29.538 2.462h-4.923v-2.462h6.154c0.326 0 0.639 0.13 0.87 0.36s0.36 0.544 0.36 0.87v6.154h-2.462v-4.923zM29.538 24.615h2.462v6.154c0 0.68-0.551 1.231-1.231 1.231h-6.154v-2.462h4.923v-4.923zM7.385 29.538v2.462h-6.154c-0.326 0-0.639-0.13-0.87-0.36s-0.36-0.544-0.36-0.87v-6.154h2.462v4.923h4.923zM2.462 2.462v4.923h-2.462v-6.154c0-0.326 0.13-0.639 0.36-0.87s0.544-0.36 0.87-0.36h6.309v2.462h-5.078zM12.308 0h7.385v2.462h-7.385v-2.462zM12.308 29.538h7.385v2.462h-7.385v-2.462zM0 12.308h2.462v7.385h-2.462v-7.385zM29.538 12.308h2.462v7.385h-2.462v-7.385z"></path>
</svg> </svg>
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="512" height="512" viewBox="0 0 512 512"> <svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
<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="M22.949 26.32c2.947 0 5.209-0.868 6.788-2.578 1.5-1.631 2.263-5.459 2.263-8.354 0-2.947-0.71-5.92-2.105-7.498-1.526-1.736-3.762-2.578-6.735-2.578h-6.735v21.009h6.525zM22.581 24.159h-3.95v-16.801h4.187c2.473 0 4.265 0.509 5.422 1.799 1.079 1.21 1.655 3.705 1.655 6.231 0 2.447-0.631 5.704-1.789 6.967-1.237 1.289-3.078 1.804-5.525 1.804z"></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> <path d="M6.604 26.689c1.947 0 3.526-0.605 4.736-1.815 1.184-1.181 1.789-2.708 1.789-4.581 0-1.21-0.316-2.218-0.947-3.025-0.579-0.807-1.421-1.383-2.526-1.757 2.052-0.749 3.078-2.305 3.078-4.61 0-1.729-0.553-3.083-1.631-4.091-1.105-1.008-2.578-1.498-4.367-1.498s-3.236 0.547-4.341 1.671c-1.158 1.124-1.842 2.679-2 4.667h2.131c0.132-1.383 0.553-2.449 1.289-3.198 0.71-0.749 1.684-1.095 2.947-1.095 1.21 0 2.184 0.317 2.868 1.008 0.631 0.634 0.974 1.527 0.974 2.679s-0.342 2.046-1 2.679c-0.658 0.605-1.605 0.922-2.868 0.922h-1.447v1.844h1.526c1.316 0 2.315 0.317 3.052 1.008s1.105 1.642 1.105 2.881c0 1.21-0.395 2.218-1.131 3.025-0.816 0.835-1.894 1.268-3.236 1.268-1.184 0-2.157-0.375-2.947-1.066-0.921-0.835-1.421-2.074-1.473-3.688h-2.184c0.158 2.305 0.895 4.062 2.236 5.272 1.131 0.98 2.578 1.498 4.367 1.498z"></path>
</svg> </svg>
{ {
"3d-label": "Enable/disable 3D labels mode", "3d-label": "Show/Hide data detail",
"component": "Component #{{index}}", "component": "Component #{{index}}",
"continue": "Resume", "continue": "Resume",
"data": "Data", "data": "Data",
...@@ -22,6 +22,7 @@ ...@@ -22,6 +22,7 @@
}, },
"matched-result-count": "{{count}} matched.", "matched-result-count": "{{count}} matched.",
"neighbors": "Neighbors", "neighbors": "Neighbors",
"no-color-map": "No color map",
"pause": "Pause", "pause": "Pause",
"perplexity": "Perplexity", "perplexity": "Perplexity",
"points": "Points", "points": "Points",
......
{ {
"3d-label": "开启/关闭3D数据标签", "3d-label": "显示/隐藏数据详情",
"component": "主成分{{index}}", "component": "主成分{{index}}",
"continue": "继续", "continue": "继续",
"data": "数据", "data": "数据",
...@@ -22,6 +22,7 @@ ...@@ -22,6 +22,7 @@
}, },
"matched-result-count": "匹配结果 {{count}}", "matched-result-count": "匹配结果 {{count}}",
"neighbors": "相邻数据点数量", "neighbors": "相邻数据点数量",
"no-color-map": "无颜色",
"pause": "暂停", "pause": "暂停",
"perplexity": "困惑度", "perplexity": "困惑度",
"points": "数据点", "points": "数据点",
......
...@@ -54,11 +54,8 @@ const Operations = styled.div` ...@@ -54,11 +54,8 @@ const Operations = styled.div`
} }
} }
&.three-d { &:hover,
font-size: ${rem(20)}; &.active {
}
&:hover {
color: var(--primary-focused-color); color: var(--primary-focused-color);
} }
...@@ -69,11 +66,12 @@ const Operations = styled.div` ...@@ -69,11 +66,12 @@ const Operations = styled.div`
`; `;
type ChartOperationsProps = { type ChartOperationsProps = {
labelCloud: boolean;
onToggleLabelCloud?: () => unknown; onToggleLabelCloud?: () => unknown;
onReset?: () => unknown; onReset?: () => unknown;
}; };
const ChartOperations: FunctionComponent<ChartOperationsProps> = ({onToggleLabelCloud, onReset}) => { const ChartOperations: FunctionComponent<ChartOperationsProps> = ({labelCloud, onToggleLabelCloud, onReset}) => {
const {t} = useTranslation('high-dimensional'); const {t} = useTranslation('high-dimensional');
return ( return (
...@@ -86,9 +84,9 @@ const ChartOperations: FunctionComponent<ChartOperationsProps> = ({onToggleLabel ...@@ -86,9 +84,9 @@ const ChartOperations: FunctionComponent<ChartOperationsProps> = ({onToggleLabel
</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" onClick={() => onToggleLabelCloud?.()}> <a onClick={() => onToggleLabelCloud?.()} className={labelCloud ? 'active' : ''}>
<span> <span>
<Icon type="three-d" /> <Icon type="a" />
</span> </span>
</a> </a>
</Tippy> </Tippy>
......
...@@ -68,6 +68,7 @@ const Chart = styled.div` ...@@ -68,6 +68,7 @@ const Chart = styled.div`
type HighDimensionalChartProps = { type HighDimensionalChartProps = {
vectors: Float32Array; vectors: Float32Array;
labels: string[]; labels: string[];
colorMap?: ScatterChartProps['colorMap'];
shape: Shape; shape: Shape;
dim: number; dim: number;
is3D: boolean; is3D: boolean;
...@@ -94,6 +95,7 @@ const HighDimensionalChart = React.forwardRef<HighDimensionalChartRef, HighDimen ...@@ -94,6 +95,7 @@ const HighDimensionalChart = React.forwardRef<HighDimensionalChartRef, HighDimen
{ {
vectors, vectors,
labels, labels,
colorMap,
shape, shape,
dim, dim,
is3D, is3D,
...@@ -265,6 +267,7 @@ const HighDimensionalChart = React.forwardRef<HighDimensionalChartRef, HighDimen ...@@ -265,6 +267,7 @@ const HighDimensionalChart = React.forwardRef<HighDimensionalChartRef, HighDimen
{shape[1]} {shape[1]}
</div> </div>
<ChartOperations <ChartOperations
labelCloud={showLabelCloud}
onToggleLabelCloud={() => setShowLabelCloud(s => !s)} onToggleLabelCloud={() => setShowLabelCloud(s => !s)}
onReset={() => chart.current?.reset()} onReset={() => chart.current?.reset()}
/> />
...@@ -276,6 +279,7 @@ const HighDimensionalChart = React.forwardRef<HighDimensionalChartRef, HighDimen ...@@ -276,6 +279,7 @@ const HighDimensionalChart = React.forwardRef<HighDimensionalChartRef, HighDimen
height={height} height={height}
data={data?.vectors ?? []} data={data?.vectors ?? []}
labels={labels} labels={labels}
colorMap={colorMap}
is3D={is3D} is3D={is3D}
rotate={reduction !== 'tsne'} rotate={reduction !== 'tsne'}
focusedIndices={focusedIndices} focusedIndices={focusedIndices}
......
...@@ -15,9 +15,10 @@ ...@@ -15,9 +15,10 @@
*/ */
import React, {FunctionComponent, useEffect, useState} from 'react'; import React, {FunctionComponent, useEffect, useState} from 'react';
import Select, {SelectProps} from '~/components/Select'; import type {SelectListItem, SelectProps} from '~/components/Select';
import SearchInput from '~/components/SearchInput'; import SearchInput from '~/components/SearchInput';
import Select from '~/components/Select';
import {rem} from '~/utils/style'; import {rem} from '~/utils/style';
import styled from 'styled-components'; import styled from 'styled-components';
import useSearchValue from '~/hooks/useSearchValue'; import useSearchValue from '~/hooks/useSearchValue';
...@@ -27,7 +28,7 @@ const Wrapper = styled.div` ...@@ -27,7 +28,7 @@ const Wrapper = styled.div`
display: flex; display: flex;
`; `;
const LabelSelect = styled<React.FunctionComponent<SelectProps<string>>>(Select)` const LabelSelect = styled<React.FunctionComponent<SelectProps<number>>>(Select)`
width: 45%; width: 45%;
min-width: ${rem(80)}; min-width: ${rem(80)};
max-width: ${rem(200)}; max-width: ${rem(200)};
...@@ -44,43 +45,34 @@ const LabelInput = styled(SearchInput)` ...@@ -44,43 +45,34 @@ const LabelInput = styled(SearchInput)`
`; `;
type LabelSearchResult = { type LabelSearchResult = {
labelBy: string | undefined; labelIndex: number | undefined;
value: string; value: string;
}; };
export type LabelSearchInputProps = { export type LabelSearchInputProps = {
labels: string[]; labels: SelectListItem<number>[];
onChange?: (result: LabelSearchResult) => unknown; onChange?: (result: LabelSearchResult) => unknown;
}; };
const LabelSearchInput: FunctionComponent<LabelSearchInputProps> = ({labels, onChange}) => { const LabelSearchInput: FunctionComponent<LabelSearchInputProps> = ({labels, onChange}) => {
const [labelBy, setLabelBy] = useState<string | undefined>(labels[0] ?? undefined); const [labelIndex, setLabelIndex] = useState<number>(0);
const [value, setValue] = useState(''); const [value, setValue] = useState('');
useEffect(() => { useEffect(() => {
if (labels.length) { setLabelIndex(0);
setLabelBy(label => {
if (label && labels.includes(label)) {
return label;
}
return labels[0];
});
} else {
setLabelBy(undefined);
}
}, [labels]); }, [labels]);
const debouncedValue = useSearchValue(value); const debouncedValue = useSearchValue(value);
useEffect(() => { useEffect(() => {
onChange?.({ onChange?.({
labelBy, labelIndex,
value: debouncedValue value: debouncedValue
}); });
}, [labelBy, onChange, debouncedValue]); }, [labelIndex, onChange, debouncedValue]);
return ( return (
<Wrapper> <Wrapper>
<LabelSelect list={labels} value={labelBy} onChange={setLabelBy} /> <LabelSelect list={labels} value={labelIndex} onChange={setLabelIndex} />
<LabelInput value={value} onChange={setValue} /> <LabelInput value={value} onChange={setValue} />
</Wrapper> </Wrapper>
); );
......
...@@ -14,12 +14,12 @@ ...@@ -14,12 +14,12 @@
* limitations under the License. * limitations under the License.
*/ */
import type {ColorMap, Point3D} from './types';
import React, {useEffect, useImperativeHandle, useRef} from 'react'; import React, {useEffect, useImperativeHandle, useRef} from 'react';
import type Chart from './ScatterChart'; import type Chart from './ScatterChart';
import type {ScatterChartOptions as ChartOptions} from './ScatterChart'; import type {ScatterChartOptions as ChartOptions} from './ScatterChart';
import LabelChart from './Labels'; import LabelChart from './Labels';
import type {Point3D} from './types';
import PointChart from './Points'; 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';
...@@ -37,6 +37,7 @@ export type ScatterChartProps = { ...@@ -37,6 +37,7 @@ export type ScatterChartProps = {
height: number; height: number;
data: Point3D[]; data: Point3D[];
labels: string[]; labels: string[];
colorMap?: ColorMap | null;
is3D: boolean; is3D: boolean;
rotate?: boolean; rotate?: boolean;
focusedIndices?: number[]; focusedIndices?: number[];
...@@ -49,7 +50,7 @@ export type ScatterChartRef = { ...@@ -49,7 +50,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, type, className}, ref) => { ({width, height, data, labels, colorMap, is3D, rotate, focusedIndices, highlightIndices, type, className}, ref) => {
const theme = useTheme(); const theme = useTheme();
const element = useRef<HTMLDivElement>(null); const element = useRef<HTMLDivElement>(null);
...@@ -84,8 +85,8 @@ const ScatterChart = React.forwardRef<ScatterChartRef, ScatterChartProps & WithS ...@@ -84,8 +85,8 @@ const ScatterChart = React.forwardRef<ScatterChartRef, ScatterChartProps & WithS
}, [is3D, rotate]); }, [is3D, rotate]);
useEffect(() => { useEffect(() => {
chart.current?.setData(data, labels); chart.current?.setData(data, labels, colorMap);
}, [data, labels, type]); }, [data, labels, colorMap, type]);
useEffect(() => { useEffect(() => {
chart.current?.setFocusedPointIndices(focusedIndices ?? []); chart.current?.setFocusedPointIndices(focusedIndices ?? []);
......
...@@ -53,6 +53,19 @@ export default class LabelScatterChart extends ScatterChart { ...@@ -53,6 +53,19 @@ export default class LabelScatterChart extends ScatterChart {
return this.mesh; return this.mesh;
} }
get defaultColor() {
return LabelScatterChart.LABEL_BACKGROUND_COLOR_DEFAULT;
}
get hoveredColor() {
return LabelScatterChart.LABEL_BACKGROUND_COLOR_HOVER;
}
get focusedColor() {
return LabelScatterChart.LABEL_BACKGROUND_COLOR_FOCUS;
}
get highLightColor() {
return LabelScatterChart.LABEL_BACKGROUND_COLOR_HIGHLIGHT;
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
protected createShaderUniforms(picking: boolean): Record<string, THREE.IUniform<any>> { protected createShaderUniforms(picking: boolean): Record<string, THREE.IUniform<any>> {
return { return {
...@@ -154,16 +167,7 @@ export default class LabelScatterChart extends ScatterChart { ...@@ -154,16 +167,7 @@ export default class LabelScatterChart extends ScatterChart {
const count = this.dataCount; const count = this.dataCount;
const colors = new Float32Array(count * VERTEX_COUNT_PER_LABEL * 3); const colors = new Float32Array(count * VERTEX_COUNT_PER_LABEL * 3);
for (let i = 0; i < count; i++) { for (let i = 0; i < count; i++) {
let color: THREE.Color; const color = this.getColorByIndex(i);
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++) { 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] = color.r;
colors[i * VERTEX_COUNT_PER_LABEL * 3 + j * 3 + 1] = color.g; colors[i * VERTEX_COUNT_PER_LABEL * 3 + j * 3 + 1] = color.g;
......
...@@ -23,7 +23,7 @@ import type {ScatterChartOptions} from '../ScatterChart'; ...@@ -23,7 +23,7 @@ import type {ScatterChartOptions} from '../ScatterChart';
import fragmentShader from './fragment.glsl'; import fragmentShader from './fragment.glsl';
import vertexShader from './vertex.glsl'; import vertexShader from './vertex.glsl';
export default class PointScatter extends ScatterChart { export default class PointScatterChart extends ScatterChart {
static readonly NUM_POINTS_FOG_THRESHOLD = 5000; static readonly NUM_POINTS_FOG_THRESHOLD = 5000;
static readonly POINT_COLOR_DEFAULT = new THREE.Color(0x7e7e7e); static readonly POINT_COLOR_DEFAULT = new THREE.Color(0x7e7e7e);
...@@ -50,6 +50,19 @@ export default class PointScatter extends ScatterChart { ...@@ -50,6 +50,19 @@ export default class PointScatter extends ScatterChart {
return this.points; return this.points;
} }
get defaultColor() {
return PointScatterChart.POINT_COLOR_DEFAULT;
}
get hoveredColor() {
return PointScatterChart.POINT_COLOR_HOVER;
}
get focusedColor() {
return PointScatterChart.POINT_COLOR_FOCUS;
}
get highLightColor() {
return PointScatterChart.POINT_COLOR_HIGHLIGHT;
}
constructor(container: HTMLElement, options: ScatterChartOptions) { constructor(container: HTMLElement, options: ScatterChartOptions) {
super(container, options); super(container, options);
this.label = new ScatterChartLabel(this.container, { this.label = new ScatterChartLabel(this.container, {
...@@ -82,25 +95,11 @@ export default class PointScatter extends ScatterChart { ...@@ -82,25 +95,11 @@ export default class PointScatter extends ScatterChart {
private convertPointsColor() { private convertPointsColor() {
const count = this.dataCount; const count = this.dataCount;
const colors = new Float32Array(count * 3); const colors = new Float32Array(count * 3);
let dst = 0;
for (let i = 0; i < count; i++) { for (let i = 0; i < count; i++) {
if (this.hoveredDataIndices.includes(i)) { const color = this.getColorByIndex(i);
colors[dst++] = PointScatter.POINT_COLOR_HOVER.r; colors[i * 3] = color.r;
colors[dst++] = PointScatter.POINT_COLOR_HOVER.g; colors[i * 3 + 1] = color.g;
colors[dst++] = PointScatter.POINT_COLOR_HOVER.b; colors[i * 3 + 2] = color.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; return colors;
} }
...@@ -110,13 +109,13 @@ export default class PointScatter extends ScatterChart { ...@@ -110,13 +109,13 @@ export default class PointScatter extends ScatterChart {
const scaleFactor = new Float32Array(count); const scaleFactor = new Float32Array(count);
for (let i = 0; i < count; i++) { for (let i = 0; i < count; i++) {
if (this.hoveredDataIndices.includes(i)) { if (this.hoveredDataIndices.includes(i)) {
scaleFactor[i] = PointScatter.POINT_SCALE_HOVER; scaleFactor[i] = PointScatterChart.POINT_SCALE_HOVER;
} else if (this.focusedDataIndices.includes(i)) { } else if (this.focusedDataIndices.includes(i)) {
scaleFactor[i] = PointScatter.POINT_SCALE_FOCUS; scaleFactor[i] = PointScatterChart.POINT_SCALE_FOCUS;
} else if (this.highLightDataIndices.includes(i)) { } else if (this.highLightDataIndices.includes(i)) {
scaleFactor[i] = PointScatter.POINT_SCALE_HIGHLIGHT; scaleFactor[i] = PointScatterChart.POINT_SCALE_HIGHLIGHT;
} else { } else {
scaleFactor[i] = PointScatter.POINT_SCALE_DEFAULT; scaleFactor[i] = PointScatterChart.POINT_SCALE_DEFAULT;
} }
} }
return scaleFactor; return scaleFactor;
...@@ -190,7 +189,9 @@ export default class PointScatter extends ScatterChart { ...@@ -190,7 +189,9 @@ export default class PointScatter extends ScatterChart {
} }
const multiplier = const multiplier =
2 - Math.min(n, PointScatter.NUM_POINTS_FOG_THRESHOLD) / PointScatter.NUM_POINTS_FOG_THRESHOLD; 2 -
Math.min(n, PointScatterChart.NUM_POINTS_FOG_THRESHOLD) /
PointScatterChart.NUM_POINTS_FOG_THRESHOLD;
fog.near = shortestDist; fog.near = shortestDist;
fog.far = furthestDist * multiplier; fog.far = furthestDist * multiplier;
......
...@@ -14,6 +14,6 @@ ...@@ -14,6 +14,6 @@
* limitations under the License. * limitations under the License.
*/ */
import PointScatter from './Points'; import PointScatterChart from './Points';
export default PointScatter; export default PointScatterChart;
...@@ -19,8 +19,9 @@ ...@@ -19,8 +19,9 @@
import * as THREE from 'three'; import * as THREE from 'three';
import * as d3 from 'd3'; import * as d3 from 'd3';
import type {Point2D, Point3D} from './types'; import type {ColorMap, Point2D, Point3D} from './types';
import {ColorType} from './types';
import {OrbitControls} from 'three/examples/jsm/controls/OrbitControls'; import {OrbitControls} from 'three/examples/jsm/controls/OrbitControls';
export type ScatterChartOptions = { export type ScatterChartOptions = {
...@@ -44,6 +45,28 @@ export default abstract class ScatterChart { ...@@ -44,6 +45,28 @@ export default abstract class ScatterChart {
static readonly PERSP_CAMERA_INIT_POSITION: Point3D = [0.45, 0.9, 1.6]; static readonly PERSP_CAMERA_INIT_POSITION: Point3D = [0.45, 0.9, 1.6];
static readonly ORTHO_CAMERA_INIT_POSITION: Point3D = [0, 0, 4]; static readonly ORTHO_CAMERA_INIT_POSITION: Point3D = [0, 0, 4];
static readonly VALUE_COLOR_MAP_RANGE = ['#ffffdd', '#1f2d86'] as const;
static readonly CATEGORY_COLOR_MAP = [
'#9BB9E8',
'#8BB8FF',
'#B4CCB7',
'#A8E9B8',
'#DB989A',
'#6DCDE4',
'#93C2CA',
'#DE7CCE',
'#DA96BC',
'#309E51',
'#D6C482',
'#6D7CE4',
'#CDCB74',
'#2576AD',
'#E46D6D',
'#CA5353',
'#E49D6D',
'#E4E06D'
].map(color => new THREE.Color(color));
width: number; width: number;
height: number; height: number;
background: string | number | THREE.Color = '#fff'; background: string | number | THREE.Color = '#fff';
...@@ -78,6 +101,9 @@ export default abstract class ScatterChart { ...@@ -78,6 +101,9 @@ export default abstract class ScatterChart {
protected blending: THREE.Blending = THREE.NormalBlending; protected blending: THREE.Blending = THREE.NormalBlending;
protected depth = true; protected depth = true;
protected colorMap: ColorMap = {type: ColorType.Null, labels: []};
protected colorGenerator: ((value: string) => THREE.Color) | null = null;
private mouseCoordinates: Point2D | null = null; private mouseCoordinates: Point2D | null = null;
private onMouseMoveBindThis: (e: MouseEvent) => void; private onMouseMoveBindThis: (e: MouseEvent) => void;
...@@ -89,6 +115,10 @@ export default abstract class ScatterChart { ...@@ -89,6 +115,10 @@ export default abstract class ScatterChart {
private animationId: number | null = null; private animationId: number | null = null;
protected abstract get object(): (THREE.Object3D & {material: THREE.Material | THREE.Material[]}) | null; protected abstract get object(): (THREE.Object3D & {material: THREE.Material | THREE.Material[]}) | null;
protected abstract get defaultColor(): THREE.Color;
protected abstract get hoveredColor(): THREE.Color;
protected abstract get focusedColor(): THREE.Color;
protected abstract get highLightColor(): THREE.Color;
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
protected abstract createShaderUniforms(picking: boolean): Record<string, THREE.IUniform<any>>; protected abstract createShaderUniforms(picking: boolean): Record<string, THREE.IUniform<any>>;
protected abstract onRender(): void; protected abstract onRender(): void;
...@@ -251,6 +281,53 @@ export default abstract class ScatterChart { ...@@ -251,6 +281,53 @@ export default abstract class ScatterChart {
}); });
} }
private convertColorMap() {
switch (this.colorMap.type) {
case ColorType.Value: {
const {minValue, maxValue} = this.colorMap;
return (label: string) => {
const value = Number.parseFloat(label);
if (!Number.isFinite(value)) {
return this.defaultColor;
}
const ranger = d3
.scaleLinear<string, string>()
.domain([minValue, maxValue])
.range(ScatterChart.VALUE_COLOR_MAP_RANGE);
return new THREE.Color(ranger(value));
};
}
case ColorType.Category: {
const categories = this.colorMap.categories;
return (label: string) => {
const index = categories.indexOf(label);
if (index === -1) {
return this.defaultColor;
}
return ScatterChart.CATEGORY_COLOR_MAP[index % ScatterChart.CATEGORY_COLOR_MAP.length];
};
}
default:
return null;
}
}
protected getColorByIndex(index: number): THREE.Color {
if (this.hoveredDataIndices.includes(index)) {
return this.hoveredColor;
}
if (this.focusedDataIndices.includes(index)) {
return this.focusedColor;
}
if (this.highLightDataIndices.includes(index)) {
return this.highLightColor;
}
if (this.colorGenerator) {
return this.colorGenerator(this.colorMap.labels[index]);
}
return this.defaultColor;
}
protected createMaterial() { protected createMaterial() {
this.material = this.createRenderMaterial(); this.material = this.createRenderMaterial();
this.pickingMaterial = this.createPickingMaterial(); this.pickingMaterial = this.createPickingMaterial();
...@@ -476,15 +553,17 @@ export default abstract class ScatterChart { ...@@ -476,15 +553,17 @@ export default abstract class ScatterChart {
} else { } else {
this.removeAxes(); this.removeAxes();
} }
this.setData(this.data, this.labels); this.setData(this.data, this.labels, this.colorMap);
} }
setData(data: Point3D[], labels: string[]) { setData(data: Point3D[], labels: string[], colorMap?: ColorMap | null) {
if (this.object) { if (this.object) {
this.scene.remove(this.object); this.scene.remove(this.object);
} }
this.labels = labels; this.labels = labels;
this.colorMap = colorMap ?? {type: ColorType.Null, labels: []};
this.data = data; this.data = data;
this.colorGenerator = this.convertColorMap();
this.positions = this.convertDataToPosition(); this.positions = this.convertDataToPosition();
this.pickingColors = this.convertDataPickingColor(); this.pickingColors = this.convertDataPickingColor();
this.onDataSet(); this.onDataSet();
......
...@@ -17,5 +17,7 @@ ...@@ -17,5 +17,7 @@
import ScatterChart from './Component'; import ScatterChart from './Component';
export type {ScatterChartProps, ScatterChartRef} from './Component'; export type {ScatterChartProps, ScatterChartRef} from './Component';
export type {ColorMap} from './types';
export {ColorType} from './types';
export default ScatterChart; export default ScatterChart;
...@@ -14,5 +14,10 @@ ...@@ -14,5 +14,10 @@
* limitations under the License. * limitations under the License.
*/ */
export type Point2D = [number, number]; import type {HighDimensionalColorMap} from '~/types';
export type Point3D = [number, number, number];
export type {Point2D, Point3D} from '~/types';
export {HighDimensionalColorType as ColorType} from '~/types';
export type ColorMap = HighDimensionalColorMap & {labels: string[]};
...@@ -115,32 +115,56 @@ const listItem = css` ...@@ -115,32 +115,56 @@ const listItem = css`
${size(height, '100%')} ${size(height, '100%')}
line-height: ${height}; line-height: ${height};
${transitionProps(['color', 'background-color'])} ${transitionProps(['color', 'background-color'])}
`;
const hoverListItem = css`
&:hover { &:hover {
background-color: var(--background-focused-color); background-color: var(--background-focused-color);
} }
`; `;
const ListItem = styled.div<{selected?: boolean}>` const ListItem = styled.div<{selected?: boolean; disabled?: boolean}>`
${ellipsis()} ${ellipsis()}
${listItem} ${listItem}
${props => (props.selected ? `color: var(--select-selected-text-color);` : '')} ${props => {
if (props.disabled) {
return css`
cursor: not-allowed;
`;
} else {
return hoverListItem;
}
}}
${props => {
if (props.selected) {
return css`
color: var(--select-selected-text-color);
`;
}
if (props.disabled) {
return css`
color: var(--text-light-color);
`;
}
}}
`; `;
const MultipleListItem = styled(Checkbox)<{selected?: boolean}>` const MultipleListItem = styled(Checkbox)<{selected?: boolean; disabled?: boolean}>`
${listItem} ${listItem}
display: flex; display: flex;
align-items: center; align-items: center;
${props => (props.disabled ? '' : hoverListItem)}
`; `;
type SelectListItem<T> = { type OnSingleChange<T> = (value: T) => unknown;
type OnMultipleChange<T> = (value: T[]) => unknown;
export type SelectListItem<T> = {
value: T; value: T;
label: string; label: string;
disabled?: boolean;
}; };
type OnSingleChange<T> = (value: T) => unknown;
type OnMultipleChange<T> = (value: T[]) => unknown;
export type SelectProps<T> = { export type SelectProps<T> = {
list?: (SelectListItem<T> | T)[]; list?: (SelectListItem<T> | T)[];
placeholder?: string; placeholder?: string;
...@@ -177,12 +201,15 @@ const Select = <T extends unknown>({ ...@@ -177,12 +201,15 @@ const Select = <T extends unknown>({
propValue propValue
]); ]);
const isSelected = useMemo(() => !!(multiple ? (value as T[]) && (value as T[]).length !== 0 : (value as T)), [ const isSelected = useMemo(
multiple, () => !!(multiple ? (value as T[]) && (value as T[]).length !== 0 : value != (null as T)),
value [multiple, value]
]); );
const changeValue = useCallback( const changeValue = useCallback(
(mutateValue: T) => { ({value: mutateValue, disabled}: SelectListItem<T>) => {
if (disabled) {
return;
}
setValue(mutateValue); setValue(mutateValue);
(onChange as OnSingleChange<T>)?.(mutateValue); (onChange as OnSingleChange<T>)?.(mutateValue);
closeDropdown(); closeDropdown();
...@@ -190,7 +217,10 @@ const Select = <T extends unknown>({ ...@@ -190,7 +217,10 @@ const Select = <T extends unknown>({
[closeDropdown, onChange] [closeDropdown, onChange]
); );
const changeMultipleValue = useCallback( const changeMultipleValue = useCallback(
(mutateValue: T, checked: boolean) => { ({value: mutateValue, disabled}: SelectListItem<T>, checked: boolean) => {
if (disabled) {
return;
}
let newValue = value as T[]; let newValue = value as T[];
if (checked) { if (checked) {
if (!newValue.includes(mutateValue)) { if (!newValue.includes(mutateValue)) {
...@@ -247,8 +277,9 @@ const Select = <T extends unknown>({ ...@@ -247,8 +277,9 @@ const Select = <T extends unknown>({
value={(value as T[]).includes(item.value)} value={(value as T[]).includes(item.value)}
key={index} key={index}
title={item.label} title={item.label}
disabled={item.disabled}
size="small" size="small"
onChange={checked => changeMultipleValue(item.value, checked)} onChange={checked => changeMultipleValue(item, checked)}
> >
{item.label} {item.label}
</MultipleListItem> </MultipleListItem>
...@@ -259,7 +290,8 @@ const Select = <T extends unknown>({ ...@@ -259,7 +290,8 @@ const Select = <T extends unknown>({
selected={item.value === value} selected={item.value === value}
key={index} key={index}
title={item.label} title={item.label}
onClick={() => changeValue(item.value)} disabled={item.disabled}
onClick={() => changeValue(item)}
> >
{item.label} {item.label}
</ListItem> </ListItem>
......
...@@ -17,6 +17,7 @@ ...@@ -17,6 +17,7 @@
import Aside, {AsideSection} from '~/components/Aside'; import Aside, {AsideSection} from '~/components/Aside';
import type { import type {
Dimension, Dimension,
LabelMetadata,
PCAResult, PCAResult,
ParseParams, ParseParams,
ParseResult, ParseResult,
...@@ -28,7 +29,7 @@ import type { ...@@ -28,7 +29,7 @@ import type {
import HighDimensionalChart, {HighDimensionalChartRef} from '~/components/HighDimensionalPage/HighDimensionalChart'; import HighDimensionalChart, {HighDimensionalChartRef} from '~/components/HighDimensionalPage/HighDimensionalChart';
import LabelSearchInput, {LabelSearchInputProps} from '~/components/HighDimensionalPage/LabelSearchInput'; import LabelSearchInput, {LabelSearchInputProps} from '~/components/HighDimensionalPage/LabelSearchInput';
import React, {FunctionComponent, useCallback, useEffect, useMemo, useRef, useState} from 'react'; import React, {FunctionComponent, useCallback, useEffect, useMemo, useRef, useState} from 'react';
import Select, {SelectProps} from '~/components/Select'; import type {SelectListItem, SelectProps} from '~/components/Select';
import type {BlobResponse} from '~/utils/fetch'; import type {BlobResponse} from '~/utils/fetch';
import BodyLoading from '~/components/BodyLoading'; import BodyLoading from '~/components/BodyLoading';
...@@ -38,8 +39,11 @@ import DimensionSwitch from '~/components/HighDimensionalPage/DimensionSwitch'; ...@@ -38,8 +39,11 @@ import DimensionSwitch from '~/components/HighDimensionalPage/DimensionSwitch';
import Error from '~/components/Error'; import Error from '~/components/Error';
import Field from '~/components/Field'; import Field from '~/components/Field';
import LabelSearchResult from '~/components/HighDimensionalPage/LabelSearchResult'; import LabelSearchResult from '~/components/HighDimensionalPage/LabelSearchResult';
import {LabelType} from '~/resource/high-dimensional';
import PCADetail from '~/components/HighDimensionalPage/PCADetail'; import PCADetail from '~/components/HighDimensionalPage/PCADetail';
import ReductionTab from '~/components/HighDimensionalPage/ReductionTab'; import ReductionTab from '~/components/HighDimensionalPage/ReductionTab';
import ScatterChart from '~/components/ScatterChart/ScatterChart';
import Select from '~/components/Select';
import TSNEDetail from '~/components/HighDimensionalPage/TSNEDetail'; import TSNEDetail from '~/components/HighDimensionalPage/TSNEDetail';
import Title from '~/components/Title'; import Title from '~/components/Title';
import UMAPDetail from '~/components/HighDimensionalPage/UMAPDetail'; import UMAPDetail from '~/components/HighDimensionalPage/UMAPDetail';
...@@ -73,7 +77,10 @@ const AsideTitle = styled.div` ...@@ -73,7 +77,10 @@ const AsideTitle = styled.div`
margin-bottom: ${rem(20)}; margin-bottom: ${rem(20)};
`; `;
const FullWidthSelect = styled<React.FunctionComponent<SelectProps<string>>>(Select)` // 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<React.FunctionComponent<SelectProps<any>>>(Select)`
width: 100%; width: 100%;
`; `;
...@@ -176,25 +183,45 @@ const HighDimensional: FunctionComponent = () => { ...@@ -176,25 +183,45 @@ const HighDimensional: FunctionComponent = () => {
const [vectorContent, setVectorContent] = useState(''); const [vectorContent, setVectorContent] = useState('');
const [metadataContent, setMetadataContent] = useState(''); const [metadataContent, setMetadataContent] = useState('');
const [vectors, setVectors] = useState<Float32Array>(new Float32Array()); const [vectors, setVectors] = useState<Float32Array>(new Float32Array());
const [labels, setLabels] = useState<string[]>([]); const [labelList, setLabelList] = useState<LabelMetadata[]>([]);
const [labelBy, setLabelBy] = useState<string>(); const labelByList = useMemo<SelectListItem<number>[]>(
() => labelList.map(({label}, index) => ({label, value: index})),
[labelList]
);
const [labelByIndex, setLabelByIndex] = useState<number>(0);
const colorByList = useMemo<SelectListItem<number>[]>(
() => [
{
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<number>(-1);
const [metadata, setMetadata] = useState<string[][]>([]); const [metadata, setMetadata] = useState<string[][]>([]);
// dimension of data // dimension of data
const [dim, setDim] = useState<number>(0); const [dim, setDim] = useState<number>(0);
const [rawShape, setRawShape] = useState<Shape>([0, 0]); const [rawShape, setRawShape] = useState<Shape>([0, 0]);
const getLabelByLabels = useCallback( const selectedMetadata = useMemo(() => metadata[labelByIndex], [labelByIndex, metadata]);
(value: string | undefined) => { const selectedColor = useMemo(
if (value != null) { () =>
const labelIndex = labels.indexOf(value); colorByIndex === -1
if (labelIndex !== -1) { ? null
return metadata.map(row => row[labelIndex]); : {
} ...labelList[colorByIndex],
} labels: metadata[colorByIndex]
return []; },
}, [colorByIndex, labelList, metadata]
[labels, metadata]
); );
const labelByLabels = useMemo(() => getLabelByLabels(labelBy), [getLabelByLabels, labelBy]);
// dimension of display // dimension of display
const [dimension, setDimension] = useState<Dimension>('3d'); const [dimension, setDimension] = useState<Dimension>('3d');
...@@ -277,8 +304,9 @@ const HighDimensional: FunctionComponent = () => { ...@@ -277,8 +304,9 @@ const HighDimensional: FunctionComponent = () => {
setRawShape(data.rawShape); setRawShape(data.rawShape);
setDim(data.dimension); setDim(data.dimension);
setVectors(data.vectors); setVectors(data.vectors);
setLabels(data.labels); setLabelList(data.labels);
setLabelBy(data.labels[0]); setLabelByIndex(0);
setColorByIndex(-1);
setMetadata(data.metadata); setMetadata(data.metadata);
} else if (data !== null) { } else if (data !== null) {
setLoadingPhase('parsing'); setLoadingPhase('parsing');
...@@ -312,23 +340,23 @@ const HighDimensional: FunctionComponent = () => { ...@@ -312,23 +340,23 @@ const HighDimensional: FunctionComponent = () => {
}, []); }, []);
const [searchResult, setSearchResult] = useState<Parameters<NonNullable<LabelSearchInputProps['onChange']>>['0']>({ const [searchResult, setSearchResult] = useState<Parameters<NonNullable<LabelSearchInputProps['onChange']>>['0']>({
labelBy: undefined, labelIndex: undefined,
value: '' value: ''
}); });
const searchedResult = useMemo(() => { const searchedResult = useMemo(() => {
if (searchResult.labelBy == null || searchResult.value === '') { if (searchResult.labelIndex == null || searchResult.value === '') {
return { return {
indices: [], indices: [],
metadata: [] metadata: []
}; };
} }
const labelByLabels = getLabelByLabels(searchResult.labelBy); const metadataList = metadata[searchResult.labelIndex];
const metadataResult: string[] = []; const metadataResult: string[] = [];
const vectorsIndices: number[] = []; const vectorsIndices: number[] = [];
for (let i = 0; i < labelByLabels.length; i++) { for (let i = 0; i < metadataList.length; i++) {
if (labelByLabels[i].includes(searchResult.value)) { if (metadataList[i].includes(searchResult.value)) {
metadataResult.push(labelByLabels[i]); metadataResult.push(metadataList[i]);
vectorsIndices.push(i); vectorsIndices.push(i);
} }
} }
...@@ -340,7 +368,7 @@ const HighDimensional: FunctionComponent = () => { ...@@ -340,7 +368,7 @@ const HighDimensional: FunctionComponent = () => {
indices: vectorsIndices, indices: vectorsIndices,
metadata: metadataResult metadata: metadataResult
}; };
}, [getLabelByLabels, searchResult.labelBy, searchResult.value]); }, [metadata, searchResult.labelIndex, searchResult.value]);
const [hoveredIndices, setHoveredIndices] = useState<number[]>([]); const [hoveredIndices, setHoveredIndices] = useState<number[]>([]);
const hoverSearchResult = useCallback( const hoverSearchResult = useCallback(
...@@ -393,11 +421,11 @@ const HighDimensional: FunctionComponent = () => { ...@@ -393,11 +421,11 @@ const HighDimensional: FunctionComponent = () => {
/> />
</Field> </Field>
<Field label={t('high-dimensional:select-label')}> <Field label={t('high-dimensional:select-label')}>
<FullWidthSelect list={labels} value={labelBy} onChange={setLabelBy} /> <FullWidthSelect list={labelByList} value={labelByIndex} onChange={setLabelByIndex} />
</Field>
<Field label={t('high-dimensional:select-color')}>
<FullWidthSelect list={colorByList} value={colorByIndex} onChange={setColorByIndex} />
</Field> </Field>
{/* <Field label={t('high-dimensional:select-color')}>
<FullWidthSelect />
</Field> */}
<Field> <Field>
<FullWidthButton rounded outline type="primary" onClick={() => setUploadModal(true)}> <FullWidthButton rounded outline type="primary" onClick={() => setUploadModal(true)}>
{t('high-dimensional:upload-data')} {t('high-dimensional:upload-data')}
...@@ -424,7 +452,19 @@ const HighDimensional: FunctionComponent = () => { ...@@ -424,7 +452,19 @@ const HighDimensional: FunctionComponent = () => {
</AsideSection> </AsideSection>
</RightAside> </RightAside>
), ),
[t, dataPath, reduction, dimension, labels, labelBy, embeddingList, selectedEmbeddingName, detail] [
t,
embeddingList,
selectedEmbeddingName,
labelByList,
labelByIndex,
colorByList,
colorByIndex,
dataPath,
reduction,
dimension,
detail
]
); );
const leftAside = useMemo( const leftAside = useMemo(
...@@ -432,7 +472,7 @@ const HighDimensional: FunctionComponent = () => { ...@@ -432,7 +472,7 @@ const HighDimensional: FunctionComponent = () => {
<LeftAside> <LeftAside>
<AsideSection> <AsideSection>
<Field> <Field>
<LabelSearchInput labels={labels} onChange={setSearchResult} /> <LabelSearchInput labels={labelByList} onChange={setSearchResult} />
</Field> </Field>
{searchResult.value !== '' && ( {searchResult.value !== '' && (
<Field className="secondary"> <Field className="secondary">
...@@ -451,7 +491,7 @@ const HighDimensional: FunctionComponent = () => { ...@@ -451,7 +491,7 @@ const HighDimensional: FunctionComponent = () => {
</AsideSection> </AsideSection>
</LeftAside> </LeftAside>
), ),
[hoverSearchResult, labels, searchResult.value, searchedResult.metadata, t] [hoverSearchResult, labelByList, searchResult.value, searchedResult.metadata, t]
); );
return ( return (
...@@ -463,7 +503,8 @@ const HighDimensional: FunctionComponent = () => { ...@@ -463,7 +503,8 @@ const HighDimensional: FunctionComponent = () => {
<HighDimensionalChart <HighDimensionalChart
ref={chart} ref={chart}
vectors={vectors} vectors={vectors}
labels={labelByLabels} labels={selectedMetadata}
colorMap={selectedColor}
shape={rawShape} shape={rawShape}
dim={dim} dim={dim}
is3D={is3D} is3D={is3D}
......
...@@ -18,6 +18,7 @@ export type { ...@@ -18,6 +18,7 @@ export type {
CalculateParams, CalculateParams,
CalculateResult, CalculateResult,
Dimension, Dimension,
LabelMetadata,
ParseParams, ParseParams,
ParseResult, ParseResult,
PCAParams, PCAParams,
...@@ -31,6 +32,8 @@ export type { ...@@ -31,6 +32,8 @@ export type {
Vectors Vectors
} from './types'; } from './types';
export {LabelType} from './types';
export {parseFromBlob, parseFromString, ParserError} from './parser'; export {parseFromBlob, parseFromString, ParserError} from './parser';
export {default as PCA} from './pca'; export {default as PCA} from './pca';
......
...@@ -15,6 +15,8 @@ ...@@ -15,6 +15,8 @@
*/ */
import type { import type {
LabelColor,
LabelMetadata,
MetadataResult, MetadataResult,
ParseFromBlobParams, ParseFromBlobParams,
ParseFromStringParams, ParseFromStringParams,
...@@ -23,7 +25,9 @@ import type { ...@@ -23,7 +25,9 @@ import type {
VectorResult VectorResult
} from './types'; } from './types';
import {LabelType} from './types';
import {safeSplit} from '~/utils'; import {safeSplit} from '~/utils';
import uniq from 'lodash/uniq';
const INDEX_METADATA_FIELD = '__index__'; const INDEX_METADATA_FIELD = '__index__';
const DEFAULT_METADATA_FIELD = '__metadata__'; const DEFAULT_METADATA_FIELD = '__metadata__';
...@@ -118,6 +122,44 @@ function shuffle(data: ParseResult, maxCount?: number, maxDimension?: number): P ...@@ -118,6 +122,44 @@ function shuffle(data: ParseResult, maxCount?: number, maxDimension?: number): P
}; };
} }
function rowOfMatrix<T>(matrix: T[][], row: number): T[] {
return matrix.map(m => m[row]);
}
function transposeMatrix<T>(matrix: T[][]): T[][] {
if (!matrix.length) {
return [];
}
const result: T[][] = [];
const dimension = matrix[0].length;
for (let i = 0; i < dimension; i++) {
result.push(rowOfMatrix(matrix, i));
}
return result;
}
function getLabelColor(labels: string[]): LabelColor {
const values = labels.map(Number.parseFloat);
const type = values.every(Number.isFinite) ? LabelType.Value : LabelType.Category;
switch (type) {
case LabelType.Value: {
return {
type,
minValue: Math.min(...values),
maxValue: Math.max(...values)
};
}
case LabelType.Category: {
return {
type,
categories: uniq(labels)
};
}
default:
return null as never;
}
}
function parseVectors(str: string): VectorResult { function parseVectors(str: string): VectorResult {
if (!str) { if (!str) {
throw new ParserError('Tenser file is empty', ParserError.CODES.TENSER_EMPTY); throw new ParserError('Tenser file is empty', ParserError.CODES.TENSER_EMPTY);
...@@ -143,11 +185,22 @@ function parseMetadata(str: string): MetadataResult { ...@@ -143,11 +185,22 @@ function parseMetadata(str: string): MetadataResult {
let metadata = split(str); let metadata = split(str);
// dimension is larger then 0 // dimension is larger then 0
const dimension = metadata[0].length; const dimension = metadata[0].length;
let labels = [DEFAULT_METADATA_FIELD];
metadata = alignItems(metadata, dimension, ''); metadata = alignItems(metadata, dimension, '');
let labels: LabelMetadata[] = [];
if (dimension > 1) { if (dimension > 1) {
// metadata is larger then 1 // metadata is larger then 1
labels = metadata.shift() as string[]; const labelNames = metadata.shift() ?? [];
labels = labelNames.map((label, i) => ({
label,
...getLabelColor(rowOfMatrix(metadata, i))
}));
} else {
labels = [
{
label: DEFAULT_METADATA_FIELD,
...getLabelColor(rowOfMatrix(metadata, 0))
}
];
} }
return { return {
dimension, dimension,
...@@ -156,7 +209,7 @@ function parseMetadata(str: string): MetadataResult { ...@@ -156,7 +209,7 @@ function parseMetadata(str: string): MetadataResult {
}; };
} }
function genMetadataAndLabels(metadata: string, count: number) { function genMetadataAndLabels(metadata: string, count: number): Pick<MetadataResult, 'labels' | 'metadata'> {
if (metadata) { if (metadata) {
const data = parseMetadata(metadata); const data = parseMetadata(metadata);
const metadataCount = data.metadata.length; const metadataCount = data.metadata.length;
...@@ -176,7 +229,12 @@ function genMetadataAndLabels(metadata: string, count: number) { ...@@ -176,7 +229,12 @@ function genMetadataAndLabels(metadata: string, count: number) {
}; };
} }
return { return {
labels: [INDEX_METADATA_FIELD], labels: [
{
label: INDEX_METADATA_FIELD,
type: LabelType.Null
}
],
metadata: Array.from({length: count}, (_, i) => [`${i}`]) metadata: Array.from({length: count}, (_, i) => [`${i}`])
}; };
} }
...@@ -194,7 +252,11 @@ export function parseFromString({vectors: v, metadata: m, maxCount, maxDimension ...@@ -194,7 +252,11 @@ export function parseFromString({vectors: v, metadata: m, maxCount, maxDimension
Object.assign(result, parseVectors(v)); Object.assign(result, parseVectors(v));
Object.assign(result, genMetadataAndLabels(m, result.count)); Object.assign(result, genMetadataAndLabels(m, result.count));
} }
return shuffle(result, maxCount, maxDimension); const {metadata, ...others} = shuffle(result, maxCount, maxDimension);
return {
...others,
metadata: transposeMatrix(metadata)
};
} }
export async function parseFromBlob({ export async function parseFromBlob({
...@@ -209,7 +271,7 @@ export async function parseFromBlob({ ...@@ -209,7 +271,7 @@ export async function parseFromBlob({
if (count * dimension !== vectors.length) { if (count * dimension !== vectors.length) {
throw new ParserError('Size of tensor does not match.', ParserError.CODES.SHAPE_MISMATCH); throw new ParserError('Size of tensor does not match.', ParserError.CODES.SHAPE_MISMATCH);
} }
return shuffle( const {metadata, ...others} = shuffle(
{ {
rawShape: shape, rawShape: shape,
count, count,
...@@ -220,4 +282,8 @@ export async function parseFromBlob({ ...@@ -220,4 +282,8 @@ export async function parseFromBlob({
maxCount, maxCount,
maxDimension maxDimension
); );
return {
...others,
metadata: transposeMatrix(metadata)
};
} }
...@@ -14,6 +14,12 @@ ...@@ -14,6 +14,12 @@
* limitations under the License. * limitations under the License.
*/ */
import type {HighDimensionalColorMap as LabelColor} from '~/types';
import {HighDimensionalColorType as LabelType} from '~/types';
export {LabelType};
export type {LabelColor};
export type Dimension = '2d' | '3d'; export type Dimension = '2d' | '3d';
export type Reduction = 'pca' | 'tsne' | 'umap'; export type Reduction = 'pca' | 'tsne' | 'umap';
...@@ -21,6 +27,8 @@ export type Vectors = [number, number, number][]; ...@@ -21,6 +27,8 @@ export type Vectors = [number, number, number][];
export type Shape = [number, number]; export type Shape = [number, number];
export type LabelMetadata = {label: string} & LabelColor;
export type VectorResult = { export type VectorResult = {
rawShape: Shape; rawShape: Shape;
dimension: number; dimension: number;
...@@ -30,7 +38,7 @@ export type VectorResult = { ...@@ -30,7 +38,7 @@ export type VectorResult = {
export type MetadataResult = { export type MetadataResult = {
dimension: number; dimension: number;
labels: string[]; labels: LabelMetadata[];
metadata: string[][]; metadata: string[][];
}; };
...@@ -60,13 +68,10 @@ export type ParseParams = ...@@ -60,13 +68,10 @@ export type ParseParams =
} }
| null; | null;
export type ParseResult = { export type ParseResult = MetadataResult & {
rawShape: Shape; rawShape: Shape;
count: number; count: number;
dimension: number;
vectors: Float32Array; vectors: Float32Array;
labels: string[];
metadata: string[][];
}; };
export type PCAParams = { export type PCAParams = {
......
...@@ -39,3 +39,26 @@ export enum TimeMode { ...@@ -39,3 +39,26 @@ export enum TimeMode {
Relative = 'relative', Relative = 'relative',
WallTime = 'wall' WallTime = 'wall'
} }
export type Point2D = [number, number];
export type Point3D = [number, number, number];
export enum HighDimensionalColorType {
Null,
Value,
Category
}
export type HighDimensionalColorMap =
| {
type: HighDimensionalColorType.Null;
}
| {
type: HighDimensionalColorType.Value;
minValue: number;
maxValue: number;
}
| {
type: HighDimensionalColorType.Category;
categories: string[];
};
...@@ -36,7 +36,7 @@ export const color = [ ...@@ -36,7 +36,7 @@ export const color = [
'#FF6600', '#FF6600',
'#FFEA00', '#FFEA00',
'#FE4A3B' '#FE4A3B'
]; ] as const;
export const colorAlt = [ export const colorAlt = [
'#9498F0', '#9498F0',
'#66E0B8', '#66E0B8',
...@@ -56,7 +56,7 @@ export const colorAlt = [ ...@@ -56,7 +56,7 @@ export const colorAlt = [
'#FFB27F', '#FFB27F',
'#FFF266', '#FFF266',
'#FE9289' '#FE9289'
]; ] as const;
export const title = { export const title = {
textStyle: { textStyle: {
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册