Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(vis-type: scatter): Add log scale option for axes #602

Merged
merged 11 commits into from
Nov 5, 2024
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 @@ -115,7 +115,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,5 +1,6 @@
import * as React from 'react';
import clamp from 'lodash/clamp';
import isFinite from 'lodash/isFinite';
import { PlotlyTypes } from '../../plotly';
import { VIS_NEUTRAL_COLOR, VIS_TRACES_COLOR } from '../general/constants';
import { IInternalScatterConfig } from './interfaces';
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
Loading