diff --git a/package.json b/package.json index 46bcf1ab..e6a667ba 100644 --- a/package.json +++ b/package.json @@ -75,13 +75,13 @@ "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "cmdk": "^1.1.1", + "color-schemes-js": "^0.1.2", "date-fns": "^4.1.0", "embla-carousel-react": "^8.6.0", "fflate": "^0.8.2", "gsap": "^3.13.0", "icechunk-js": "^0.6.0", "input-otp": "^1.4.2", - "js-colormaps-es": "^0.0.5", "lucide-react": "^0.562.0", "next": "^16.2.7", "next-themes": "^0.4.6", diff --git a/src/GlobalStates/GlobalStore.ts b/src/GlobalStates/GlobalStore.ts index 65d7ca66..94e1b418 100644 --- a/src/GlobalStates/GlobalStore.ts +++ b/src/GlobalStates/GlobalStore.ts @@ -144,8 +144,18 @@ const createStore = () => create((set, get) => ({ setShape: (shape) => set({ shape }), setValueScales: (valueScales) => set({ valueScales }), setColormap: (colormap) => set({ colormap }), - setColormapName: (colormapName) => set({ colormapName }), - setFlipColormap: (flipColormap) => set({ flipColormap }), + setColormapName: (colormapName) => { + const prev = get().colormap; + const palette = (colormapName === 'Default') ? 'Spectral' : colormapName; + const tex = GetColorMapTexture(prev, palette, 1, '#000000', 0, get().flipColormap); + set({ colormapName, colormap: tex }); + }, + setFlipColormap: (flipColormap) => { + const palette = (get().colormapName === 'Default') ? 'Spectral' : get().colormapName; + const prev = get().colormap; + const tex = GetColorMapTexture(prev, palette, 1, '#000000', 0, flipColormap); + set({ flipColormap, colormap: tex }); + }, setTimeSeries: (timeSeries) => set({ timeSeries }), updateTimeSeries: (newEntries) => { const merged = { ...newEntries, ...get().timeSeries }; diff --git a/src/components/plots/FlatMap.tsx b/src/components/plots/FlatMap.tsx index 105f3c73..47d48594 100644 --- a/src/components/plots/FlatMap.tsx +++ b/src/components/plots/FlatMap.tsx @@ -10,7 +10,7 @@ import { vertShader } from '@/components/computation/shaders' import { useShallow } from 'zustand/shallow' import { ThreeEvent } from '@react-three/fiber'; import { coarsenFlatArray, GetCurrentArray, GetTimeSeries, parseUVCoords, deg2rad } from '@/utils/HelperFuncs'; -import { evaluate_cmap } from 'js-colormaps-es'; +import { evaluateColorMap } from '@/components/textures'; import { useCoordBounds } from '@/hooks/useCoordBounds'; import { flatFrag } from '../textures/shaders'; import { SquareMeshes } from './TransectMeshes'; @@ -153,8 +153,8 @@ const FlatMap = ({textures: propTextures, infoSetters} : {textures : THREE.DataT dimCoords = dimCoords.filter(val => val !== null) const tsID = `${dimCoords[0]}_${dimCoords[1]}` const tsObj = { - color:evaluate_cmap(getColorIdx()/10,"Paired"), - data:tempTS, + color: evaluateColorMap(getColorIdx() / 10, 'Paired'), + data: tempTS, normal, uv: tsUV, } diff --git a/src/components/plots/Sphere.tsx b/src/components/plots/Sphere.tsx index cab3b9f1..12c10a91 100644 --- a/src/components/plots/Sphere.tsx +++ b/src/components/plots/Sphere.tsx @@ -5,7 +5,7 @@ import { useGlobalStore } from '@/GlobalStates/GlobalStore'; import { usePlotStore } from '@/GlobalStates/PlotStore'; import { useShallow } from 'zustand/shallow' import { parseUVCoords, GetTimeSeries, GetCurrentArray, deg2rad } from '@/utils/HelperFuncs'; -import { evaluate_cmap } from 'js-colormaps-es'; +import { evaluateColorMap } from '@/components/textures'; import { useCoordBounds } from '@/hooks/useCoordBounds' import { SquareMeshes } from './TransectMeshes'; import { usePaddedTextures } from '@/hooks/usePaddedTextures'; @@ -175,8 +175,8 @@ export const Sphere = ({textures: propTextures} : {textures: THREE.Data3DTexture dimCoords = dimCoords.filter(val => val !== null) const tsID = `${dimCoords[0]}_${dimCoords[1]}` const tsObj = { - color:evaluate_cmap(getColorIdx()/10,"Paired"), - data:tempTS, + color: evaluateColorMap(getColorIdx() / 10, 'Paired'), + data: tempTS, normal, uv: tsUV, } diff --git a/src/components/plots/UVCube.tsx b/src/components/plots/UVCube.tsx index 301caa68..53ce51ec 100644 --- a/src/components/plots/UVCube.tsx +++ b/src/components/plots/UVCube.tsx @@ -5,7 +5,7 @@ import { useAnalysisStore } from '@/GlobalStates/AnalysisStore'; import { useGlobalStore } from '@/GlobalStates/GlobalStore'; import { usePlotStore } from '@/GlobalStates/PlotStore'; import { useShallow } from 'zustand/shallow'; -import { evaluate_cmap } from 'js-colormaps-es'; +import { evaluateColorMap } from '@/components/textures'; function normalizeUV(uv:number, scale:number, pos:number){ return (uv*scale) + (pos-0.5*scale+0.5) @@ -121,10 +121,10 @@ export const UVCube = ( {scale} : {scale?:THREE.Vector3} )=>{ dimCoords = dimCoords.filter(val => val !== null) const tsID = `${dimCoords[0]}_${dimCoords[1]}` const tsObj = { - color:evaluate_cmap(getColorIdx()/10,"Paired"), + color: evaluateColorMap(getColorIdx() / 10, 'Paired'), normal, uv, - data:tempTS + data: tempTS } updateTimeSeries({ [tsID] : tsObj}) incrementColorIdx(); diff --git a/src/components/textures/colormap.tsx b/src/components/textures/colormap.tsx index 4b4b26b3..4e42e9c9 100644 --- a/src/components/textures/colormap.tsx +++ b/src/components/textures/colormap.tsx @@ -1,9 +1,95 @@ import * as THREE from 'three'; -// https://github.com/vasturiano/three-globe/blob/master/src/utils/color-utils.js -import { evaluate_cmap } from 'js-colormaps-es'; -// import { rgbToHex } from './updateColorbar'; +import { colorschemes, get, findColorScheme } from 'color-schemes-js'; -export const colormaps = ['magma', 'inferno', 'plasma', 'viridis', 'cividis', 'twilight', 'twilight_shifted', 'turbo', 'Blues', 'BrBG', 'BuGn', 'BuPu', 'CMRmap', 'GnBu', 'Greens', 'Greys', 'OrRd', 'Oranges', 'PRGn', 'PiYG', 'PuBu', 'PuBuGn', 'PuOr', 'PuRd', 'Purples', 'RdBu', 'RdGy', 'RdPu', 'RdYlBu', 'RdYlGn', 'Reds', 'Spectral', 'Wistia', 'YlGn', 'YlGnBu', 'YlOrBr', 'YlOrRd', 'afmhot', 'autumn', 'binary', 'bone', 'brg', 'bwr', 'cool', 'coolwarm', 'copper', 'cubehelix', 'flag', 'gist_earth', 'gist_gray', 'gist_heat', 'gist_ncar', 'gist_rainbow', 'gist_stern', 'gist_yarg', 'gnuplot', 'gnuplot2', 'gray', 'hot', 'hsv', 'jet', 'nipy_spectral', 'ocean', 'pink', 'prism', 'rainbow', 'seismic', 'spring', 'summer', 'terrain', 'winter', 'Accent', 'Dark2', 'Paired', 'Pastel1', 'Pastel2', 'Set1', 'Set2', 'Set3', 'tab10', 'tab20', 'tab20b', 'tab20c'] +export const colormaps = ['magma', 'inferno', 'plasma', 'viridis', 'cividis', 'twilight', 'twilight_shifted', 'turbo', 'Blues', 'BrBG', 'BuGn', 'BuPu', 'CMRmap', 'GnBu', 'Greens', 'Greys', 'OrRd', 'Oranges', 'PRGn', 'PiYG', 'PuBu', 'PuBuGn', 'PuOr', 'PuRd', 'Purples', 'RdBu', 'RdGy', 'RdPu', 'RdYlBu', 'RdYlGn', 'Reds', 'Spectral', 'Wistia', 'YlGn', 'YlGnBu', 'YlOrBr', 'YlOrRd', 'afmhot', 'autumn', 'binary', 'bone', 'brg', 'bwr', 'cool', 'coolwarm', 'copper', 'cubehelix', 'flag', 'gist_earth', 'gist_gray', 'gist_heat', 'gist_ncar', 'gist_rainbow', 'gist_stern', 'gist_yarg', 'gnuplot', 'gnuplot2', 'gray', 'hot', 'hsv', 'jet', 'nipy_spectral', 'ocean', 'pink', 'prism', 'rainbow', 'seismic', 'spring', 'summer', 'terrain', 'winter', 'Accent', 'Dark2', 'Paired', 'Pastel1', 'Pastel2', 'Set1', 'Set2', 'Set3', 'tab10', 'tab20', 'tab20b', 'tab20c']; + +export const availableColorMapNames = Object.keys(colorschemes).sort(); + +export type ColormapEntry = { + name: string; + category?: string; + notes?: string; +}; + +// Single searchable index, built once at module load from the color-schemes-js data +export const colormapIndex: ColormapEntry[] = availableColorMapNames.map((name) => { + const scheme = colorschemes[name]; + return { + name, + category: scheme.category, + notes: scheme.notes, + }; +}); + +const legacySchemeMap: Record = { + Spectral: 'Spectral_11', + Paired: 'Paired_12', + Set1: 'Set1_9', + Set2: 'Set2_8', + Set3: 'Set3_12', + Accent: 'Accent_8', + Dark2: 'Dark2_8', + Pastel1: 'Pastel1_9', + Pastel2: 'Pastel2_8', + twilight_shifted: 'twilight', + gray: 'grays', + winter: 'Winter', +}; + +const resolutionCache = new Map(); +export function resolveColorSchemeName(name: string): string { + const trimmed = name?.trim(); + if (!trimmed) return 'viridis'; + + const cached = resolutionCache.get(trimmed); + if (cached) return cached; + + let resolved = 'viridis'; + if (legacySchemeMap[trimmed]) { + resolved = legacySchemeMap[trimmed]; + } else if (colorschemes[trimmed]) { + resolved = trimmed; + } else { + const exact = Object.keys(colorschemes).find((key) => key.toLowerCase() === trimmed.toLowerCase()); + if (exact) { + resolved = exact; + } else { + const results = findColorScheme(trimmed); + if (results.length > 0) { + resolved = results.sort((a, b) => b.scheme.length - a.scheme.length)[0].name; + } + } + } + + resolutionCache.set(trimmed, resolved); + return resolved; +} + +export function evaluateColorMap(value: number, palette: string, reverse = false): [number, number, number] { + const schemeName = resolveColorSchemeName(palette); + const scheme = colorschemes[schemeName] || colorschemes['viridis']; + if (!scheme) { + return [0, 0, 0]; + } + const t = Math.max(0, Math.min(1, reverse ? 1 - value : value)); + const color = get(scheme, t); + const rgb = color.toRgb255(); + return [rgb.r, rgb.g, rgb.b]; +} + +export function getColormapGradientCss(name: string): string { + const schemeName = resolveColorSchemeName(name); + const scheme = colorschemes[schemeName]; + if (!scheme) { + return 'linear-gradient(to right, #222, #999)'; + } + const stops = Array.from({ length: 16 }, (_, idx) => { + const t = idx / 15; + const { r, g, b } = get(scheme, t).toRgb255(); + return `rgb(${r}, ${g}, ${b}) ${(t * 100).toFixed(1)}%`; + }); + return `linear-gradient(to right, ${stops.join(', ')})`; +} export function minMax(values: number[]): { min: number | undefined, max: number | undefined } { // Filter out NaN values @@ -24,7 +110,7 @@ export function GetColorMapTexture( ): THREE.DataTexture { let unitInterval = Array.from({ length: 255 }, (_, index) => index / 254); unitInterval = reverse ? unitInterval.reverse() : unitInterval - const rgbv = unitInterval.map(value => evaluate_cmap(value, palette, false)); + const rgbv = unitInterval.map(value => evaluateColorMap(value, palette, false)); const colData = new Uint8Array((rgbv.length + 1) * 4); for (let i = 0; i < rgbv.length; i++) { diff --git a/src/components/textures/index.ts b/src/components/textures/index.ts index d9d0cda0..435212ac 100644 --- a/src/components/textures/index.ts +++ b/src/components/textures/index.ts @@ -1,9 +1,13 @@ -import { GetColorMapTexture, colormaps } from './colormap'; -import {ArrayToTexture, CreateTexture} from './TextureMakers' +import { GetColorMapTexture, colormaps, evaluateColorMap, availableColorMapNames, getColormapGradientCss, colormapIndex } from './colormap'; +import {ArrayToTexture, CreateTexture}from './TextureMakers' export { GetColorMapTexture, colormaps, + evaluateColorMap, + availableColorMapNames, + getColormapGradientCss, + colormapIndex, ArrayToTexture, CreateTexture } \ No newline at end of file diff --git a/src/components/textures/js-colormaps-es.d.ts b/src/components/textures/js-colormaps-es.d.ts deleted file mode 100644 index 6be74555..00000000 --- a/src/components/textures/js-colormaps-es.d.ts +++ /dev/null @@ -1,4 +0,0 @@ -declare module 'js-colormaps-es' { - export function evaluate_cmap(value: number, name: string, reverse?: boolean): [number, number, number]; - export const data: Record; - } \ No newline at end of file diff --git a/src/components/ui/MainPanel/Colormaps.tsx b/src/components/ui/MainPanel/Colormaps.tsx index 8713d351..7c1de364 100644 --- a/src/components/ui/MainPanel/Colormaps.tsx +++ b/src/components/ui/MainPanel/Colormaps.tsx @@ -1,14 +1,30 @@ "use client"; -import React, {useEffect, useState} from 'react' -import { GetColorMapTexture } from '@/components/textures'; +import React, { useEffect, useState, useMemo, useRef } from 'react' +import { GetColorMapTexture, colormaps, availableColorMapNames, getColormapGradientCss, colormapIndex } from '@/components/textures'; import { useGlobalStore } from '@/GlobalStates/GlobalStore'; -import { colormaps } from '@/components/textures'; import { useShallow } from 'zustand/shallow'; import { MdOutlineSwapVert } from "react-icons/md"; +import { ButtonGroup } from "@/components/ui/button-group"; import { Popover, PopoverTrigger, PopoverContent } from "@/components/ui/popover" import { Button } from "@/components/ui/button-enhanced"; -import Image from 'next/image'; +import { Input } from "@/components/ui/input"; +import { Separator } from "@/components/ui/separator"; +import { Search, X, Eye, EyeOff } from "lucide-react" +import { + InputGroup, + InputGroupAddon, + InputGroupInput, +} from "@/components/ui/input-group" +import { + Select, + SelectTrigger, + SelectValue, + SelectContent, + SelectItem, +} from "@/components/ui/select"; + +// Render gradients directly instead of using pre-generated icon images import { Tooltip, TooltipContent, @@ -17,7 +33,10 @@ import { const Colormaps = () => { + const [searchQuery, setSearchQuery] = useState(''); const [hoveredCmap, setHoveredCmap] = useState(null); + const [selectedCategory, setSelectedCategory] = useState(''); + const [showNames, setShowNames] = useState(true); const { colormap, setColormap, colormapName, flipColormap, setColormapName, setFlipColormap } = useGlobalStore( useShallow((state) => ({ setColormap: state.setColormap, @@ -30,11 +49,94 @@ const Colormaps = () => { ); const [popoverSide, setPopoverSide] = useState<"left" | "top">("left"); + const [prevColormapName, setPrevColormapName] = useState(colormapName || ''); + const previousTextureRef = useRef(colormap); + const colormapNameRef = useRef(colormapName); + const flipColormapRef = useRef(flipColormap); + const lastHoveredCmap = useRef(null); + + const categories = useMemo(() => { + const set = new Set(); + colormapIndex.forEach((entry) => { + if (entry.category) set.add(entry.category); + }); + return ['None', ...Array.from(set).sort()]; + }, []); + + const filteredColormaps = useMemo(() => { + const query = searchQuery.trim().toLowerCase(); + + // No search: category filtering works normally + if (!query) { + if (!selectedCategory || selectedCategory === 'None') { + return colormaps; + } + + return colormapIndex + .filter((entry) => entry.category === selectedCategory) + .map((entry) => entry.name); + } + + // Search ALWAYS uses the complete collection + const nameMatches: string[] = []; + const otherMatches: string[] = []; + + for (const entry of colormapIndex) { + const nameHit = entry.name.toLowerCase().includes(query); + const categoryHit = entry.category?.toLowerCase().includes(query); + const notesHit = entry.notes?.toLowerCase().includes(query); + + if (nameHit) { + nameMatches.push(entry.name); + } else if (categoryHit || notesHit) { + otherMatches.push(entry.name); + } + } + + return [...nameMatches, ...otherMatches]; + }, [searchQuery, selectedCategory]); + + const visibleMatches = useMemo(() => filteredColormaps.slice(0, 64), [filteredColormaps]); + const hasMoreResults = filteredColormaps.length > visibleMatches.length; + + // Keep refs in sync with store state changes + useEffect(() => { + colormapNameRef.current = colormapName; + flipColormapRef.current = flipColormap; + }, [colormapName, flipColormap]); + useEffect(() => { - setColormap( - GetColorMapTexture(colormap, (hoveredCmap || colormapName) === "Default" ? "Spectral" : (hoveredCmap || colormapName), 1, "#000000", 0, flipColormap) - ); - }, [colormapName, flipColormap, hoveredCmap]); + previousTextureRef.current = colormap; + }, [colormap]); + + useEffect(() => { + if (hoveredCmap !== null) { + // Show hovered colormap preview + setColormap( + GetColorMapTexture( + previousTextureRef.current, + hoveredCmap === "Default" ? "Spectral" : hoveredCmap, + 1, + "#000000", + 0, + flipColormapRef.current + ) + ); + } else if (lastHoveredCmap.current !== null) { + // Mouse left hover: revert to selected colormap + setColormap( + GetColorMapTexture( + previousTextureRef.current, + colormapNameRef.current === "Default" ? "Spectral" : colormapNameRef.current, + 1, + "#000000", + 0, + flipColormapRef.current + ) + ); + } + lastHoveredCmap.current = hoveredCmap; + }, [hoveredCmap, setColormap]); useEffect(() => { const handleResize = () => { @@ -55,10 +157,12 @@ const Colormaps = () => {
+ + + + + +
+ + + + + + {showNames ? "Hide names" : "Show names"} + + + + + + + + + {colormapName} + + +
+
+ + + setSearchQuery(event.target.value)} + placeholder="Search..." + /> + + + + {searchQuery ? ( + + ) : null} + + + + {(searchQuery.trim() || selectedCategory !== 'None') && ( +
+ Showing {visibleMatches.length} of {filteredColormaps.length} + {selectedCategory && selectedCategory !== 'None' && !searchQuery.trim() && <> in {selectedCategory}} + {searchQuery.trim() && <> matching "{searchQuery}"} + {hasMoreResults && ' — first 64 shown'} +
+ )} + +
+
+
+ {visibleMatches.map((val) => ( + + ))} +
+
+ {filteredColormaps.length === 0 && ( +
+ No colormaps found. Try a different search term. +
+ )} +
diff --git a/src/components/ui/css/MainPanel.css b/src/components/ui/css/MainPanel.css index 2272319f..962eae99 100644 --- a/src/components/ui/css/MainPanel.css +++ b/src/components/ui/css/MainPanel.css @@ -45,7 +45,7 @@ .cmap{ padding: 0px 0px; cursor: pointer; - max-width: 200px; + max-width: 95%; height: 36px; border-radius: 6px; transition: 0.25s; @@ -62,7 +62,7 @@ } .colormaps{ - max-height: 50vh; + max-height: 70vh; width: 234px; display:grid; gap:3px; diff --git a/src/utils/updateColorbar.tsx b/src/utils/updateColorbar.tsx index d9d5630b..d6a6b5f4 100644 --- a/src/utils/updateColorbar.tsx +++ b/src/utils/updateColorbar.tsx @@ -1,4 +1,4 @@ -import { evaluate_cmap } from 'js-colormaps-es'; +import { evaluateColorMap } from '@/components/textures'; interface UpdateColorbarParams { colorbarId: string; @@ -66,7 +66,7 @@ export function updateColorbar(params: UpdateColorbarParams) { export function getColors(palette: string): [number, number, number][] { const unitInterval = Array.from({ length: 32 }, (_, index) => index / 31); - return unitInterval.map(value => evaluate_cmap(value, palette, false)); + return unitInterval.map((value) => evaluateColorMap(value, palette, false)); } export function rgbToHex(color: [number, number, number]): string {