import * as d3 from "d3" ; import { DataViewRow , ReadableProxy , Size } from "spotfire-api" ; import { highlight , markingHandler , markRowforAllTimes } from "./modutils" ; import { Grid } from "./Grid" ; import { animationControl } from "./animationControl" ; import { setIdle } from "./interactionLock" ; /**
 * Constants
 */ const defaultTransitionSpeed = 100 , relativeMarkerSize = 0.1 , defaultOpacity = 0.8 , markingOpacity = 0.95 , defaultAnimationControlHeight = 32 , yaxiswidth = 60 , xaxisheight = 32 , displayedRowsLimit = 100000 ; /**
 * Create the graphical elements with attributes that will remain the same through the lifetime of the
 * visualization in order to minimize unnecessary creation of elements in the main
 * interactive visualization loop. Drawing order are controlled via svg group (g) elements.
 */ const modContainer = d3 . select ( "#mod-container" ) ; const svg = modContainer . append ( "svg" ) . attr ( "xmlns" , "http://www.w3.org/2000/svg" ) ; const guideLayer = svg . append ( "g" ) . attr ( "id" , "guideLayer" ) ; const xAxisScaleGuideGroup = guideLayer . append ( "g" ) . attr ( "id" , "xAxisScaleGuideGroup" ) . style ( "cursor" , "default" ) . style ( "user-select" , "none" ) ; const yAxisScaleGuideGroup = guideLayer . append ( "g" ) . attr ( "id" , "yAxisScaleGuideGroup" ) . style ( "cursor" , "default" ) . style ( "user-select" , "none" ) ; const animationControlGroup = guideLayer . append ( "g" ) . attr ( "id" , "animationControlGroup" ) . style ( "cursor" , "default" ) . style ( "user-select" , "none" ) ; const backgroundLayer = svg . append ( "g" ) . attr ( "id" , "backgroundLayer" ) ; const interactionLayer = svg . append ( "g" ) . attr ( "id" , "interactionLayer" ) . attr ( "fill" , "white" ) . attr ( "fill-opacity" , 0.0 ) ; const xAxisScaleGuideArea = interactionLayer . append ( "rect" ) . attr ( "id" , "xAxisScaleGuideArea" ) ; const yAxisScaleGuideArea = interactionLayer . append ( "rect" ) . attr ( "id" , "yAxisScaleGuideArea" ) ; const markingArea = interactionLayer . append ( "rect" ) . attr ( "id" , "markingArea" ) ; const markerLayer = svg . append ( "g" ) . attr ( "id" , "markerLayer" ) ; const markingOverlay = svg . append ( "rect" ) . attr ( "id" , "markingOverlay" ) . attr ( "class" , "inactiveMarking" ) ; const messageLayer = svg . append ( "g" ) . attr ( "id" , "messageOverlay" ) ; let animationControlInstance ; type FIX_TYPE = any ; export function clearCanvas ( ) { markerLayer . selectAll ( "*" ) . remove ( ) ; } export async function render ( dataView , windowSize , toolTipDisplayAxes , mod , showLabels , xLogScale , yLogScale , animationSpeedProperty , animationSpeed , onScaleClick ) { animationControlInstance = animationControlInstance || animationControl ( animationSpeedProperty ) ; let context = mod . getRenderContext ( ) ; document . querySelector ( "#extra_styling" ) ! . innerHTML = `
    .tick { color: ${ context . styling . scales . tick . stroke } }
    .domain { color: ${ context . styling . scales . line . stroke } }
    .bottomGraphBorder { color: ${ context . styling . scales . line . stroke } }
    .leftGraphBorder { color: ${ context . styling . scales . line . stroke } }
    .animation-tray { fill: ${ context . styling . scales . line . stroke } }
    .playbutton { fill: ${ context . styling . scales . line . stroke } }
    .animationSpeed { fill: ${ context . styling . scales . line . stroke } }
    .dot.marked { stroke: ${ context . styling . general . font . color }}
    .label { fill: ${ context . styling . general . font . color }; font-size: ${ context . styling . general . font . fontSize }px; font-weight: ${ context . styling . general . font . fontWeight }; font-style: ${ context . styling . general . font . fontStyle };}
    .message { fill: ${ context . styling . general . font . color }; font-size: ${ context . styling . general . font . fontSize }px; font-weight: ${ context . styling . general . font . fontWeight }; font-style: ${ context . styling . general . font . fontStyle };}
    #animationSpeedLabel { color: ${ context . styling . general . font . color }; font-size: ${ context . styling . general . font . fontSize }px; font-weight: ${ context . styling . general . font . fontWeight }; font-style: ${ context . styling . general . font . fontStyle };}
    ` ; const hasSize = ! ! ( await dataView . continuousAxis ( "Size" ) ) ; const hasX = ! ! ( await dataView . continuousAxis ( "X" ) ) ; const hasY = ! ! ( await dataView . continuousAxis ( "Y" ) ) ; const rows = ( await dataView . allRows ( ) ) ?. filter ( rowFilter ) ; if ( ! rows || rows . length < 1 ) { clearCanvas ( ) ; return ; } rows . forEach ( ( r ) => ( ( r as any ) . key = r . elementId ( true , [ "AnimateBy" ] ) ) ) ; let animateLeaves = ( await ( await dataView . hierarchy ( "AnimateBy" ) ) ?. root ( ) ) ?. leaves ( ) ; if ( ! animateLeaves || animateLeaves . length == 0 ) { clearCanvas ( ) ; return ; } let modHeight = windowSize . height ; let modWidth = windowSize . width ; let maxMarkerSize = ( relativeMarkerSize * Math . min ( modHeight , modWidth ) ) / 2 ; let innerMargin = maxMarkerSize ; let animationControlHeight = 0 ; if ( animateLeaves . length > 1 ) { animationControlHeight = defaultAnimationControlHeight ; } else { animationControlHeight = 0 ; } let grid = new Grid ( modWidth , modHeight , `${ yaxiswidth }px ${ innerMargin }px 1fr ${ innerMargin }px` , `${ innerMargin }px 1fr ${ innerMargin }px ${ xaxisheight }px ${ animationControlHeight }px` ) ; let xAxesArea = grid . getCoords ( "c4" ) ; let yAxisArea = grid . getCoords ( "a2" ) ; let bubbleArea = grid . getCoords ( "b1:d3" ) ; let bubbleDrawingArea = grid . getCoords ( "c2" ) ; let animationControlArea = grid . getCoords ( "b5:d5" ) ; svg . attr ( "viewBox" , "0 0 " + modWidth + " " + modHeight ) ; async function minValue ( axis ) { if ( ! ( await dataView . continuousAxis ( axis ) ) ) { return 0 ; } return rows ! . map ( ( r ) => r . continuous < number > ( axis ) . value ( ) || 0 ) . reduce ( ( p , c ) => ( c < p ? c : p ) , Infinity ) ; } async function maxValue ( axis ) { if ( ! ( await dataView . continuousAxis ( axis ) ) ) { return 0 ; } return rows ! . map ( ( r ) => r . continuous < number > ( axis ) . value ( ) || 0 ) . reduce ( ( p , c ) => ( c > p ? c : p ) , - Infinity ) ; } var xmin = await minValue ( "X" ) ; var xmax = await maxValue ( "X" ) ; var xScale = xLogScale ? d3 . scaleLog ( ) . domain ( [ xmin , xmax ] ) . range ( [ 0 , xAxesArea . width ] ) : d3 . scaleLinear ( ) . domain ( [ xmin , xmax ] ) . range ( [ 0 , xAxesArea . width ] ) . nice ( ) ; xAxisScaleGuideGroup . attr ( "transform" , `translate(${ xAxesArea . x1 },${ xAxesArea . y1 })` ) . call ( d3 . axisBottom ( xScale ) . ticks ( 10 , "r" ) ) ; let bottomGraphBorder = guideLayer . select ( ".bottomGraphBorder" ) ; if ( bottomGraphBorder . empty ( ) ) bottomGraphBorder = guideLayer . append ( "line" ) . attr ( "class" , "bottomGraphBorder" ) ; bottomGraphBorder . attr ( "x1" , bubbleArea . x1 ) . attr ( "y1" , bubbleArea . y2 + 0.5 ) . attr ( "x2" , bubbleDrawingArea . x2 ) . attr ( "y2" , bubbleArea . y2 + 0.5 ) . attr ( "stroke-width" , 1 ) . attr ( "stroke" , "currentColor" ) ; xAxisScaleGuideArea . on ( "contextmenu" , function ( ) { onScaleClick ( ... d3 . mouse ( document . body ) , "X" ) ; d3 . event . preventDefault ( ) ; d3 . event . stopPropagation ( ) ; return false ; } ) . attr ( "x" , xAxesArea . x1 ) . attr ( "y" , xAxesArea . y1 ) . attr ( "width" , xAxesArea . width ) . attr ( "height" , xAxesArea . height ) . call ( markingHandler ( svg , markingOverlay , markerLayer , dataView , [ undefined , 1 , undefined , bubbleArea . height , ] ) as FIX_TYPE ) ; var ymin = await minValue ( "Y" ) ; var ymax = await maxValue ( "Y" ) ; var yScale = yLogScale ? d3 . scaleLog ( ) . domain ( [ ymin , ymax ] ) . range ( [ yAxisArea . height , 0 ] ) : d3 . scaleLinear ( ) . domain ( [ ymin , ymax ] ) . range ( [ yAxisArea . height , 0 ] ) . nice ( ) ; yAxisScaleGuideGroup . attr ( "transform" , `translate(${ yAxisArea . x2 },${ yAxisArea . y1 })` ) . call ( d3 . axisLeft ( yScale ) . ticks ( 10 , "r" ) ) ; var leftGraphBorder = guideLayer . select ( ".leftGraphBorder" ) ; if ( leftGraphBorder . empty ( ) ) leftGraphBorder = guideLayer . append ( "line" ) . attr ( "class" , "leftGraphBorder" ) ; leftGraphBorder . attr ( "x1" , bubbleArea . x1 + 0.5 ) . attr ( "y1" , bubbleDrawingArea . y1 ) . attr ( "x2" , bubbleArea . x1 + 0.5 ) . attr ( "y2" , bubbleArea . y2 ) . attr ( "stroke-width" , 1 ) . attr ( "stroke" , "currentColor" ) ; yAxisScaleGuideArea . on ( "contextmenu" , function ( ) { onScaleClick ( ... d3 . mouse ( document . body ) , "Y" ) ; d3 . event . preventDefault ( ) ; d3 . event . stopPropagation ( ) ; return false ; } ) . attr ( "x" , yAxisArea . x1 ) . attr ( "y" , yAxisArea . y1 ) . attr ( "width" , yAxisArea . width ) . attr ( "height" , yAxisArea . height ) . call ( markingHandler ( svg , markingOverlay , markerLayer , dataView , [ bubbleArea . x1 + 0.5 , undefined , bubbleArea . width - 0.5 , undefined , ] ) as FIX_TYPE ) ; markingArea . attr ( "x" , bubbleArea . x1 ) . attr ( "y" , bubbleArea . y1 ) . attr ( "width" , bubbleArea . width ) . attr ( "height" , bubbleArea . height ) . on ( "click" , ( ) => { if ( ! d3 . event . defaultPrevented ) { dataView . clearMarking ( ) ; } } ) . call ( markingHandler ( svg , markingOverlay , markerLayer , dataView ) as FIX_TYPE ) ; var smin = await minValue ( "Size" ) ; var smax = await maxValue ( "Size" ) ; var sizeScale = d3 . scaleSqrt ( ) . domain ( [ smin , smax ] ) . range ( [ 2 , maxMarkerSize ] ) ; let animationEnabled = animateLeaves . length > 1 ; let animationIndex = 0 ; if ( animationEnabled ) { animationControlGroup . attr ( "class" , "enabled" ) ; let animationScale = d3 . scaleLinear ( ) . domain ( [ 0 , animateLeaves . length - 1 ] ) . range ( [ 0 , animationControlArea . width ] ) . clamp ( true ) ; animationControlInstance . update ( animationScale , AnimationValueChanged , animationSpeed ) ; animationIndex = animationControlInstance ?. getIndex ( ) || 0 ; animationControlGroup . attr ( "transform" , `translate (${ animationControlArea . x1 } ${ animationControlArea . y1 })` ) . call ( animationControlInstance . render ) ; d3 . scaleLinear ( ) . tickFormat ( 5 ) ; } else { animationControlGroup . attr ( "class" , "disabled" ) ; } await updateBubbleChart ( animationControlInstance . isPlaying ( ) ? animationSpeed : 0 ) ; async function AnimationValueChanged ( value , changedByUser ) { animationIndex = value ; try { if ( changedByUser ) { await updateBubbleChart ( ) ; } else { await updateBubbleChart ( animationSpeed ) ; } } catch ( err ) { console . log ( err ) ; } } function getKey ( row ) { return row . key ; } /** Filter out rows that can't be visualized.
     * This includes null values and 0 for log scales */ function rowFilter ( row ) { if ( ! hasX || ( xLogScale && ! row . continuous ( "X" ) . value ( ) ) || row . continuous ( "X" ) . value ( ) == null ) { return false ; } if ( ! hasY || ( yLogScale && ! row . continuous ( "Y" ) . value ( ) ) || row . continuous ( "Y" ) . value ( ) == null ) { return false ; } return true ; } function showMessage ( message ) { messageLayer . selectAll ( "*" ) . remove ( ) ; messageLayer . append ( "text" ) . attr ( "class" , "message" ) . attr ( "x" , bubbleDrawingArea . x1 ) . attr ( "y" , bubbleDrawingArea . y2 ) . attr ( "dy" , ".3em" ) . text ( message ) ; } function hideMessage ( ) { messageLayer . selectAll ( "*" ) . remove ( ) ; } function updateBubbleChart ( transitionDuration = defaultTransitionSpeed ) { if ( ( animateLeaves ?. length || 0 ) <= animationIndex ) { return Promise . resolve ( ) ; } let displayRows = animateLeaves ! [ animationIndex ] . rows ( ) . filter ( rowFilter ) ; let rowCount = displayRows . length ; if ( rowCount > displayedRowsLimit ) { markerLayer . selectAll ( "*" ) . remove ( ) ; showMessage ( `☹️ Cannot render - too many bubbles (Number of bubbles: ${ rowCount }, limit: ${ displayedRowsLimit }) ` ) ; return Promise . resolve ( ) ; } hideMessage ( ) ; if ( hasSize ) { displayRows . sort ( sortContinuousAxis ( "Size" ) ) ; } let allOrNoneMarked = ! ( displayRows . find ( ( r ) => r . isMarked ( ) ) && displayRows . find ( ( r ) => ! r . isMarked ( ) ) ) ; let opacity = allOrNoneMarked ? defaultOpacity : markingOpacity ; let hl = highlight ( mod , toolTipDisplayAxes ) ; markerLayer . attr ( "transform" , `translate (${ bubbleDrawingArea . x1 } ${ bubbleDrawingArea . y1 })` ) ; updateLabels ( ) ; updateBackground ( ) ; return updateDots ( ) ; function updateBackground ( ) { let bgText = animateLeaves ! . length > 1 ? animateLeaves ! [ animationIndex ] . formattedPath ( ) : "" ; backgroundLayer . selectAll ( ".backgroundText" ) . data ( [ null ] ) . enter ( ) . append ( "text" ) . attr ( "class" , "backgroundText" ) ; let fontSize = Math . min ( ( 1.8 * bubbleDrawingArea . width ) / bgText . length , bubbleDrawingArea . height ) ; backgroundLayer . selectAll ( ".backgroundText" ) . attr ( "x" , bubbleDrawingArea . x1 + bubbleDrawingArea . width / 2 ) . attr ( "y" , bubbleDrawingArea . y1 + bubbleDrawingArea . height / 2 ) . attr ( "text-anchor" , "middle" ) . attr ( "alignment-baseline" , "middle" ) . style ( "color" , context . styling . scales . font . color ) . style ( "font-size" , fontSize + "px" ) . text ( bgText ) ; } function updateDots ( ) { let dots = markerLayer . selectAll < SVGCircleElement , DataViewRow > ( ".dot" ) . data ( displayRows , getKey ) ; let newdots = dots . enter ( ) . append ( "circle" ) . attr ( "cx" , ( row ) => xScale ( row . continuous ( "X" ) . value < number > ( ) || 0 ) || 0 ) . attr ( "cy" , ( row ) => yScale ( row . continuous ( "Y" ) . value < number > ( ) || 0 ) || 0 ) . attr ( "r" , 0 ) . attr ( "fill" , "transparent" ) ; let allDots = dots . merge ( newdots ) ; let transitionEnd = allDots . attr ( "id" , getKey ) . attr ( "class" , ( r ) => `dot ${ r . isMarked ( ) ? "marked" : "" }` ) . call ( markingHandler ( svg , markingOverlay , markerLayer , dataView ) as FIX_TYPE ) . style ( "fill-opacity" , opacity ) . transition ( ) . ease ( d3 . easeLinear ) . duration ( transitionDuration ) . attr ( "cx" , ( row ) => xScale ( Number ( row . continuous < number > ( "X" ) . value ( ) ) ) || 0 ) . attr ( "cy" , ( row ) => yScale ( Number ( row . continuous < number > ( "Y" ) . value ( ) ) ) || 0 ) . attr ( "r" , radius ) . style ( "fill" , ( row ) => row . color ( ) . hexCode ) . end ( ) . catch ( ( ) => { } ) ; allDots . on ( "click" , function ( row ) { setIdle ( ) ; markRowforAllTimes ( row , dataView , d3 . event . ctrlKey || d3 . event . metaKey ) ; d3 . event . preventDefault ( ) ; } ) . call ( hl ) ; allDots . filter ( ( row ) => row . isMarked ( ) ) . raise ( ) ; dots . exit ( ) . transition ( ) . duration ( transitionDuration ) . style ( "fill-opacity" , 0 ) . attr ( "r" , 0 ) . remove ( ) ; return transitionEnd ; } function radius ( row ) { return hasSize ? sizeScale ( Number ( row . continuous < number > ( "Size" ) . value ( ) ) ) || 0 : 8 ; } function updateLabels ( ) { const labelDX = ( row ) => radius ( row ) + ( xScale ( row . continuous ( "X" ) . value < number > ( ) || 0 ) || 0 ) ; const labelDY = ( row ) => ( yScale ( Number ( row . continuous < number > ( "Y" ) . value ( ) ) ) || 0 ) - radius ( row ) ; const labelText = ( row ) => { return showLabels && row . isMarked ( ) ? row . leafNode ( "MarkerBy" ) ! . formattedPath ( ) : "" ; } ; let labels = markerLayer . selectAll < SVGTextElement , DataViewRow > ( ".label" ) . data ( displayRows , ( d ) => getKey ( d ) + "_label" ) ; let newLabels = labels . enter ( ) . append ( "text" ) . attr ( "dx" , labelDX ) . attr ( "dy" , labelDY ) . text ( labelText ) ; let allLabels = labels . merge ( newLabels ) ; allLabels . attr ( "id" , ( d ) => getKey ( d ) + "_label" ) . attr ( "class" , ( r ) => `label ${ r . isMarked ( ) ? "marked" : "" }` ) . call ( markingHandler ( svg , markingOverlay , markerLayer , dataView ) as FIX_TYPE ) . style ( "fill-opacity" , opacity ) . transition ( ) . ease ( d3 . easeLinear ) . duration ( transitionDuration ) . attr ( "dx" , labelDX ) . attr ( "dy" , labelDY ) . text ( labelText ) ; allLabels . on ( "click" , function ( row ) { markRowforAllTimes ( row , dataView , d3 . event . ctrlKey || d3 . event . metaKey ) ; } ) . call ( hl ) ; allLabels . filter ( ( row ) => row . isMarked ( ) ) . raise ( ) ; labels . exit ( ) . remove ( ) ; } } } function sortContinuousAxis ( axisName ) { return ( a , b ) => ( b . continuous < number > ( axisName ) . value ( ) || 0 ) - ( a . continuous < number > ( axisName ) . value ( ) || 0 ) ; }