Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 42 additions & 0 deletions src/GlobalStates/PlotStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,21 @@ type PlotState ={
camera: THREE.Camera | undefined;
nativeCRS: string | undefined;
destCRS: string | undefined;
colorScale: string;
logConstant: number;
lowclip: string;
highclip: string;
useLowclip: boolean;
useHighclip: boolean;
isCategorical: boolean;
categoricalMode: 'unique' | 'bins';
numBins: number;
uniqueCategories: number[];

setIsCategorical: (isCategorical: boolean) => void;
setCategoricalMode: (categoricalMode: 'unique' | 'bins') => void;
setNumBins: (numBins: number) => void;
setUniqueCategories: (uniqueCategories: number[]) => void;

setQuality: (quality: number) => void;
setTimeScale: (timeScale : number) =>void;
Expand All @@ -91,6 +106,12 @@ type PlotState ={
setAnimProg: (animProg: number) => void;
setCOffset: (cOffset: number) => void;
setCScale: (cScale: number) => void;
setColorScale: (colorScale: string) => void;
setLogConstant: (logConstant: number) => void;
setLowclip: (lowclip: string) => void;
setHighclip: (highclip: string) => void;
setUseLowclip: (useLowclip: boolean) => void;
setUseHighclip: (useHighclip: boolean) => void;
setUseFragOpt: (useFragOpt: boolean) => void;
setResetCamera: (resetCamera: boolean) => void;
setUseCustomColor: (useCustomColor: boolean) => void;
Expand Down Expand Up @@ -193,6 +214,21 @@ export const usePlotStore = create<PlotState>((set, get) => ({
camera: undefined,
nativeCRS: undefined,
destCRS: undefined,
colorScale: "identity",
logConstant: 1.0,
lowclip: "#000000",
highclip: "#ffffff",
useLowclip: false,
useHighclip: false,
isCategorical: false,
categoricalMode: 'bins',
numBins: 10,
uniqueCategories: [],

setIsCategorical: (isCategorical) => set({ isCategorical }),
setCategoricalMode: (categoricalMode) => set({ categoricalMode }),
setNumBins: (numBins) => set({ numBins }),
setUniqueCategories: (uniqueCategories) => set({ uniqueCategories }),

setVTransferRange: (vTransferRange) => set({ vTransferRange }),
setVTransferScale: (vTransferScale) => set({ vTransferScale }),
Expand All @@ -219,6 +255,12 @@ export const usePlotStore = create<PlotState>((set, get) => ({
setAnimProg: (animProg) => set({ animProg }),
setCOffset: (cOffset) => set({ cOffset }),
setCScale: (cScale) => set({ cScale }),
setColorScale: (colorScale) => set({ colorScale }),
setLogConstant: (logConstant) => set({ logConstant }),
setLowclip: (lowclip) => set({ lowclip }),
setHighclip: (highclip) => set({ highclip }),
setUseLowclip: (useLowclip) => set({ useLowclip }),
setUseHighclip: (useHighclip) => set({ useHighclip }),
setUseFragOpt: (useFragOpt) => set({ useFragOpt }),
setResetCamera: (resetCamera) => set({ resetCamera }),
setUseCustomColor: (useCustomColor) => set({ useCustomColor }),
Expand Down
163 changes: 163 additions & 0 deletions src/__tests__/colorScale.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
import { describe, it, expect } from 'vitest';
import { applyColorScale, colorScaleToId, exprToGLSL } from '../components/textures/colormap';

describe('Color Scale Options', () => {
describe('1. Linear Scale: identity', () => {
it('returns original normalized x directly', () => {
expect(applyColorScale(0.0, 'identity')).toBe(0.0);
expect(applyColorScale(0.25, 'identity')).toBe(0.25);
expect(applyColorScale(0.5, 'identity')).toBe(0.5);
expect(applyColorScale(1.0, 'identity')).toBe(1.0);
});
});

describe('2. Log Scale: log(x)', () => {
it('Test 1: log(x) on range (1.0, 10.0)', () => {
const minVal = 1.0;
const maxVal = 10.0;
const dataRange = maxVal - minVal;
const n = 10;

const logA = Math.log10(minVal);
const logB = Math.log10(maxVal);
const dataPoints = Array.from({ length: n }, (_, i) => {
const logVal = logA + (i / (n - 1)) * (logB - logA);
return Math.pow(10, logVal);
});

const expectedPositions = Array.from({ length: n }, (_, i) => i / (n - 1));

dataPoints.forEach((d, i) => {
const x = (d - minVal) / dataRange;
const pos = applyColorScale(x, 'log(x)', 1.0, 0.0001, dataRange, minVal);
expect(pos).toBeCloseTo(expectedPositions[i], 4);
});
});

it('Test 2: log(x) on range (0.001, 1.0)', () => {
const minVal = 0.001;
const maxVal = 1.0;
const dataRange = maxVal - minVal;
const n = 10;

const logA = Math.log10(minVal);
const logB = Math.log10(maxVal);
const dataPoints = Array.from({ length: n }, (_, i) => {
const logVal = logA + (i / (n - 1)) * (logB - logA);
return Math.pow(10, logVal);
});

const expectedPositions = Array.from({ length: n }, (_, i) => i / (n - 1));

dataPoints.forEach((d, i) => {
const x = (d - minVal) / dataRange;
const pos = applyColorScale(x, 'log(x)', 1.0, 0.0001, dataRange, minVal);
expect(pos).toBeCloseTo(expectedPositions[i], 4);
});
});

it('Test 3: log(x) on range (0.0, 1000.0) with zero clipping', () => {
const minVal = 0.0;
const maxVal = 1000.0;
const dataRange = maxVal - minVal;
const logEps = 0.001; // v_pos_min = 1.0

expect(applyColorScale(0.0, 'log(x)', 1.0, logEps, dataRange, minVal)).toBe(0.0);
expect(applyColorScale(0.001, 'log(x)', 1.0, logEps, dataRange, minVal)).toBeCloseTo(0.0, 4);
expect(applyColorScale(0.01, 'log(x)', 1.0, logEps, dataRange, minVal)).toBeCloseTo(0.3333, 3);
expect(applyColorScale(0.1, 'log(x)', 1.0, logEps, dataRange, minVal)).toBeCloseTo(0.6667, 3);
expect(applyColorScale(1.0, 'log(x)', 1.0, logEps, dataRange, minVal)).toBeCloseTo(1.0, 4);
});
});

describe('3. Standard Log Offset: log(1+x)', () => {
it('expands lower values across the range (0.0, 1000.0)', () => {
const dataRange = 1000.0;

expect(applyColorScale(0.0, 'log(1+x)', 1.0, 0.0001, dataRange)).toBe(0.0);
expect(applyColorScale(0.01, 'log(1+x)', 1.0, 0.0001, dataRange)).toBeCloseTo(0.3472, 3);
expect(applyColorScale(0.1, 'log(1+x)', 1.0, 0.0001, dataRange)).toBeCloseTo(0.6680, 3);
expect(applyColorScale(1.0, 'log(1+x)', 1.0, 0.0001, dataRange)).toBeCloseTo(1.0, 4);
});
});

describe('4. Custom Constant Log Offset: log(x+c)', () => {
it('matches log(1+x) when c = 1.0', () => {
const dataRange = 1000.0;

expect(applyColorScale(0.0, 'log(x+c)', 1.0, 0.0001, dataRange)).toBe(0.0);
expect(applyColorScale(0.01, 'log(x+c)', 1.0, 0.0001, dataRange)).toBeCloseTo(0.3472, 3);
expect(applyColorScale(1.0, 'log(x+c)', 1.0, 0.0001, dataRange)).toBeCloseTo(1.0, 4);
});

it('expands lower values more aggressively when c = 0.1', () => {
const dataRange = 1000.0;
const c = 0.1;

expect(applyColorScale(0.0, 'log(x+c)', c, 0.0001, dataRange)).toBe(0.0);
expect(applyColorScale(0.01, 'log(x+c)', c, 0.0001, dataRange)).toBeCloseTo(0.5010, 3);
expect(applyColorScale(1.0, 'log(x+c)', c, 0.0001, dataRange)).toBeCloseTo(1.0, 4);
});
});

describe('5. Sign-Preserving Sqrt: sign(x)*sqrt(abs(x))', () => {
it('expands lower-end values scale-invariantly', () => {
expect(applyColorScale(0.0, 'sign(x)*sqrt(abs(x))')).toBe(0.0);
expect(applyColorScale(0.01, 'sign(x)*sqrt(abs(x))')).toBeCloseTo(0.10, 4);
expect(applyColorScale(0.25, 'sign(x)*sqrt(abs(x))')).toBeCloseTo(0.50, 4);
expect(applyColorScale(1.0, 'sign(x)*sqrt(abs(x))')).toBeCloseTo(1.0, 4);
});

it('preserves negative signs for symmetric variables', () => {
expect(applyColorScale(-0.01, 'sign(x)*sqrt(abs(x))')).toBeCloseTo(-0.10, 4);
expect(applyColorScale(-0.25, 'sign(x)*sqrt(abs(x))')).toBeCloseTo(-0.50, 4);
});
});

describe('6. Exponential Transform: exp(x)/100', () => {
it('maps log-space input back to linear colormap coordinates', () => {
const dataRange = 5.0;

expect(applyColorScale(0.0, 'exp(x)/100', 1.0, 0.0001, dataRange)).toBe(0.0);
expect(applyColorScale(0.5, 'exp(x)/100', 1.0, 0.0001, dataRange)).toBeCloseTo(0.0758, 3);
expect(applyColorScale(1.0, 'exp(x)/100', 1.0, 0.0001, dataRange)).toBeCloseTo(1.0, 4);
});
});

describe('7. Generic Custom Expressions', () => {
it('evaluates piecewise ternary "x > 0 ? x/2 : x"', () => {
const expr = 'x > 0 ? x/2 : x';
expect(colorScaleToId(expr)).toBe(6);
expect(applyColorScale(0.0, expr)).toBe(0.0);
expect(applyColorScale(0.5, expr)).toBeCloseTo(0.5, 4);
expect(applyColorScale(1.0, expr)).toBe(1.0);
});

it('evaluates piecewise ternary "x > 0 ? x*2 : x"', () => {
const expr = 'x > 0 ? x*2 : x';
expect(colorScaleToId(expr)).toBe(6);
expect(applyColorScale(0.0, expr)).toBe(0.0);
expect(applyColorScale(0.5, expr)).toBeCloseTo(0.5, 4);
expect(applyColorScale(1.0, expr)).toBe(1.0);
});

it('evaluates offset expression "x + 10"', () => {
const expr = 'x + 10';
expect(applyColorScale(0.0, expr)).toBe(0.0);
expect(applyColorScale(0.5, expr)).toBe(0.5);
expect(applyColorScale(1.0, expr)).toBe(1.0);
});

it('evaluates power expression "x * x"', () => {
const expr = 'x * x';
expect(applyColorScale(0.0, expr)).toBe(0.0);
expect(applyColorScale(0.5, expr)).toBe(0.25);
expect(applyColorScale(1.0, expr)).toBe(1.0);
});

it('converts JS expressions to float-safe GLSL code', () => {
expect(exprToGLSL('x > 0 ? x/2 : x')).toBe('(val) > 0.0 ? (val)/2.0 : (val)');
expect(exprToGLSL('x + 10')).toBe('(val) + 10.0');
});
});
});
52 changes: 34 additions & 18 deletions src/components/computation/shaders/frag.glsl
Original file line number Diff line number Diff line change
@@ -1,19 +1,35 @@
uniform sampler2D data;
uniform sampler2D cmap;

uniform float cOffset;
uniform float cScale;

in vec2 vUv;
out vec4 Color;

void main() {
vec4 val = texture(data,vUv);
float d = val.x;
float sampLoc = d == 1. ? d : (d - 0.5)*cScale + 0.5;
sampLoc = d == 1. ? d : min(sampLoc+cOffset,0.99);
vec4 color = texture(cmap, vec2(sampLoc,0.5));
color.a = val.x > 0.999 ? 0. : 1.;

Color = color;
uniform sampler2D data;
uniform sampler2D cmap;

uniform float cOffset;
uniform float cScale;
uniform vec2 threshold;
uniform int colorScale;
uniform float logConstant;
uniform float logEps;
uniform float dataRange;
uniform float minVal;
uniform vec4 lowclip;
uniform vec4 highclip;
uniform bool useLowclip;
uniform bool useHighclip;

in vec2 vUv;
out vec4 Color;

// APPLY_COLOR_SCALE

void main() {
vec4 val = texture(data, vUv);
float d = val.x;
if (d >= 0.999) {
Color = vec4(0.0);
return;
}

Color = evaluateColorScale(
d, threshold, 0.999, vec3(0.0), 0.0,
cmap, cScale, cOffset, colorScale, logConstant, logEps,
dataRange, minVal, lowclip, highclip, useLowclip, useHighclip
);
}
34 changes: 19 additions & 15 deletions src/components/computation/shaders/index.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,20 @@
import MaxFrag from './Max.glsl'
import MinFrag from './Min.glsl'
import MeanFrag from './Mean.glsl'
import StDevFrag from './StDev.glsl'
import vertShader from './vert.glsl'
import fragShader from './frag.glsl'
import correlateFrag from './Correlation.glsl'
export {
MaxFrag,
MinFrag,
MeanFrag,
StDevFrag,
vertShader,
fragShader,
correlateFrag
import applyColorScaleChunk from '@/components/textures/shaders/applyColorScale.glsl';
import MaxFrag from './Max.glsl'
import MinFrag from './Min.glsl'
import MeanFrag from './Mean.glsl'
import StDevFrag from './StDev.glsl'
import vertShader from './vert.glsl'
import fragShaderRaw from './frag.glsl'
import correlateFrag from './Correlation.glsl'

const fragShader = fragShaderRaw.replace('// APPLY_COLOR_SCALE', applyColorScaleChunk);

export {
MaxFrag,
MinFrag,
MeanFrag,
StDevFrag,
vertShader,
fragShader,
correlateFrag
}
10 changes: 7 additions & 3 deletions src/components/plots/AnalysisWG.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -154,16 +154,20 @@ const AnalysisWG = ({ setTexture, }: { setTexture: React.Dispatch<React.SetState
}

// --- Value scaling logic ---
let minVal, maxVal;
let minVal = valueScales.minVal;
let maxVal = valueScales.maxVal;
const needsRescale = ['StDev', 'LinearSlope', 'Covariance', 'CUMSUM3D'].some(op => currentOperation.includes(op));
const isCorrelation = currentOperation.includes('Correlation');

if (needsRescale) {
if (!valueScalesOrig) setValueScalesOrig(valueScales);
[minVal, maxVal] = ArrayMinMax(newArray);
const [calculatedMin, calculatedMax] = ArrayMinMax(newArray);
minVal = calculatedMin ?? valueScales.minVal;
maxVal = calculatedMax ?? valueScales.maxVal;
} else if (isCorrelation) {
if (!valueScalesOrig) setValueScalesOrig(valueScales);
[minVal, maxVal] = [-1, 1];
minVal = -1;
maxVal = 1;
} else {
({ minVal, maxVal } = valueScales);
}
Expand Down
Loading
Loading