diff --git a/package.json b/package.json index bd68a1a70..0572b88b7 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "visyn_core", "description": "Core repository for datavisyn applications.", - "version": "8.0.1", + "version": "8.1.0", "author": { "name": "datavisyn GmbH", "email": "contact@datavisyn.io", diff --git a/src/demo/MainApp.tsx b/src/demo/MainApp.tsx index fbcf6bbac..fe6e8e437 100644 --- a/src/demo/MainApp.tsx +++ b/src/demo/MainApp.tsx @@ -3,7 +3,7 @@ import * as React from 'react'; import { VisynApp, VisynHeader, useVisynAppContext } from '../app'; import { DatavisynTaggle, VisynRanking, autosizeWithSMILESColumn } from '../ranking'; import { defaultBuilder } from '../ranking/EagerVisynRanking'; -import { BaseVisConfig, ENumericalColorScaleType, EScatterSelectSettings, ESupportedPlotlyVis, IScatterConfig, Vis } from '../vis'; +import { BaseVisConfig, ELabelingOptions, ENumericalColorScaleType, EScatterSelectSettings, ESupportedPlotlyVis, IScatterConfig, Vis } from '../vis'; import { iris } from '../vis/stories/irisData'; import { MyNumberScore, MySMILESScore, MyStringScore } from './scoresUtils'; import { fetchIrisData } from '../vis/stories/fetchIrisData'; @@ -34,6 +34,7 @@ export function MainApp() { dragMode: EScatterSelectSettings.RECTANGLE, alphaSliderVal: 1, sizeSliderVal: 5, + showLabels: ELabelingOptions.SELECTED, } as IScatterConfig); const columns = React.useMemo(() => (user ? fetchIrisData() : []), [user]); const [selection, setSelection] = React.useState([]); diff --git a/src/vis/scatter/LabelingOptions.tsx b/src/vis/scatter/LabelingOptions.tsx new file mode 100644 index 000000000..2d63c9297 --- /dev/null +++ b/src/vis/scatter/LabelingOptions.tsx @@ -0,0 +1,26 @@ +import { Input, SegmentedControl } from '@mantine/core'; +import * as React from 'react'; +import { ELabelingOptions } from './interfaces'; + +interface LabelingOptionsProps { + callback: (s: ELabelingOptions) => void; + currentSelected: ELabelingOptions | null; +} + +export function LabelingOptions({ callback, currentSelected }: LabelingOptionsProps) { + return ( + + + + ); +} diff --git a/src/vis/scatter/ScatterVis.tsx b/src/vis/scatter/ScatterVis.tsx index 442faf449..d14b94d3c 100644 --- a/src/vis/scatter/ScatterVis.tsx +++ b/src/vis/scatter/ScatterVis.tsx @@ -11,7 +11,7 @@ import { beautifyLayout } from '../general/layoutUtils'; import { EScatterSelectSettings, ICommonVisProps } from '../interfaces'; import { BrushOptionButtons } from '../sidebar/BrushOptionButtons'; import { createScatterTraces } from './utils'; -import { IScatterConfig } from './interfaces'; +import { ELabelingOptions, IScatterConfig } from './interfaces'; export function ScatterVis({ config, @@ -55,6 +55,7 @@ export function ScatterVis({ config.numColorScaleType, scales, shapes, + config.showLabels, ]); React.useEffect(() => { @@ -108,6 +109,17 @@ export function ScatterVis({ p.data.selectedpoints = temp; + if (selectedList.length === 0 && config.showLabels === ELabelingOptions.SELECTED) { + // @ts-ignore + p.data.selected.textfont.color = 'white'; + } else if (selectedList.length === 0 && config.showLabels === ELabelingOptions.ALWAYS) { + // @ts-ignore + p.data.selected.textfont.color = `rgba(102, 102, 102, ${config.alphaSliderVal})`; + } else { + // @ts-ignore + p.data.selected.textfont.color = `rgba(102, 102, 102, 1)`; + } + if (selectedList.length === 0 && config.color) { // @ts-ignore p.data.selected.marker.opacity = config.alphaSliderVal; @@ -121,7 +133,7 @@ export function ScatterVis({ } return []; - }, [selectedMap, traces, selectedList, config.color, config.alphaSliderVal]); + }, [traces, selectedList.length, config.showLabels, config.color, config.alphaSliderVal, selectedMap]); const plotlyData = useMemo(() => { if (traces) { diff --git a/src/vis/scatter/ScatterVisSidebar.tsx b/src/vis/scatter/ScatterVisSidebar.tsx index 564991cac..b99a98761 100644 --- a/src/vis/scatter/ScatterVisSidebar.tsx +++ b/src/vis/scatter/ScatterVisSidebar.tsx @@ -7,7 +7,8 @@ import { MultiSelect } from '../sidebar/MultiSelect'; import { SingleSelect } from '../sidebar/SingleSelect'; import { ColorSelect } from './ColorSelect'; import { OpacitySlider } from './OpacitySlider'; -import { IScatterConfig } from './interfaces'; +import { ELabelingOptions, IScatterConfig } from './interfaces'; +import { LabelingOptions } from './LabelingOptions'; const defaultConfig = { color: { @@ -22,6 +23,10 @@ const defaultConfig = { enable: true, customComponent: null, }, + labels: { + enable: true, + customComponent: null, + }, }; export function ScatterVisSidebar({ config, optionsConfig, columns, filterCallback, setConfig }: ICommonVisSideBarProps) { @@ -68,6 +73,18 @@ export function ScatterVisSidebar({ config, optionsConfig, columns, filterCallba }} currentValue={config.alphaSliderVal} /> + {mergedOptionsConfig.labels.enable + ? mergedOptionsConfig.labels.customComponent || ( + { + if (config.showLabels !== showLabels) { + setConfig({ ...config, showLabels }); + } + }} + currentSelected={config.showLabels} + /> + ) + : null} {filterCallback && mergedOptionsConfig.filter.enable ? mergedOptionsConfig.filter.customComponent || : null} diff --git a/src/vis/scatter/interfaces.ts b/src/vis/scatter/interfaces.ts index 7f495fe55..5530209ac 100644 --- a/src/vis/scatter/interfaces.ts +++ b/src/vis/scatter/interfaces.ts @@ -9,8 +9,15 @@ export interface IScatterConfig extends BaseVisConfig { dragMode: EScatterSelectSettings; alphaSliderVal: number; sizeSliderVal: number; + showLabels: ELabelingOptions; } export function isScatterConfig(s: BaseVisConfig): s is IScatterConfig { return s.type === ESupportedPlotlyVis.SCATTER; } + +export enum ELabelingOptions { + NEVER = 'Never', + ALWAYS = 'Always', + SELECTED = 'Selected', +} diff --git a/src/vis/scatter/utils.ts b/src/vis/scatter/utils.ts index 7c86981d4..27165983e 100644 --- a/src/vis/scatter/utils.ts +++ b/src/vis/scatter/utils.ts @@ -19,7 +19,7 @@ import { VisNumericalValue, } from '../interfaces'; import { getCol } from '../sidebar'; -import { IScatterConfig } from './interfaces'; +import { ELabelingOptions, IScatterConfig } from './interfaces'; function calculateDomain(domain: [number | undefined, number | undefined], vals: number[]): [number, number] { if (!domain) return null; @@ -43,6 +43,7 @@ const defaultConfig: IScatterConfig = { dragMode: EScatterSelectSettings.RECTANGLE, alphaSliderVal: 0.5, sizeSliderVal: 8, + showLabels: ELabelingOptions.SELECTED, }; export function scatterMergeDefaultConfig(columns: VisColumn[], config: IScatterConfig): IScatterConfig { @@ -86,6 +87,7 @@ export async function createScatterTraces( colorScaleType: ENumericalColorScaleType, scales: Scales, shapes: string[] | null, + showLabels: ELabelingOptions, ): Promise { let plotCounter = 1; @@ -112,6 +114,7 @@ export async function createScatterTraces( const colorCol = await resolveSingleColumn(getCol(columns, color)); const idToLabelMapper = await createIdToLabelMapper(columns); + const textPositionOptions = ['top center', 'bottom center']; const shapeScale = shape ? d3v7 .scaleOrdinal() @@ -127,6 +130,7 @@ export async function createScatterTraces( max = d3v7.max(colorCol.resolvedValues.map((v) => +v.val).filter((v) => v !== null)); } + const textPositions = ['top center', 'bottom center']; const numericalColorScale = color ? d3v7 .scaleLinear() @@ -162,7 +166,7 @@ export async function createScatterTraces( xaxis: plotCounter === 1 ? 'x' : `x${plotCounter}`, yaxis: plotCounter === 1 ? 'y' : `y${plotCounter}`, type: 'scattergl', - mode: 'markers', + mode: showLabels === ELabelingOptions.NEVER ? 'markers' : 'text+markers', showlegend: false, hoverlabel: { bgcolor: 'black', @@ -175,7 +179,8 @@ export async function createScatterTraces( ), hoverinfo: 'text', text: validCols[0].resolvedValues.map((v) => v.id.toString()), - + // @ts-ignore + textposition: validCols[0].resolvedValues.map((v, i) => textPositionOptions[i % textPositionOptions.length]), marker: { symbol: shapeCol ? shapeCol.resolvedValues.map((v) => shapeScale(v.val as string)) : 'circle', @@ -195,6 +200,9 @@ export async function createScatterTraces( opacity: 1, size: sizeSliderVal, }, + textfont: { + color: showLabels === ELabelingOptions.NEVER ? 'white' : '#666666', + }, }, unselected: { marker: { @@ -205,6 +213,9 @@ export async function createScatterTraces( opacity: alphaSliderVal, size: sizeSliderVal, }, + textfont: { + color: showLabels === ELabelingOptions.ALWAYS ? `rgba(179, 179, 179, ${alphaSliderVal})` : 'white', + }, }, }, xLabel: columnNameWithDescription(validCols[0].info), @@ -252,7 +263,7 @@ export async function createScatterTraces( xaxis: plotCounter === 1 ? 'x' : `x${plotCounter}`, yaxis: plotCounter === 1 ? 'y' : `y${plotCounter}`, type: 'scattergl', - mode: 'markers', + mode: showLabels === ELabelingOptions.NEVER ? 'markers' : 'text+markers', hovertext: xCurr.resolvedValues.map( (v, i) => `${v.id}
x: ${v.val}
y: ${yCurr.resolvedValues[i].val}
${ @@ -265,6 +276,8 @@ export async function createScatterTraces( }, showlegend: false, text: validCols[0].resolvedValues.map((v) => v.id.toString()), + // @ts-ignore + textposition: validCols[0].resolvedValues.map((v, i) => (i % textPositions.length === 0 ? 'top center' : 'bottom center')), marker: { color: colorCol ? colorCol.resolvedValues.map((v) =>