import { colorToRgba } from '../../../../common/color_library_wrappers' ; import { Colors } from '../../../../common/colors' ; import { TAU } from '../../../../common/constants' ; import { fillTextColor } from '../../../../common/fill_text_color' ; import { Distance , meanAngle , Pixels , PointTuple , Radius , trueBearingToStandardPositionAngle , } from '../../../../common/geometry' ; import { Part } from '../../../../common/text_utils' ; import { GroupByAccessor } from '../../../../specs' ; import { TextMeasure } from '../../../../utils/bbox/canvas_text_bbox_calculator' ; import { StrokeStyle , ColorVariant } from '../../../../utils/common' ; import { Size } from '../../../../utils/dimensions' ; import { Logger } from '../../../../utils/logger' ; import { FillLabelConfig , PartitionStyle } from '../../../../utils/themes/partition' ; import { BackgroundStyle } from '../../../../utils/themes/theme' ; import { Layer , PartitionSpec } from '../../specs' ; import { MODEL_KEY , percentValueGetter } from '../config' ; import { PartitionLayout } from '../types/config_types' ; import { nullShapeViewModel , OutsideLinksViewModel , PartitionSmallMultiplesModel , PickFunction , QuadViewModel , RawTextGetter , RowSet , ShapeTreeNode , ShapeViewModel , ValueGetterFunction , } from '../types/viewmodel_types' ; import { ringSectorConstruction } from '../utils/circline_geometry' ; import { aggregateAccessor , ArrayEntry , depthAccessor , entryKey , entryValue , mapEntryValue , parentAccessor , sortIndexAccessor , HierarchyOfArrays , pathAccessor , } from '../utils/group_by_rollup' ; import { sunburst } from '../utils/sunburst' ; import { getTopPadding , LayerLayout , treemap } from '../utils/treemap' ; import { waffle } from '../utils/waffle' ; import { fillTextLayout , getRectangleRowGeometry , getSectorRowGeometry , inSectorRotation , nodeId , } from './fill_text_layout' ; import { linkTextLayout } from './link_text_layout' ; internal export const isMosaic = ( p ) => p === PartitionLayout . mosaic ; internal export const isTreemap = ( p ) => p === PartitionLayout . treemap ; internal export const isSunburst = ( p ) => p === PartitionLayout . sunburst ; internal export const isIcicle = ( p ) => p === PartitionLayout . icicle ; internal export const isFlame = ( p ) => p === PartitionLayout . flame ; internal export const isWaffle = ( p ) => p === PartitionLayout . waffle ; internal export const isLinear = ( p ) => isFlame ( p ) || isIcicle ( p ) ; internal export const isSimpleLinear = ( layout , fillLabel , layers ) => isLinear ( layout ) && layers . every ( ( l ) => l . fillLabel ?. clipText ?? fillLabel ?. clipText ) ; function grooveAccessor ( n ) { return entryValue ( n ) . depth > 1 ? 1 : [ 0 , 2 ] [ entryValue ( n ) . depth ] ; } function topGrooveAccessor ( topGroovePx ) { return ( n ) => ( entryValue ( n ) . depth > 0 ? topGroovePx : grooveAccessor ( n ) ) ; } function rectangleFillOrigins ( n ) { return [ ( n . x0 + n . x1 ) / 2 , ( n . y0 + n . y1 ) / 2 ] ; } internal export const ringSectorInnerRadius = ( n ) : Radius => n . y0px ; internal export const ringSectorOuterRadius = ( n ) : Radius => n . y1px ; internal export const ringSectorMiddleRadius = ( n ) : Radius => n . yMidPx ; function sectorFillOrigins ( fillOutside ) { return ( node ) : [ number , number ] => { const midAngle = ( node . x0 + node . x1 ) / 2 ; const divider = 10 ; const innerBias = fillOutside ? 9 : 1 ; const outerBias = divider - innerBias ; const radius = ( innerBias * ringSectorInnerRadius ( node ) + outerBias * ringSectorOuterRadius ( node ) ) / divider ; const cx = Math . cos ( trueBearingToStandardPositionAngle ( midAngle ) ) * radius ; const cy = Math . sin ( trueBearingToStandardPositionAngle ( midAngle ) ) * radius ; return [ cx , cy ] ; } ; } const minRectHeightForText = 8 ; internal export function makeQuadViewModel ( childNodes , layers , sectorLineWidth , sectorLineStroke , smAccessorValue , index , innerIndex , fillLabel , { color : backgroundColor , fallbackColor : fallbackBGColor } ) { if ( colorToRgba ( backgroundColor ) [ 3 ] < 1 ) { Logger . expected ( 'Text contrast requires an opaque background color, using fallbackColor' , 'an opaque color' , backgroundColor ) ; } return childNodes . map ( ( node ) => { const layer = layers [ node . depth - 1 ] ; const fill = layer ?. shape ?. fillColor ?? 'rgba(128, 0, 0, 0.5)' ; const fillColor = typeof fill === 'function' ? fill ( node , node . sortIndex , node [ MODEL_KEY ] . children ) : fill ; const strokeWidth = sectorLineWidth ; const strokeStyle = sectorLineStroke ; const textNegligible = node . y1px - node . y0px < minRectHeightForText ; const textColor = textNegligible ? Colors . Transparent . keyword : fillLabel . textColor === ColorVariant . Adaptive ? fillTextColor ( fallbackBGColor , fillColor , backgroundColor ) : fillLabel . textColor ; return { index , innerIndex , smAccessorValue , strokeWidth , strokeStyle , fillColor , textColor , ... node } ; } ) ; } internal export function makeOutsideLinksViewModel ( outsideFillNodes , rowSets , linkLabelRadiusPadding ) { return outsideFillNodes . map < OutsideLinksViewModel > ( ( node , i ) => { const rowSet = rowSets [ i ] ; if ( ! rowSet . rows . reduce ( ( p , row ) => p + row . rowWords . length , 0 ) ) return { points : [ ] } ; const radius = ringSectorOuterRadius ( node ) ; const midAngle = trueBearingToStandardPositionAngle ( meanAngle ( node . x0 , node . x1 ) ) ; const cos = Math . cos ( midAngle ) ; const sin = Math . sin ( midAngle ) ; const x0 = cos * radius ; const y0 = sin * radius ; const x = cos * ( radius + linkLabelRadiusPadding ) ; const y = sin * ( radius + linkLabelRadiusPadding ) ; return { points : [ [ x0 , y0 ] , [ x , y ] , ] , } ; } ) . filter ( ( { points } ) => points . length > 1 ) ; } internal export interface RectangleConstruction { x0 : Pixels ; x1 : Pixels ; y0 : Pixels ; y1 : Pixels ; } function rectangleConstruction ( treeHeight , topGroove ) { return function rectangleConstructionClosure ( node ) : RectangleConstruction { return node . depth < treeHeight && topGroove !== null ? { x0 : node . x0 , y0 : node . y0px , x1 : node . x1 , y1 : node . y0px + getTopPadding ( topGroove , node . y1px - node . y0px ) , } : { x0 : node . x0 , y0 : node . y0px , x1 : node . x1 , y1 : node . y1px , } ; } ; } const rawChildNodes = ( partitionLayout , tree , topGroove , width , height , clockwiseSectors , specialFirstInnermostSector , maxDepth ) : Array < Part > => { const totalValue = tree . reduce ( ( p , n ) : number => p + mapEntryValue ( n ) , 0 ) ; switch ( partitionLayout ) { case PartitionLayout . sunburst : const sunburstValueToAreaScale = TAU / totalValue ; const sunburstAreaAccessor = ( e ) => sunburstValueToAreaScale * mapEntryValue ( e ) ; return sunburst ( tree , sunburstAreaAccessor , { x0 : 0 , y0 : - 1 } , clockwiseSectors , specialFirstInnermostSector ) ; case PartitionLayout . treemap : case PartitionLayout . mosaic : const treemapInnerArea = width * height ; const treemapValueToAreaScale = treemapInnerArea / totalValue ; const treemapAreaAccessor = ( e ) => treemapValueToAreaScale * mapEntryValue ( e ) ; return treemap ( tree , treemapAreaAccessor , topGrooveAccessor ( topGroove ) , grooveAccessor , { x0 : 0 , y0 : 0 , width , height , } , isMosaic ( partitionLayout ) ? [ LayerLayout . vertical , LayerLayout . horizontal ] : [ ] ) ; case PartitionLayout . waffle : return waffle ( tree , totalValue , { x0 : 0 , y0 : 0 , width , height , } ) ; case PartitionLayout . icicle : case PartitionLayout . flame : const icicleLayout = isIcicle ( partitionLayout ) ; const icicleValueToAreaScale = width / totalValue ; const icicleAreaAccessor = ( e ) => icicleValueToAreaScale * mapEntryValue ( e ) ; const icicleRowHeight = height / ( maxDepth - 1 ) ; const result = sunburst ( tree , icicleAreaAccessor , { x0 : 0 , y0 : - icicleRowHeight } , true , false , icicleRowHeight ) ; return icicleLayout ? result : result . map ( ( { y0 , y1 , ... rest } ) => ( { y0 : height - y1 , y1 : height - y0 , ... rest } ) ) ; default : return ( ( layout ) => layout ?? [ ] ) ( partitionLayout ) ; } } ; internal export function shapeViewModel ( textMeasure , spec , style , chartDimensions , rawTextGetter , valueGetter , tree , backgroundStyle , panelModel ) { const { layout , layers , topGroove , valueFormatter : specifiedValueFormatter , percentFormatter : specifiedPercentFormatter , fillOutside , clockwiseSectors , maxRowCount , specialFirstInnermostSector , animation , } = spec ; const { emptySizeRatio , outerSizeRatio , linkLabel , minFontSize , sectorLineWidth , sectorLineStroke , fillLabel , } = style ; const { width , height } = chartDimensions ; const { marginLeftPx , marginTopPx , panel } = panelModel ; const treemapLayout = isTreemap ( layout ) ; const mosaicLayout = isMosaic ( layout ) ; const sunburstLayout = isSunburst ( layout ) ; const icicleLayout = isIcicle ( layout ) ; const flameLayout = isFlame ( layout ) ; const simpleLinear = isSimpleLinear ( layout , fillLabel , layers ) ; const waffleLayout = isWaffle ( layout ) ; const diskCenter = isSunburst ( layout ) ? { x : marginLeftPx + panel . innerWidth / 2 , y : marginTopPx + panel . innerHeight / 2 , } : { x : marginLeftPx , y : marginTopPx , } ; if ( ! ( width > 0 ) || ! ( height > 0 ) || tree . length === 0 ) { return nullShapeViewModel ( layout , style , diskCenter ) ; } const longestPath = ( [ , { children , path } ] ) : number => children . length > 0 ? children . reduce ( ( p , n ) => Math . max ( p , longestPath ( n ) ) , 0 ) : path . length ; const maxDepth = longestPath ( tree [ 0 ] ) - 2 ; const childNodes = rawChildNodes ( layout , tree , topGroove , panel . innerWidth , panel . innerHeight , clockwiseSectors , specialFirstInnermostSector , maxDepth ) ; const shownChildNodes = childNodes . filter ( ( n ) => { const layerIndex = entryValue ( n . node ) . depth - 1 ; const layer = layers [ layerIndex ] ; return ! layer || ! layer . showAccessor || layer . showAccessor ( entryKey ( n . node ) ) ; } ) ; const circleMaximumSize = Math . min ( panel . innerWidth , panel . innerHeight - ( panel . title . length > 0 ? panel . fontSize * 2 : 0 ) ) ; const outerRadius = Math . min ( outerSizeRatio * circleMaximumSize , circleMaximumSize - sectorLineWidth ) / 2 ; const innerRadius = outerRadius - ( 1 - emptySizeRatio ) * outerRadius ; const treeHeight = shownChildNodes . reduce ( ( p , n ) => Math . max ( p , entryValue ( n . node ) . depth ) , 0 ) ; const ringThickness = ( outerRadius - innerRadius ) / treeHeight ; const partToShapeFn = partToShapeTreeNode ( ! sunburstLayout , innerRadius , ringThickness ) ; const quadViewModel = makeQuadViewModel ( shownChildNodes . slice ( 1 ) . map ( partToShapeFn ) , layers , sectorLineWidth , sectorLineStroke , panelModel . smAccessorValue , panelModel . index , panelModel . innerIndex , fillLabel , backgroundStyle ) ; const roomCondition = ( n ) => { const diff = n . x1 - n . x0 ; return sunburstLayout ? ( diff < 0 ? TAU + diff : diff ) * ringSectorMiddleRadius ( n ) > Math . max ( minFontSize , linkLabel . maximumSection ) : n . x1 - n . x0 > minFontSize && n . y1px - n . y0px > minFontSize ; } ; const nodesWithRoom = quadViewModel . filter ( roomCondition ) ; const outsideFillNodes = fillOutside && sunburstLayout ? nodesWithRoom : [ ] ; const textFillOrigins = nodesWithRoom . map ( sunburstLayout ? sectorFillOrigins ( fillOutside ) : rectangleFillOrigins ) ; const valueFormatter = valueGetter === percentValueGetter ? specifiedPercentFormatter : specifiedValueFormatter ; const getRowSets = sunburstLayout ? fillTextLayout ( ringSectorConstruction ( spec , style , innerRadius , ringThickness ) , getSectorRowGeometry , inSectorRotation ( style . horizontalTextEnforcer , style . horizontalTextAngleThreshold ) ) : simpleLinear || waffleLayout ? ( ) => [ ] : fillTextLayout ( rectangleConstruction ( treeHeight , treemapLayout || mosaicLayout ? topGroove : null ) , getRectangleRowGeometry , ( ) => 0 ) ; const rowSets = getRowSets ( textMeasure , rawTextGetter , valueGetter , valueFormatter , nodesWithRoom , style , layers , textFillOrigins , maxRowCount , ! sunburstLayout , ! ( treemapLayout || mosaicLayout ) ) ; const outsideLinksViewModel = makeOutsideLinksViewModel ( outsideFillNodes , rowSets , linkLabel . radiusPadding ) ; const currentY = [ - height , - height , - height , - height ] ; const nodesWithoutRoom = fillOutside || treemapLayout || mosaicLayout || icicleLayout || flameLayout || waffleLayout ? [ ] : quadViewModel . filter ( ( n ) => { const id = nodeId ( n ) ; const foundInFillText = rowSets . find ( ( r ) => r . id === id ) ; return ! ( foundInFillText && foundInFillText . rows . length > 0 ) ; } ) ; const maxLinkedLabelTextLength = style . linkLabel . maxTextLength ; const linkLabelViewModels = linkTextLayout ( panel . innerWidth , panel . innerHeight , textMeasure , style , nodesWithoutRoom , currentY , outerRadius , rawTextGetter , valueGetter , valueFormatter , maxLinkedLabelTextLength , { x : width * panelModel . left + panel . innerWidth / 2 , y : height * panelModel . top + panel . innerHeight / 2 , } , backgroundStyle ) ; const pickQuads = sunburstLayout ? ( x , y ) => { return quadViewModel . filter ( ( { x0 , y0px , x1 , y1px } ) => { const angleX = ( Math . atan2 ( y , x ) + TAU / 4 + TAU ) % TAU ; const yPx = Math . sqrt ( x * x + y * y ) ; return x0 <= angleX && angleX <= x1 && y0px <= yPx && yPx <= y1px ; } ) ; } : ( x , y , { currentFocusX0 , currentFocusX1 } ) => { return quadViewModel . filter ( ( { x0 , y0px , x1 , y1px } ) => { const scale = width / ( currentFocusX1 - currentFocusX0 ) ; const fx0 = Math . max ( ( x0 - currentFocusX0 ) * scale , 0 ) ; const fx1 = Math . min ( ( x1 - currentFocusX0 ) * scale , width ) ; return fx0 <= x && x < fx1 && y0px < y && y <= y1px ; } ) ; } ; return { layout , smAccessorValue : panelModel . smAccessorValue , index : panelModel . index , innerIndex : panelModel . innerIndex , width : panelModel . width , height : panelModel . height , top : panelModel . top , left : panelModel . left , innerRowCount : panelModel . innerRowCount , innerColumnCount : panelModel . innerColumnCount , innerRowIndex : panelModel . innerRowIndex , innerColumnIndex : panelModel . innerColumnIndex , marginLeftPx : panelModel . marginLeftPx , marginTopPx : panelModel . marginTopPx , panel : { ... panelModel . panel , } , style , layers , diskCenter , quadViewModel , rowSets , linkLabelViewModels , outsideLinksViewModel , pickQuads , outerRadius , chartDimensions , animation , } ; } function partToShapeTreeNode ( treemapLayout , innerRadius , ringThickness ) { return ( { node , x0 , x1 , y0 , y1 } ) : ShapeTreeNode => ( { dataName : entryKey ( node ) , depth : depthAccessor ( node ) , value : aggregateAccessor ( node ) , [ MODEL_KEY ] : parentAccessor ( node ) , sortIndex : sortIndexAccessor ( node ) , path : pathAccessor ( node ) , x0 , x1 , y0 , y1 , y0px : treemapLayout ? y0 : innerRadius + y0 * ringThickness , y1px : treemapLayout ? y1 : innerRadius + y1 * ringThickness , yMidPx : treemapLayout ? ( y0 + y1 ) / 2 : innerRadius + ( ( y0 + y1 ) / 2 ) * ringThickness , } ) ; }