import type { Domain , NumericType } from '@h5web/shared' ; import { getValidDomainForScale , ScaleType , isDefined , formatTick , isScaleType , getBounds , DTypeClass , } from '@h5web/shared' ; import { scaleLinear , scaleThreshold } from '@visx/scale' ; import { tickStep , range } from 'd3-array' ; import type { ScaleLinear , ScaleThreshold } from 'd3-scale' ; import type { NdArray } from 'ndarray' ; import type { Vector3 } from 'three' ; import { Matrix4 } from 'three' ; import { clamp } from 'three/src/math/MathUtils' ; import type { Size , AxisScale , AxisConfig , AxisOffsets , ScaleGammaConfig , VisxScaleConfig , VisScaleType , } from './models' ; import { H5WEB_SCALES } from './scales' ; export const DEFAULT_DOMAIN = [ 0.1 , 1 ] ; const AXIS_OFFSETS = { vertical : 72 , horizontal : 40 , fallback : 16 } ; export const adaptedNumTicks = scaleLinear ( { domain : [ 300 , 900 ] , range : [ 3 , 10 ] , clamp : true , round : true , } ) ; const adaptedLogTicksThreshold = scaleLinear ( { domain : [ 300 , 500 ] , range : [ 0.8 , 1.4 ] , } ) ; export function createAxisScale ( scaleType , config ) { if ( isScaleType ( scaleType ) ) { return H5WEB_SCALES [ scaleType ] . createScale ( config ) ; } const [ , exponent ] = scaleType ; return H5WEB_SCALES [ ScaleType . Gamma ] . createScale ( { ... config , exponent } ) ; } export function getSizeToFit ( availableSize , ratioToRespect ) { const { width , height } = availableSize ; if ( ! ratioToRespect ) { return { width , height } ; } const availableRatio = width / height ; return { width : availableRatio > ratioToRespect ? height * ratioToRespect : width , height : availableRatio < ratioToRespect ? width / ratioToRespect : height , } ; } export function getDomain ( valuesArray , scaleType = ScaleType . Linear , errorArray ? ) { const bounds = getBounds ( valuesArray , errorArray ) ; return getValidDomainForScale ( bounds , scaleType ) ; } export function getDomains ( arrays , scaleType = ScaleType . Linear ) { return arrays . map ( ( arr ) => getDomain ( arr , scaleType ) ) ; } function extendEmptyDomain ( value , extendFactor , scaleType ) { if ( scaleType === ScaleType . Log ) { return [ value * 10 ** - extendFactor , value * 10 ** extendFactor ] ; } if ( value === 0 ) { return [ - 1 , 1 ] ; } const extension = Math . abs ( value ) * extendFactor ; return [ value - extension , value + extension ] ; } export function clampBound ( val , supportedDomain = [ - Number . MAX_VALUE / 2 , Number . MAX_VALUE / 2 ] ) { const [ supportedMin , supportedMax ] = supportedDomain ; return clamp ( val , supportedMin , supportedMax ) ; } export function extendDomain ( domain , extendFactor , scaleType = ScaleType . Linear ) { if ( extendFactor <= 0 ) { return domain ; } const { validMin } = H5WEB_SCALES [ scaleType ] ; const [ min ] = domain ; if ( min < validMin ) { throw new Error ( `Expected domain compatible with ${ scaleType } scale` ) ; } const extendedDomain = unsafeExtendDomain ( domain , extendFactor , scaleType ) ; return [ Math . max ( validMin , extendedDomain [ 0 ] ) , extendedDomain [ 1 ] ] ; } function unsafeExtendDomain ( domain , extendFactor , scaleType ) { const [ min , max ] = domain ; if ( min === max ) { return extendEmptyDomain ( min , extendFactor , scaleType ) ; } const scale = createAxisScale ( scaleType , { domain , range : [ 0 , 1 ] } ) ; return [ scale . invert ( - extendFactor ) , scale . invert ( 1 + extendFactor ) ] ; } export function getValueToIndexScale ( values , switchAtMidpoints ? ) { const rawThresholds = switchAtMidpoints ? values . map ( ( _ , i ) => values [ i - 1 ] + ( values [ i ] - values [ i - 1 ] ) / 2 ) : values ; const thresholds = rawThresholds . slice ( 1 ) ; const indices = range ( values . length ) ; return scaleThreshold < number , number > ( isDescending ( thresholds ) ? { domain : [ ... thresholds ] . reverse ( ) , range : [ ... indices ] . reverse ( ) , } : { domain : thresholds , range : indices } ) ; } export function getCanvasScale ( config , canvasSize ) { const { scaleType , visDomain , flip } = config ; const range = [ - canvasSize / 2 , canvasSize / 2 ] ; return createAxisScale ( scaleType ?? ScaleType . Linear , { domain : visDomain , range : flip ? range . reverse ( ) : range , } ) ; } /**
 * We can't rely on the axis scale's `ticks` method to get integer tick values because
 * `d3.tickStep` sometimes returns incoherent step values - e.g. `d3.tickStep(0, 2, 3)`
 * returns 0.5 instead of 1.
 *
 * So we implement our own, simplified version of `d3.ticks` that always outputs integer values.
 */ export function getIntegerTicks ( domain , count ) { const min = Math . min ( ... domain ) ; const max = Math . max ( ... domain ) ; const intMin = Math . ceil ( min ) ; const intMax = Math . floor ( max ) ; const domainLength = intMax - intMin + 1 ; const optimalCount = Math . min ( domainLength , count ) ; if ( optimalCount === 0 ) { return [ ] ; } const step = Math . max ( tickStep ( intMin , intMax , optimalCount ) , 1 ) ; const start = Math . ceil ( min / step ) ; const stop = Math . floor ( max / step ) ; const numTicks = stop - start + 1 ; return Array . from ( { length : numTicks } , ( _ , i ) => ( start + i ) * step ) ; } export function getTickFormatter ( domain , availableSize , scaleType ) { if ( scaleType !== ScaleType . Log ) { return formatTick ; } const [ min , max ] = domain [ 0 ] > 0 ? domain : [ - domain [ 1 ] , - [ domain [ 0 ] ] ] ; const threshold = adaptedLogTicksThreshold ( availableSize ) ; if ( max / min < 10 ** threshold ) { return formatTick ; } return ( val ) => { const loggedVal = Math . log10 ( Math . abs ( val . valueOf ( ) ) ) ; return loggedVal === Math . floor ( loggedVal ) ? formatTick ( val ) : '' ; } ; } export function getCombinedDomain ( domains ) { const domainsToCombine = domains . filter ( isDefined ) ; if ( domainsToCombine . length === 0 ) { return undefined ; } return domainsToCombine . reduce ( ( accDomain , nextDomain ) => [ Math . min ( accDomain [ 0 ] , nextDomain [ 0 ] ) , Math . max ( accDomain [ 1 ] , nextDomain [ 1 ] ) , ] ) ; } export function getAxisOffsets ( hasLabel = { } ) { const { horizontal , vertical , fallback } = AXIS_OFFSETS ; return { left : ( hasLabel . left ? fallback : 0 ) + vertical , bottom : ( hasLabel . bottom ? fallback : 0 ) + horizontal , right : hasLabel . right ? vertical : fallback , top : hasLabel . top ? horizontal : fallback , } ; } export function isDescending ( array ) { return array [ array . length - 1 ] - array [ 0 ] < 0 ; } export function getAxisDomain ( axisValues , scaleType = ScaleType . Linear , extensionFactor = 0 ) { const rawDomain = getDomain ( axisValues , scaleType ) ; if ( ! rawDomain ) { return undefined ; } const extendedDomain = extendDomain ( rawDomain , extensionFactor , scaleType ) ; return isDescending ( axisValues ) ? [ extendedDomain [ 1 ] , extendedDomain [ 0 ] ] : extendedDomain ; } const TYPE_STRINGS = { [ DTypeClass . Integer ] : 'int' , [ DTypeClass . Unsigned ] : 'uint' , [ DTypeClass . Float ] : 'float' , } ; export function formatNumType ( numType ) { return `${ TYPE_STRINGS [ numType . class ] }${ numType . size }` ; } export function projectCameraToHtml ( cameraVector , width , height ) { const cameraToHtmlMatrix = new Matrix4 ( ) . makeScale ( width / 2 , - height / 2 , 1 ) ; cameraToHtmlMatrix . setPosition ( width / 2 , height / 2 ) ; return cameraVector . clone ( ) . applyMatrix4 ( cameraToHtmlMatrix ) ; }