Skip to content

Commit

Permalink
feat(vis-type: scatter): Add log scale option for axes (#602)
Browse files Browse the repository at this point in the history
Co-authored-by: Holger Stitz <[email protected]>
  • Loading branch information
dvmoritzschoefl and thinkh authored Nov 5, 2024
1 parent bf95ceb commit f2d6b71
Show file tree
Hide file tree
Showing 9 changed files with 91 additions and 31 deletions.
13 changes: 7 additions & 6 deletions src/vis/general/utils.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,19 @@
import { NAN_REPLACEMENT, VIS_NEUTRAL_COLOR } from './constants';

const formatter = new Intl.NumberFormat('en-US', {
maximumFractionDigits: 4,
maximumSignificantDigits: 4,
notation: 'compact',
compactDisplay: 'short',
});

/**
*
* @param label the label to check for undefined, null or empty
* @param unknownLabel default: NAN_REPLACEMENT; the label to return if the input label is undefined, null or empty
* @returns the label if it is not undefined, null or empty, otherwise NAN_REPLACEMENT (Unknown)
*/
export function getLabelOrUnknown(label: string | number | null | undefined, unknownLabel: string = NAN_REPLACEMENT): string {
const formatter = new Intl.NumberFormat('en-US', {
maximumFractionDigits: 4,
maximumSignificantDigits: 4,
notation: 'compact',
compactDisplay: 'short',
});
return label === null || label === 'null' || label === undefined || label === 'undefined' || label === ''
? unknownLabel
: Number(label) && !Number.isInteger(label) // if it is a number, but not an integer, apply NumberFormat
Expand Down
8 changes: 7 additions & 1 deletion src/vis/scatter/ScatterVis.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,13 @@ export function ScatterVis({
// If the useAsync arguments change, clear the internal layout state.
// Why not just use the config to compare things?
// Because the useAsync takes one render cycle to update the value, and inbetween that, plotly has already updated the internalLayoutRef again with the old one.
if (args?.[1] !== previousArgs.current?.[1] || args?.[5] !== previousArgs.current?.[5]) {
if (
args?.[1] !== previousArgs.current?.[1] ||
args?.[6] !== previousArgs.current?.[6] ||
args?.[3] !== previousArgs.current?.[3] ||
config?.xAxisScale !== internalLayoutRef.current?.xaxis?.type ||
config?.yAxisScale !== internalLayoutRef.current?.yaxis?.type
) {
internalLayoutRef.current = {};
previousArgs.current = args;
}
Expand Down
29 changes: 28 additions & 1 deletion src/vis/scatter/ScatterVisSidebar.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Divider } from '@mantine/core';
import { Divider, Select } from '@mantine/core';
import merge from 'lodash/merge';
import * as React from 'react';
import { useMemo } from 'react';
Expand Down Expand Up @@ -131,6 +131,33 @@ export function ScatterVisSidebar({ config, optionsConfig, columns, filterCallba
</>
)
: null}

<Select
label="X-axis scale"
data={[
{ value: 'linear', label: 'Linear' },
{ value: 'log', label: 'Logarithmic' },
]}
clearable={false}
value={config.xAxisScale}
onChange={(value) => {
setConfig({ ...config, xAxisScale: value as 'log' | 'linear' });
}}
/>

<Select
label="Y-axis scale"
data={[
{ value: 'linear', label: 'Linear' },
{ value: 'log', label: 'Logarithmic' },
]}
clearable={false}
value={config.yAxisScale}
onChange={(value) => {
setConfig({ ...config, yAxisScale: value as 'log' | 'linear' });
}}
/>

{filterCallback && mergedOptionsConfig.filter.enable
? mergedOptionsConfig.filter.customComponent || (
<>
Expand Down
10 changes: 10 additions & 0 deletions src/vis/scatter/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,16 @@ export interface IScatterConfig extends BaseVisConfig {
regressionLineOptions?: IRegressionLineOptions;
showLegend?: boolean;
labelColumns?: ColumnInfo[];

/**
* Scale type of the x-axis (linear or log)
*/
xAxisScale?: 'linear' | 'log';

/**
* Scale type of the y-axis (linear or log)
*/
yAxisScale?: 'linear' | 'log';
}

/**
Expand Down
6 changes: 3 additions & 3 deletions src/vis/scatter/useData.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ export function useData({
yaxis: pair.yref,
textposition: subplots.text.map((_, i) => textPositionOptions[i % textPositionOptions.length]),
...(isEmpty(selectedList) ? {} : { selectedpoints: selectedList.map((idx) => subplots.idToIndex.get(idx)) }),
mode: config.showLabels === ELabelingOptions.NEVER ? 'markers' : 'text+markers',
mode: config.showLabels === ELabelingOptions.NEVER || config.xAxisScale === 'log' || config.yAxisScale === 'log' ? 'markers' : 'text+markers',
...(config.showLabels === ELabelingOptions.NEVER
? {}
: config.showLabels === ELabelingOptions.ALWAYS
Expand Down Expand Up @@ -118,7 +118,7 @@ export function useData({
// text: scatter.plotlyData.text,
textposition: scatter.plotlyData.text.map((_, i) => textPositionOptions[i % textPositionOptions.length]),
...(isEmpty(selectedList) ? {} : { selectedpoints: selectedList.map((idx) => scatter.idToIndex.get(idx)) }),
mode: config.showLabels === ELabelingOptions.NEVER ? 'markers' : 'text+markers',
mode: config.showLabels === ELabelingOptions.NEVER || config.xAxisScale === 'log' || config.yAxisScale === 'log' ? 'markers' : 'text+markers',
...(config.showLabels === ELabelingOptions.NEVER
? {}
: config.showLabels === ELabelingOptions.ALWAYS
Expand Down Expand Up @@ -162,7 +162,7 @@ export function useData({
y: group.data.y,
xaxis: group.xref,
yaxis: group.yref,
mode: config.showLabels === ELabelingOptions.NEVER ? 'markers' : 'text+markers',
mode: config.showLabels === ELabelingOptions.NEVER || config.xAxisScale === 'log' || config.yAxisScale === 'log' ? 'markers' : 'text+markers',
textposition: group.data.text.map((_, i) => textPositionOptions[i % textPositionOptions.length]),
...(config.showLabels === ELabelingOptions.NEVER
? {}
Expand Down
18 changes: 4 additions & 14 deletions src/vis/scatter/useDataPreparation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -185,21 +185,11 @@ export function useDataPreparation({
return index !== -1 ? index : Infinity;
});

let xDomain: [number, number] | [undefined, undefined] = [0, 1];
let yDomain: [number, number] | [undefined, undefined] = [0, 1];

// Get shared range for all plots
xDomain = d3v7.extent(value.validColumns[0].resolvedValues.map((v) => v.val as number));
yDomain = d3v7.extent(value.validColumns[1].resolvedValues.map((v) => v.val as number));

if (xDomain[0] !== undefined && xDomain[1] !== undefined && yDomain[0] !== undefined && yDomain[1] !== undefined) {
const xStretch = xDomain[1] - xDomain[0];
const yStretch = yDomain[1] - yDomain[0];
console.log(xStretch, yStretch);

xDomain = [xDomain[0] - xStretch * 0.5, xDomain[1] + xStretch * 0.5];
yDomain = [yDomain[0] - yStretch * 0.5, yDomain[1] + yStretch * 0.5];
}
const { xDomain, yDomain } = getStretchedDomains(
value.validColumns[0].resolvedValues.map((v) => v.val as number),
value.validColumns[1].resolvedValues.map((v) => v.val as number),
);

const resultData = groupedData.map((grouped, index) => {
const idToIndex = new Map<string, number>();
Expand Down
34 changes: 28 additions & 6 deletions src/vis/scatter/useLayout.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import clamp from 'lodash/clamp';
import isFinite from 'lodash/isFinite';
import * as React from 'react';
import { PlotlyTypes } from '../../plotly';
import { VIS_NEUTRAL_COLOR, VIS_TRACES_COLOR } from '../general/constants';
Expand Down Expand Up @@ -55,6 +56,21 @@ function gaps(width: number, height: number, nSubplots: number) {
};
}

function toLogRange(axisType: 'linear' | 'log', domain: [number, number] | [undefined, undefined]) {
if (axisType === 'linear') {
return [...domain];
}

const e0 = Math.log10(domain[0]);
const e1 = Math.log10(domain[1]);

if (isFinite(e1)) {
return [isFinite(e0) ? e0 : 0, e1];
}

return [0, 1];
}

export function useLayout({
scatter,
facet,
Expand Down Expand Up @@ -86,7 +102,8 @@ export function useLayout({
subplots.xyPairs.forEach((pair, plotCounter) => {
axes[`xaxis${plotCounter > 0 ? plotCounter + 1 : ''}`] = {
...AXIS_TICK_STYLES,
range: pair.xDomain,
range: toLogRange(config.xAxisScale!, pair.xDomain),
type: config.xAxisScale,
// Spread the previous layout to keep things like zoom
...(internalLayoutRef.current?.[`xaxis${plotCounter > 0 ? plotCounter + 1 : ''}` as 'xaxis'] || {}),
title: {
Expand All @@ -100,7 +117,8 @@ export function useLayout({
};
axes[`yaxis${plotCounter > 0 ? plotCounter + 1 : ''}`] = {
...AXIS_TICK_STYLES,
range: pair.yDomain,
range: toLogRange(config.yAxisScale!, pair.yDomain),
type: config.yAxisScale,
// Spread the previous layout to keep things like zoom
...(internalLayoutRef.current?.[`yaxis${plotCounter > 0 ? plotCounter + 1 : ''}` as 'yaxis'] || {}),
title: {
Expand Down Expand Up @@ -171,8 +189,9 @@ export function useLayout({
...BASE_LAYOUT,
xaxis: {
...AXIS_TICK_STYLES,
range: scatter.xDomain,
range: toLogRange(config.xAxisScale!, scatter.xDomain),
...internalLayoutRef.current?.xaxis,
type: config.xAxisScale,
title: {
font: {
size: 12,
Expand All @@ -183,8 +202,9 @@ export function useLayout({
},
yaxis: {
...AXIS_TICK_STYLES,
range: scatter.yDomain,
range: toLogRange(config.yAxisScale!, scatter.yDomain),
...internalLayoutRef.current?.yaxis,
type: config.yAxisScale,
title: {
font: {
size: 12,
Expand Down Expand Up @@ -212,7 +232,8 @@ export function useLayout({
facet.resultData.forEach((group, plotCounter) => {
axes[`xaxis${plotCounter > 0 ? plotCounter + 1 : ''}`] = {
...AXIS_TICK_STYLES,
range: facet.xDomain,
range: toLogRange(config.xAxisScale!, facet.xDomain),
type: config.xAxisScale,
// Spread the previous layout to keep things like zoom
...(internalLayoutRef.current?.[`xaxis${plotCounter > 0 ? plotCounter + 1 : ''}` as 'xaxis'] || {}),
...(plotCounter > 0 ? { matches: 'x' } : {}),
Expand All @@ -229,7 +250,8 @@ export function useLayout({
};
axes[`yaxis${plotCounter > 0 ? plotCounter + 1 : ''}`] = {
...AXIS_TICK_STYLES,
range: facet.yDomain,
range: toLogRange(config.yAxisScale!, facet.yDomain),
type: config.yAxisScale,
// Spread the previous layout to keep things like zoom
...(internalLayoutRef.current?.[`yaxis${plotCounter > 0 ? plotCounter + 1 : ''}` as 'yaxis'] || {}),
...(plotCounter > 0 ? { matches: 'y' } : {}),
Expand Down
2 changes: 2 additions & 0 deletions src/vis/scatter/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ export const defaultConfig: IScatterConfig = {
lineStyle: defaultRegressionLineStyle,
showStats: true,
},
xAxisScale: 'linear',
yAxisScale: 'linear',
};

export function scatterMergeDefaultConfig(columns: VisColumn[], config: IScatterConfig): IScatterConfig {
Expand Down
2 changes: 2 additions & 0 deletions src/vis/stories/Vis/Scatter/ScatterIris.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ Basic.args = {
name: 'Sepal Width',
},
],
xAxisScale: 'log',
yAxisScale: 'log',
color: null,
facets: null,
numColorScaleType: ENumericalColorScaleType.SEQUENTIAL,
Expand Down

0 comments on commit f2d6b71

Please sign in to comment.