未验证 提交 0eca8a87 编写于 作者: N Niandalu 提交者: GitHub

feat: high-dimensional features without zoom/refit (#576)

* feat: high-dimensional features without zoom/refit

* fix: mute eslint
上级 b2b2eb41
......@@ -29,7 +29,9 @@ const LineChart: FunctionComponent<LineChartProps & WithStyled> = ({
loading,
className
}) => {
const [ref, echart] = useECharts<HTMLDivElement>(!!loading);
const [ref, echart] = useECharts<HTMLDivElement>({
loading: !!loading
});
const xAxisFormatter = useCallback(
(value: number) => (type === 'time' ? new Date(value).toLocaleTimeString() : value),
......
import React, {FunctionComponent, useEffect} from 'react';
import React, {FunctionComponent, useEffect, useMemo} from 'react';
import {EChartOption} from 'echarts';
import {WithStyled, primaryColor} from '~/utils/style';
import useECharts from '~/hooks/useECharts';
import {Dimension} from '~/types';
const SYMBOL_SIZE = 12;
type Point = {
name: string;
value: [number, number] | [number, number, number];
};
type ScatterChartProps = {
data?: ([number, number] | [number, number, number])[];
labels?: string[];
loading?: boolean;
dimension?: Dimension;
keyword: string;
loading: boolean;
points: Point[];
dimension: Dimension;
};
const assemble2D = (points: {highlighted: Point[]; others: Point[]}, label: EChartOption.SeriesBar['label']) => {
// eslint-disable-next-line
const createSeries = (name: string, data: Point[], patch?: {[k: string]: any}) => ({
name,
symbolSize: SYMBOL_SIZE,
data,
type: 'scatter',
label,
...(patch || {})
});
return {
xAxis: {},
yAxis: {},
toolbox: {
show: true,
showTitle: false,
itemSize: 0,
feature: {
dataZoom: {},
restore: {},
saveAsImage: {}
}
},
series: [
createSeries('highlighted', points.highlighted),
createSeries('others', points.others, {color: primaryColor})
]
};
};
const assemble3D = (points: {highlighted: Point[]; others: Point[]}, label: EChartOption.SeriesBar['label']) => {
// eslint-disable-next-line
const createSeries = (name: string, data: Point[], patch?: {[k: string]: any}) => ({
name,
symbolSize: SYMBOL_SIZE,
data,
type: 'scatter3D',
label,
...(patch || {})
});
return {
grid3D: {},
xAxis3D: {},
yAxis3D: {},
zAxis3D: {},
series: [
createSeries('highlighted', points.highlighted),
createSeries('others', points.others, {color: primaryColor})
]
};
};
const getChartOptions = (
settings: Pick<ScatterChartProps, 'dimension'> & {points: {highlighted: Point[]; others: Point[]}}
) => {
const {dimension, points} = settings;
const label = {
show: true,
position: 'top',
formatter: (params: {data: {name: string; showing: boolean}}) => (params.data.showing ? params.data.name : '')
};
const assemble = dimension === '2d' ? assemble2D : assemble3D;
return assemble(points, label);
};
const dividePoints = (points: Point[], keyword: string): [Point[], Point[]] => {
if (!keyword) {
return [[], points];
}
const matched: Point[] = [];
const missing: Point[] = [];
points.forEach(point => {
if (point.name.includes(keyword)) {
matched.push(point);
return;
}
missing.push(point);
});
return [matched, missing];
};
const ScatterChart: FunctionComponent<ScatterChartProps & WithStyled> = ({
data,
labels,
points,
keyword,
loading,
dimension,
className
}) => {
const [ref, echart] = useECharts<HTMLDivElement>(!!loading);
const [ref, echart] = useECharts<HTMLDivElement>({
loading,
gl: true
});
const [highlighted, others] = useMemo(() => dividePoints(points, keyword), [points, keyword]);
const chartOptions = useMemo(
() =>
getChartOptions({
dimension,
points: {
highlighted,
others
}
}),
[dimension, highlighted, others]
);
useEffect(() => {
if (process.browser) {
(async () => {
const is3D = dimension === '3d';
if (is3D) {
await import('echarts-gl');
}
echart.current?.setOption(
{
...(is3D
? {
yAxis3D: {},
xAxis3D: {},
zAxis3D: {},
grid3D: {}
}
: {
xAxis: {},
yAxis: {}
}),
series: [
{
data,
label: {
show: true,
position: 'top',
formatter: (
params: EChartOption.Tooltip.Format | EChartOption.Tooltip.Format[]
) => {
if (!labels) {
return '';
}
const {dataIndex: index} = Array.isArray(params) ? params[0] : params;
if (index == null) {
return '';
}
return labels[index] ?? '';
}
},
symbolSize: 12,
itemStyle: {
color: primaryColor
},
type: is3D ? 'scatter3D' : 'scatter'
}
]
},
{notMerge: true}
);
})();
if (!process.browser) {
return;
}
}, [data, labels, dimension, echart]);
echart.current?.setOption(
chartOptions,
true // not merged
);
}, [chartOptions, echart]);
return <div className={className} ref={ref}></div>;
};
......
......@@ -2,16 +2,20 @@ import {useRef, useEffect, useCallback, MutableRefObject} from 'react';
import echarts, {ECharts} from 'echarts';
import {useTranslation} from 'react-i18next';
const useECharts = <T extends HTMLElement>(
loading: boolean
): [MutableRefObject<T | null>, MutableRefObject<ECharts | null>] => {
const useECharts = <T extends HTMLElement>(options: {
loading: boolean;
gl?: boolean;
}): [MutableRefObject<T | null>, MutableRefObject<ECharts | null>] => {
const {t} = useTranslation('common');
const ref = useRef(null);
const echart = useRef(null as ECharts | null);
const createChart = useCallback(() => {
echart.current = echarts.init((ref.current as unknown) as HTMLDivElement);
}, []);
const loadExtension = options.gl ? import('echarts-gl') : Promise.resolve();
loadExtension.then(() => {
echart.current = echarts.init((ref.current as unknown) as HTMLDivElement);
});
}, [options.gl]);
const destroyChart = useCallback(() => {
echart.current?.dispose();
......@@ -26,7 +30,7 @@ const useECharts = <T extends HTMLElement>(
useEffect(() => {
if (process.browser) {
if (loading) {
if (options.loading) {
echart.current?.showLoading('default', {
text: t('loading'),
color: '#c23531',
......@@ -38,7 +42,7 @@ const useECharts = <T extends HTMLElement>(
echart.current?.hideLoading();
}
}
}, [t, loading]);
}, [t, options.loading]);
return [ref, echart];
};
......
import React, {useState, useEffect} from 'react';
import React, {useState, useEffect, useMemo} from 'react';
import styled from 'styled-components';
import useSWR from 'swr';
import {NextPage} from 'next';
......@@ -58,6 +58,7 @@ const HighDimensional: NextPage = () => {
const [dimension, setDimension] = useState(dimensions[0] as Dimension);
const [reduction, setReduction] = useState(reductions[0]);
const [running, setRunning] = useState(true);
const [labelVisibility, setLabelVisibility] = useState(true);
const {data, error} = useSWR<Data>(
`/embeddings/embeddings?run=${encodeURIComponent(run ?? '')}&dimension=${Number.parseInt(
......@@ -67,6 +68,21 @@ const HighDimensional: NextPage = () => {
refreshInterval: running ? 15 * 1000 : 0
}
);
const points = useMemo(() => {
if (!data) {
return [];
}
const {embedding, labels} = data;
return embedding.map((value, i) => {
const name = labels[i] || '';
return {
name,
showing: labelVisibility,
value
};
});
}, [data, labelVisibility]);
const aside = (
<section>
......@@ -81,7 +97,9 @@ const HighDimensional: NextPage = () => {
<SearchInput placeholder={t('common:search')} value={search} onChange={setSearch} />
</Field>
<Field>
<Checkbox>{t('display-all-label')}</Checkbox>
<Checkbox value={labelVisibility} onChange={setLabelVisibility}>
{t('display-all-label')}
</Checkbox>
</Field>
<AsideDivider />
<AsideTitle>
......@@ -119,12 +137,7 @@ const HighDimensional: NextPage = () => {
<>
<Title>{t('common:high-dimensional')}</Title>
<Content aside={aside}>
<StyledScatterChart
data={data?.embedding}
labels={data?.labels}
dimension={dimension}
loading={!data && !error}
/>
<StyledScatterChart points={points} dimension={dimension} loading={!data && !error} keyword={search} />
</Content>
</>
);
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册