import { colorToRgba , RGBATupleToString } from '../../../../common/color_library_wrappers' ; import { Color } from '../../../../common/colors' ; import { TAU } from '../../../../common/constants' ; import { Pixels } from '../../../../common/geometry' ; import { cssFontShorthand , HorizontalAlignment } from '../../../../common/text_utils' ; import { renderLayers , withContext } from '../../../../renderers/canvas' ; import { MIN_STROKE_WIDTH } from '../../../xy_chart/renderer/canvas/primitives/line' ; import { LinkLabelVM , OutsideLinksViewModel , QuadViewModel , RowBox , RowSet , ShapeViewModel , TextRow , } from '../../layout/types/viewmodel_types' ; import { LinkLabelsViewModelSpec } from '../../layout/viewmodel/link_text_layout' ; import { isSunburst } from '../../layout/viewmodel/viewmodel' ; const LINE_WIDTH_MULT = 10 ; const TAPER_OFF_LIMIT = 50 ; const getCurrentRowX = ( row , horizontalAlignment , rotation ) => { const rowLength = Math . cos ( rotation ) * row . length ; const offset = horizontalAlignment === HorizontalAlignment . left ? - row . maximumLength / 2 : horizontalAlignment === HorizontalAlignment . right ? row . maximumLength / 2 - rowLength : - rowLength / 2 ; return row . rowAnchorX + offset ; } ; const getFillTextXOffset = ( box , rowLength , isRTL ) => { return isRTL ? rowLength - box . width / 2 - box . wordBeginning : box . width / 2 + box . wordBeginning ; } ; function renderTextRow ( ctx , { fontSize , fillTextColor , rotation , verticalAlignment , horizontalAlignment , container , clipText , isRTL } , linkLabelTextColor ) { return ( currentRow ) => { const crx = getCurrentRowX ( currentRow , horizontalAlignment , rotation ) ; const cry = - currentRow . rowAnchorY + ( Math . sin ( rotation ) * currentRow . length ) / 2 ; if ( ! Number . isFinite ( crx ) || ! Number . isFinite ( cry ) ) { return ; } withContext ( ctx , ( ) => { ctx . scale ( 1 , - 1 ) ; if ( clipText ) { ctx . rect ( container . x0 + 1 , container . y0 + 1 , container . x1 - container . x0 - 2 , container . y1 - container . y0 - 2 ) ; ctx . clip ( ) ; } ctx . beginPath ( ) ; ctx . translate ( crx , cry ) ; ctx . rotate ( - rotation ) ; ctx . fillStyle = fillTextColor ?? linkLabelTextColor ; ctx . textBaseline = verticalAlignment ; ctx . direction = isRTL ? 'rtl' : 'ltr' ; currentRow . rowWords . forEach ( ( box ) => { if ( box . isValue ) ctx . direction = 'ltr' ; ctx . font = cssFontShorthand ( box , fontSize ) ; ctx . fillText ( box . text , getFillTextXOffset ( box , currentRow . length , isRTL ) , 0 ) ; } ) ; ctx . closePath ( ) ; } ) ; } ; } function renderTextRows ( ctx , rowSet , linkLabelTextColor ) { rowSet . rows . forEach ( renderTextRow ( ctx , rowSet , linkLabelTextColor ) ) ; } function renderRowSets ( ctx , rowSets , linkLabelTextColor ) { rowSets . forEach ( ( rowSet ) => renderTextRows ( ctx , rowSet , linkLabelTextColor ) ) ; } function renderTaperedBorder ( ctx , { strokeWidth , strokeStyle , fillColor , x0 , x1 , y0px , y1px } ) { const X0 = x0 - TAU / 4 ; const X1 = x1 - TAU / 4 ; ctx . fillStyle = fillColor ; ctx . beginPath ( ) ; ctx . arc ( 0 , 0 , y0px , X0 , X0 ) ; ctx . arc ( 0 , 0 , y1px , X0 , X1 , false ) ; ctx . arc ( 0 , 0 , y0px , X1 , X0 , true ) ; ctx . fill ( ) ; if ( strokeWidth > 0.001 && ! ( x0 === 0 && x1 === TAU ) ) { ctx . lineWidth = strokeWidth ; const tapered = x1 - x0 < ( 15 * TAU ) / 360 ; if ( tapered ) { ctx . beginPath ( ) ; ctx . arc ( 0 , 0 , y1px , X0 , X1 , false ) ; ctx . stroke ( ) ; ctx . beginPath ( ) ; ctx . arc ( 0 , 0 , y0px , X1 , X0 , true ) ; ctx . stroke ( ) ; ctx . fillStyle = strokeStyle ; ctx . beginPath ( ) ; const yThreshold = Math . max ( TAPER_OFF_LIMIT , ( LINE_WIDTH_MULT * strokeWidth ) / ( X1 - X0 ) ) ; const beta = strokeWidth / yThreshold ; ctx . arc ( 0 , 0 , y0px , X0 , X0 + beta * ( yThreshold / y0px ) ) ; ctx . arc ( 0 , 0 , Math . min ( yThreshold , y1px ) , X0 + beta , X0 + beta ) ; ctx . arc ( 0 , 0 , y1px , X0 + beta * ( yThreshold / y1px ) , X0 , true ) ; ctx . arc ( 0 , 0 , y0px , X0 , X0 ) ; ctx . fill ( ) ; } else { ctx . strokeStyle = strokeStyle ; ctx . stroke ( ) ; } } } function renderSectors ( ctx , quadViewModel ) { withContext ( ctx , ( ) => { ctx . scale ( 1 , - 1 ) ; quadViewModel . forEach ( ( quad ) => { if ( quad . x0 !== quad . x1 ) renderTaperedBorder ( ctx , quad ) ; } ) ; } ) ; } function renderRectangles ( ctx , quadViewModel ) { withContext ( ctx , ( ) => { ctx . scale ( 1 , - 1 ) ; quadViewModel . forEach ( ( { strokeWidth , fillColor , x0 , x1 , y0px , y1px } ) => { if ( x1 - x0 >= 1 && y1px - y0px >= 1 ) { ctx . fillStyle = fillColor ; ctx . beginPath ( ) ; ctx . moveTo ( x0 , y0px ) ; ctx . lineTo ( x0 , y1px ) ; ctx . lineTo ( x1 , y1px ) ; ctx . lineTo ( x1 , y0px ) ; ctx . lineTo ( x0 , y0px ) ; ctx . fill ( ) ; if ( strokeWidth > MIN_STROKE_WIDTH ) { ctx . lineWidth = strokeWidth ; ctx . stroke ( ) ; } } } ) ; } ) ; } function renderFillOutsideLinks ( ctx , outsideLinksViewModel , linkLabelTextColor , linkLabelLineWidth ) { withContext ( ctx , ( ) => { ctx . lineWidth = linkLabelLineWidth ; ctx . strokeStyle = linkLabelTextColor ; outsideLinksViewModel . forEach ( ( { points } ) => { ctx . beginPath ( ) ; ctx . moveTo ( points [ 0 ] [ 0 ] , points [ 0 ] [ 1 ] ) ; for ( let i = 1 ; i < points . length ; i ++ ) { ctx . lineTo ( points [ i ] [ 0 ] , points [ i ] [ 1 ] ) ; } ctx . stroke ( ) ; } ) ; } ) ; } function getLinkTextXOffset ( { textAlign , width , valueWidth , isRTL } , labelValueGap ) { const isRightAligned = textAlign === HorizontalAlignment . right ; const multiplier = isRightAligned ? - 1 : 1 ; const isAligned = isRightAligned === isRTL ; const to = multiplier * ( labelValueGap + ( isAligned ? width : valueWidth ) ) ; return isAligned ? [ 0 , to ] : [ to , 0 ] ; } function renderLinkLabels ( ctx , linkLabelFontSize , linkLabelLineWidth , { linkLabels : allLinkLabels , labelFontSpec , valueFontSpec , strokeColor } , linkLineColor ) { const labelColor = RGBATupleToString ( colorToRgba ( labelFontSpec . textColor ) ) ; const valueColor = RGBATupleToString ( colorToRgba ( valueFontSpec . textColor ) ) ; const labelValueGap = linkLabelFontSize / 2 ; withContext ( ctx , ( ) => { ctx . lineWidth = linkLabelLineWidth ; allLinkLabels . forEach ( ( { linkLabels , translate , textAlign , text , valueText , width , valueWidth , isRTL } ) => { ctx . beginPath ( ) ; ctx . moveTo ( ... linkLabels [ 0 ] ) ; linkLabels . slice ( 1 ) . forEach ( ( point ) => ctx . lineTo ( ... point ) ) ; ctx . strokeStyle = strokeColor ?? linkLineColor ; ctx . stroke ( ) ; const [ labelX , valueX ] = getLinkTextXOffset ( { textAlign , width , valueWidth , isRTL } , labelValueGap ) ; withContext ( ctx , ( ) => { ctx . translate ( ... translate ) ; ctx . scale ( 1 , - 1 ) ; ctx . textAlign = textAlign ; ctx . fillStyle = labelColor ; ctx . font = cssFontShorthand ( labelFontSpec , linkLabelFontSize ) ; ctx . direction = isRTL ? 'rtl' : 'ltr' ; ctx . fillText ( text , labelX , 0 ) ; ctx . fillStyle = valueColor ; ctx . font = cssFontShorthand ( valueFontSpec , linkLabelFontSize ) ; ctx . direction = 'ltr' ; ctx . fillText ( valueText , valueX , 0 ) ; } ) ; } ) ; } ) ; } const midlineOffset = 0.35 ; internal export function renderPartitionCanvas2d ( ctx , dpr , { layout , width , height , style , quadViewModel , rowSets , outsideLinksViewModel , linkLabelViewModels , diskCenter , outerRadius , panel , chartDimensions , } ) { const { sectorLineWidth , sectorLineStroke , linkLabel } = style ; const linkLineColor = linkLabelViewModels . strokeColor ; withContext ( ctx , ( ) => { ctx . scale ( dpr , dpr ) ; ctx . textAlign = 'center' ; ctx . textBaseline = 'bottom' ; ctx . font = cssFontShorthand ( panel . fontFace , panel . fontSize ) ; ctx . fillStyle = panel . fontFace . textColor ; const innerPad = midlineOffset * panel . fontSize ; ctx . fillText ( panel . title , isSunburst ( layout ) ? diskCenter . x : diskCenter . x + ( chartDimensions . width * width ) / 2 , isSunburst ( layout ) ? style . linkLabel . maxCount > 0 ? diskCenter . y - ( chartDimensions . height * height ) / 2 + panel . fontSize : diskCenter . y - outerRadius - innerPad : diskCenter . y + 12 ) ; ctx . textBaseline = 'middle' ; ctx . translate ( diskCenter . x , diskCenter . y ) ; ctx . scale ( 1 , - 1 ) ; ctx . lineJoin = 'round' ; ctx . strokeStyle = sectorLineStroke ; ctx . lineWidth = sectorLineWidth ; renderLayers ( ctx , [ ( ) => ( isSunburst ( layout ) ? renderSectors ( ctx , quadViewModel ) : renderRectangles ( ctx , quadViewModel ) ) , ( ) => renderRowSets ( ctx , rowSets , linkLineColor ) , ( ) => renderFillOutsideLinks ( ctx , outsideLinksViewModel , linkLineColor , linkLabel . lineWidth ) , ( ) => renderLinkLabels ( ctx , linkLabel . fontSize , linkLabel . lineWidth , linkLabelViewModels , linkLineColor ) , ] ) ; } ) ; }