import { scaleLinear } from "d3-scale" ; import { TernaryPlot , Barycentric , Domains , Coord , Tick , TextAnchor , TernaryAxis , AxisLabel , } from "./types" ; const getDomainLengths = ( domains ) => new Set ( domains . map ( ( domain ) => { const d0 = Math . round ( ( domain [ 0 ] + Number . EPSILON ) * 100 ) / 100 ; const d1 = Math . round ( ( domain [ 1 ] + Number . EPSILON ) * 100 ) / 100 ; const difference = Math . abs ( d1 - d0 ) ; return Math . round ( ( difference + Number . EPSILON ) * 100 ) / 100 ; } ) ) ; param m { number } param c { number } returns const getdXdY = ( m , c ) : [ number , number ] => { const theta = Math . atan ( m ) ; const dx = c * Math . cos ( theta ) * Math . sign ( theta ) ; const dy = c * Math . sin ( theta ) * Math . sign ( theta ) ; return [ dx , dy ] ; } ; param m param distance returns const getTranslateCorrections = ( m , distance ) : [ number , number ] => { if ( m === 0 ) return [ 0 , - distance ] ; const inverseSlope = - 1 / m ; return getdXdY ( inverseSlope , distance ) ; } ; param param0 param param1 returns function lineBetween ( [ x1 , y1 ] , [ x2 , y2 ] ) { return function ( t ) : Coord { return [ x1 + t * ( x2 - x1 ) , y1 + t * ( y2 - y1 ) ] ; } ; } param param0 param param1 returns { number } const getSlope = ( [ x1 , y1 ] , [ x2 , y2 ] ) => ( y2 - y1 ) / ( x2 - x1 ) ; const epsilon = 1e-4 ; const insideDomain = ( n ) => ( n > 0.999999 ? 1 : n < 0.000001 ? 0 : n ) ; param b1 param b2 param m returns const parallelLinesDistance = ( b1 , b2 , m ) => ( ( b2 - b1 ) * Math . sign ( b1 ) ) / Math . sqrt ( m ** 2 + 1 ) ; export default function ternaryPlot ( barycentric ) { let radius = 300 , k = 1 , tx = 0 , ty = 0 , tickFormat = "%" , reverse = false ; let unscaledVertices = barycentric . vertices ( ) ; function ternaryPlot ( _ ) { const [ x , y ] = barycentric ( _ ) ; return [ x * radius , y * radius ] ; } /**
     * Returns the plots current, scaled vertices.
     */ function scaleVertices ( ) ; param newScaledVertices function scaleVertices ( newScaledVertices ) ; function scaleVertices ( newScaledVertices ? ) { if ( newScaledVertices ) { const newUnscaledVertices = newScaledVertices . map ( ( [ x , y ] ) : Coord => [ x / radius , y / radius ] ) ; barycentric . vertices ( newUnscaledVertices as [ Coord , Coord , Coord ] ) ; return ternaryPlot ; } const vertices = barycentric . vertices ( ) ; let scaledVertices = vertices . map ( ( [ x , y ] ) : Coord => [ x * radius , y * radius ] ) ; return scaledVertices as [ Coord , Coord , Coord ] ; } let [ svA , svB , svC ] = scaleVertices ( ) ; let A = { label : "A" , labelAngle : 0 , labelOffset : 45 , gridLine : lineBetween ( svC , svA ) , scale : scaleLinear ( ) . domain ( [ 0 , 1 ] ) , tickAngle : 0 , tickSize : 6 , tickTextAnchor : "start" , conjugate : null , } ; let B = { label : "B" , labelAngle : 60 , labelOffset : 45 , gridLine : lineBetween ( svA , svB ) , scale : scaleLinear ( ) . domain ( [ 0 , 1 ] ) , tickAngle : 60 , tickSize : 6 , tickTextAnchor : "end" , conjugate : null , } ; let C = { label : "C" , labelAngle : - 60 , labelOffset : 45 , gridLine : lineBetween ( svB , svC ) , scale : scaleLinear ( ) . domain ( [ 0 , 1 ] ) , tickAngle : - 60 , tickSize : 6 , tickTextAnchor : "end" , conjugate : null , } ; A . conjugate = B ; B . conjugate = C ; C . conjugate = A ; ternaryPlot . vertices = scaleVertices ; /**
     * Generates and return an array containing axis label objects. Each axis label object contains the following properties.
     * - `position`: an array of [x,y] coords
     * - `labelAngle`: the rotation of the axis label
     * - `label`: The axis label
     *
     * Takes an optional configuration object that specifies whether axis labels should be placed at the center of the axis, the default is `false`.
     */ ternaryPlot . axisLabels = function ( { center = false } = { } ) { return [ A , B , C ] . map ( ( d ) : AxisLabel => { const { label , labelAngle } = d ; const [ x , y ] = d . gridLine ( center ? 0.5 : 1 ) ; const position = [ ( x / radius ) * ( radius + d . labelOffset ) , ( y / radius ) * ( radius + d . labelOffset ) , ] ; return { position , label , angle : labelAngle , } ; } ) as [ AxisLabel , AxisLabel , AxisLabel ] ; } ; ternaryPlot . setDomains = function ( domains ) { const [ domainA , domainB , domainC ] = domains ; A . scale . domain ( domainA ) ; B . scale . domain ( domainB ) ; C . scale . domain ( domainC ) ; return ternaryPlot ; } ; ternaryPlot . reverseVertices = function ( ) { reverse = true ; const swappedVertices = [ svC , svA , svB ] ; const [ vA , vB , vC ] = unscaledVertices ; unscaledVertices = [ vC , vA , vB ] ; ternaryPlot . vertices ( swappedVertices ) ; A . gridLine = lineBetween ( svA , svC ) ; B . gridLine = lineBetween ( svB , svA ) ; C . gridLine = lineBetween ( svC , svB ) ; return ternaryPlot ; } ; /**
     * Returns the current domains, which defaults to `[[0, 1], [0, 1], [0, 1]]`.
     */ function domainsFunc ( ) ; /**
     * Sets the domains of the ternary plot to the specified domains in order `[A, B, C]` and checks if the supplied domains are reversed.
     * If this is the case, [`reverseVertices()`](#ternaryPlotReverseVertices) is called. The scale and translation offset associated with
     * the domains are [applied](#ternaryPlotTransformDoc) to correctly scale and translate the plot. At last it returns the ternaryPlot.
     */ function domainsFunc ( domains ) ; function domainsFunc ( domains ? ) { if ( ! domains ) return [ A . scale . domain ( ) , B . scale . domain ( ) , C . scale . domain ( ) ] ; const domainLengths = getDomainLengths ( domains ) ; if ( domainLengths . size !== 1 ) { throw new Error ( "Domains must all be of equal length" ) ; } const isReverseDomain = domains . every ( ( d ) => d [ 0 ] > d [ 1 ] ) ; if ( isReverseDomain ) { ternaryPlot . reverseVertices ( ) ; } else { reverse = false ; } ternaryPlot . setDomains ( domains ) ; const { x , y , k } = ternaryPlot . transformFromDomains ( domains ) ; ternaryPlot . translate ( [ x , y ] ) ; ternaryPlot . scale ( k ) ; return ternaryPlot ; } ternaryPlot . domains = domainsFunc ; param counts returns function gridLinesFunc ( counts ) ; function gridLinesFunc ( counts ) ; function gridLinesFunc ( counts = 20 ) { return [ A , B , C ] . map ( ( axis , i ) => { const gridCount = Array . isArray ( counts ) ? + counts [ i ] : + counts ; const gridValues = axis . scale . ticks ( gridCount - 1 ) ; return gridValues . map ( ( d ) => [ axis . gridLine ( axis . scale ( d ) ) , axis . conjugate ! . gridLine ( 1 - axis . scale ( d ) ) , ] ) ; } ) as [ [ Coord , Coord ] [ ] , [ Coord , Coord ] [ ] , [ Coord , Coord ] [ ] ] ; } ternaryPlot . gridLines = gridLinesFunc ; param counts function ticksFunc ( counts ) ; function ticksFunc ( counts ) ; function ticksFunc ( counts = 10 ) { return [ A , B , C ] . map ( ( axis , i ) => { const tickCount = Array . isArray ( counts ) ? + counts [ i ] : + counts ; const tickValues = axis . scale . ticks ( tickCount ) ; const format = typeof tickFormat === "function" ? tickFormat : axis . scale . tickFormat ( tickCount , tickFormat ) ; return tickValues . map ( ( tick ) : Tick => { const tickPos = reverse ? 1 - axis . scale ( tick ) : axis . scale ( tick ) ; return { tick : format ( tick ) , position : axis . gridLine ( tickPos ) , angle : axis . tickAngle , size : axis . tickSize , textAnchor : axis . tickTextAnchor , } ; } ) ; } ) ; } ternaryPlot . ticks = ticksFunc ; /**
     * Returns the current tick angles, which defaults to `[0, 60, -60]`.
     */ function tickAngles ( ) ; param tickAngles function tickAngles ( tickAngles ) ; function tickAngles ( tickAngles ? ) { return tickAngles ? ( ( A . tickAngle = tickAngles [ 0 ] ) , ( B . tickAngle = tickAngles [ 1 ] ) , ( C . tickAngle = tickAngles [ 2 ] ) , ternaryPlot ) : [ A . tickAngle , B . tickAngle , C . tickAngle ] ; } ternaryPlot . tickAngles = tickAngles ; /**
     * Returns the current tick sizes, which defaults to `[6, 6, 6]` (px).
     */ function tickSizes ( ) ; param _ function tickSizes ( _ ) ; /**
     * Sets the axis tick sizes to the specified tick sizes in order `[A, B, C]` and returns the ternary plot.
     * */ function tickSizes ( _ ) ; function tickSizes ( _ ? ) { return _ ? Array . isArray ( _ ) ? ( ( A . tickSize = _ [ 0 ] ) , ( B . tickSize = _ [ 1 ] ) , ( C . tickSize = _ [ 2 ] ) , ternaryPlot ) : ( ( A . tickSize = B . tickSize = C . tickSize = + _ ) , ternaryPlot ) : [ A . tickSize , B . tickSize , C . tickSize ] ; } ternaryPlot . tickSizes = tickSizes ; /**
     * Returns the current tick sizes, which defaults to `"%"`, meaning ticks are formatted as percentages.
     */ function setTickFormat ( ) ; param _ function setTickFormat ( _ ) ; function setTickFormat ( _ ? ) { return _ ? ( ( tickFormat = _ ) , ternaryPlot ) : tickFormat ; } ternaryPlot . tickFormat = setTickFormat ; /**
     * Returns the current tick text-anchors, which defaults to `["start", "start", "end"]`.
     */ function tickTextAnchors ( ) ; param _ function tickTextAnchors ( _ ) ; function tickTextAnchors ( _ ? ) { return _ ? ( ( A . tickTextAnchor = _ [ 0 ] ) , ( B . tickTextAnchor = _ [ 1 ] ) , ( C . tickTextAnchor = _ [ 2 ] ) , ternaryPlot ) : [ A . tickTextAnchor , B . tickTextAnchor , C . tickTextAnchor ] ; } ternaryPlot . tickTextAnchors = tickTextAnchors ; /**
     * Returns the current labels, which defaults to `[A, B, C]`.
     */ function labels ( ) ; param _ function labels ( _ ) ; function labels ( _ ? ) { return _ ? ( ( A . label = String ( _ [ 0 ] ) ) , ( B . label = String ( _ [ 1 ] ) ) , ( C . label = String ( _ [ 2 ] ) ) , ternaryPlot ) : [ A . label , B . label , C . label ] ; } ternaryPlot . labels = labels ; /**
     * Returns the current label angles, which defaults to `[0, 60, -60]`
     */ function labelAngles ( ) ; param _ function labelAngles ( _ ) ; function labelAngles ( _ ? ) { return _ ? ( ( A . labelAngle = _ [ 0 ] ) , ( B . labelAngle = _ [ 1 ] ) , ( C . labelAngle = _ [ 2 ] ) , ternaryPlot ) : [ A . labelAngle , B . labelAngle , C . labelAngle ] ; } ternaryPlot . labelAngles = labelAngles ; /**
     * The label offset is the spacing of the label to the vertex in pixels.
     * Returns the current label offsets, which defaults to `[45, 45, 45]` px.
     */ function labelOffsets ( ) ; param _ function labelOffsets ( _ ) ; param _ function labelOffsets ( _ ) ; function labelOffsets ( _ ? ) { return _ ? Array . isArray ( _ ) ? ( ( A . labelOffset = _ [ 0 ] ) , ( B . labelOffset = _ [ 1 ] ) , ( C . labelOffset = _ [ 2 ] ) , ternaryPlot ) : ( ( A . labelOffset = B . labelOffset = C . labelOffset = + _ ) , ternaryPlot ) : [ A . labelOffset , B . labelOffset , C . labelOffset ] ; } ternaryPlot . labelOffsets = labelOffsets ; /**
     * Returns an [SVG path command](https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/d) for a the outer triangle.
     * This is used for the bounds of the ternary plot and its [clipPath](https://developer.mozilla.org/en-US/docs/Web/SVG/Element/clipPath).
     */ ternaryPlot . triangle = function ( ) { return `M${ svA }L${ svB }L${ svC }Z` ; } ; /**
     * Returns the current radius, which defaults to 300 (px).
     */ function setRadius ( ) ; param _ function setRadius ( _ ) ; function setRadius ( _ ? ) { if ( typeof _ === "undefined" ) return radius ; radius = + _ ; [ svA , svB , svC ] = ternaryPlot . vertices ( ) ; A . gridLine = lineBetween ( svC , svA ) ; B . gridLine = lineBetween ( svA , svB ) ; C . gridLine = lineBetween ( svB , svC ) ; return ternaryPlot ; } ternaryPlot . radius = setRadius ; /**
     * Returns the current scale factor, which defaults to `1`.
     *
     * The scale factor corresponds inversely to the domain length.
     * For example a domains of `[[0, 0.5], [0, 0.5], [0.5, 1]` corresponds to a scale of 2.
     */ function setScale ( ) ; param _ function setScale ( _ ) ; function setScale ( _ ? ) { return _ ? ( ( k = + _ ) , ternaryPlot . transform ( ) , ternaryPlot ) : k ; } ternaryPlot . scale = setScale ; /**
     * Returns the current translation offset which defaults to `[0, 0]`.
     *
     * Note when setting the translation, the translation offsets are not scaled by the plot radius.
     */ function setTranslate ( ) ; param _ function setTranslate ( _ ) ; function setTranslate ( _ ? ) { return _ ? ( ( tx = _ [ 0 ] ) , ( ty = _ [ 1 ] ) , ternaryPlot . transform ( ) , ternaryPlot ) : [ tx , ty ] ; } ternaryPlot . translate = setTranslate ; param _ returns ternaryPlot . invert = function ( _ ) : [ number , number , number ] { const xy = [ _ [ 0 ] / radius , _ [ 1 ] / radius ] ; const inverted = barycentric . invert ( xy ) ; return inverted ; } ; returns ternaryPlot . transform = function ( ) { if ( k === 1 ) { tx = 0 ; ty = 0 ; return ternaryPlot ; } const [ vA , vB , vC ] = unscaledVertices ; const [ newvA , newvB , _newvC ] = unscaledVertices . map ( ( [ vx , vy ] ) : Coord => [ vx * k + tx , vy * k + ty ] ) ; const mAB = getSlope ( vA , vB ) , mAC = getSlope ( vA , vC ) , mBC = getSlope ( vB , vC ) ; const bAB = newvA [ 1 ] - mAB * newvA [ 0 ] , bAC = newvA [ 1 ] - mAC * newvA [ 0 ] , bBC = newvB [ 1 ] - mBC * newvB [ 0 ] ; const lineDistanceAC = parallelLinesDistance ( vA [ 1 ] , bAC , mAC ) , lineDistanceAB = reverse ? parallelLinesDistance ( vB [ 1 ] , bAB , mAB ) : parallelLinesDistance ( vA [ 1 ] , bAB , mAB ) , lineDistanceBC = parallelLinesDistance ( vB [ 1 ] , bBC , mBC ) ; if ( lineDistanceAB < - epsilon ) { const [ correctionX , correctionY ] = getTranslateCorrections ( mAB , lineDistanceAB ) ; tx += correctionX ; ty += correctionY ; } if ( lineDistanceAC < - epsilon ) { const [ correctionX , correctionY ] = getTranslateCorrections ( mAC , lineDistanceAC ) ; tx += correctionX ; ty += correctionY ; } if ( lineDistanceBC < - epsilon ) { const [ correctionX , correctionY ] = getTranslateCorrections ( mBC , lineDistanceBC ) ; tx += correctionX ; ty += correctionY ; } const [ uvA , uvB , uvC ] = unscaledVertices ; const scaleAndTranslateVertex = ( [ vx , vy ] ) : Coord => [ vx * k + tx , vy * k + ty , ] ; const transformedVertices = [ scaleAndTranslateVertex ( uvA ) , scaleAndTranslateVertex ( uvB ) , scaleAndTranslateVertex ( uvC ) , ] ; barycentric . vertices ( transformedVertices ) ; return ternaryPlot ; } ; param domains ternaryPlot . transformFromDomains = function ( domains ) { const [ domainA , domainB , domainC ] = domains ; const domainLengths = getDomainLengths ( domains ) ; const domainLength = [ ... domainLengths ] [ 0 ] ; const [ uvA , uvB , uvC ] = unscaledVertices ; const k = 1 / domainLength ; const domainFromScale = ( k ) => ( k - 1 ) / ( k * 3 ) ; const untranslatedDomainStart = domainFromScale ( k ) ; const shiftA = untranslatedDomainStart - domainA [ 0 ] , shiftB = untranslatedDomainStart - domainB [ 0 ] , shiftC = untranslatedDomainStart - domainC [ 0 ] ; const [ tx , ty ] = [ uvA [ 0 ] * shiftA + uvB [ 0 ] * shiftB + uvC [ 0 ] * shiftC , uvA [ 1 ] * shiftA + uvB [ 1 ] * shiftB + uvC [ 1 ] * shiftC , ] . map ( ( d ) => d * k ) ; return { k , x : tx , y : ty } ; } ; /**
     * Computes and returns the domains corresponding to the current transform.
     * This is used for syncing domains while zooming and panning.
     */ ternaryPlot . domainsFromVertices = function ( ) { const [ bA , bB , bC ] = unscaledVertices . map ( barycentric . invert ) ; const newADomain = [ bB [ 0 ] , bA [ 0 ] ] . map ( insideDomain ) , newBDomain = [ bC [ 1 ] , bB [ 1 ] ] . map ( insideDomain ) , newCDomain = [ bA [ 2 ] , bC [ 2 ] ] . map ( insideDomain ) ; return [ newADomain , newBDomain , newCDomain ] ; } ; return ternaryPlot ; }