All Downloads are FREE. Search and download functionalities are using the official Maven repository.

commonMain.jetbrains.datalore.plot.builder.PlotSvgComponent.kt Maven / Gradle / Ivy

There is a newer version: 4.5.3-alpha1
Show newest version
/*
 * Copyright (c) 2019. JetBrains s.r.o.
 * Use of this source code is governed by the MIT license that can be found in the LICENSE file.
 */

package jetbrains.datalore.plot.builder

import jetbrains.datalore.base.event.Event
import jetbrains.datalore.base.gcommon.base.Throwables
import jetbrains.datalore.base.geometry.DoubleRectangle
import jetbrains.datalore.base.geometry.DoubleVector
import jetbrains.datalore.base.logging.PortableLogging
import jetbrains.datalore.base.registration.Registration
import jetbrains.datalore.base.values.Color
import jetbrains.datalore.base.values.SomeFig
import jetbrains.datalore.plot.FeatureSwitch.PLOT_DEBUG_DRAWING
import jetbrains.datalore.plot.base.render.svg.SvgComponent
import jetbrains.datalore.plot.base.render.svg.TextLabel
import jetbrains.datalore.plot.base.render.svg.TextLabel.HorizontalAnchor
import jetbrains.datalore.plot.base.render.svg.TextLabel.VerticalAnchor
import jetbrains.datalore.plot.builder.event.MouseEventPeer
import jetbrains.datalore.plot.builder.guide.Orientation
import jetbrains.datalore.plot.builder.interact.PlotInteractor
import jetbrains.datalore.plot.builder.interact.PlotTooltipBounds
import jetbrains.datalore.plot.builder.layout.*
import jetbrains.datalore.plot.builder.layout.PlotLayoutUtil.liveMapBounds
import jetbrains.datalore.plot.builder.presentation.Defaults.DEF_PLOT_SIZE
import jetbrains.datalore.plot.builder.presentation.Style
import jetbrains.datalore.plot.builder.theme.AxisTheme
import jetbrains.datalore.plot.builder.theme.Theme
import jetbrains.datalore.vis.svg.SvgElement
import jetbrains.datalore.vis.svg.SvgGElement
import jetbrains.datalore.vis.svg.SvgNode
import jetbrains.datalore.vis.svg.SvgRectElement
import jetbrains.datalore.vis.svg.event.SvgEventHandler
import jetbrains.datalore.vis.svg.event.SvgEventSpec

class PlotSvgComponent constructor(
    private val title: String?,
    private val layersByTile: List>,
    private var plotLayout: PlotLayout,
    private val frameOfReferenceProvider: TileFrameOfReferenceProvider,
    private val legendBoxInfos: List,
    val interactionsEnabled: Boolean,
    val theme: Theme
) : SvgComponent() {

    val flippedAxis = frameOfReferenceProvider.flipAxis
    val mouseEventPeer = MouseEventPeer()

    var interactor: PlotInteractor? = null
        set(value) {
            require(field == null) { "Can be intialize only once." } // TODO: to not waste time on an exhaustive Disposable lifecycle control
            field = value

            // PlotInteractor controls subscribtions and clears them out on Dispose
            value?.apply {
                onViewReset {
                    println("onViewReset()")
                }

                onViewPanning {
                    println("onViewPanning($it)")
                }

                onViewZoomArea {
                    println("onViewArea($it)")
                }

                onViewZoomIn {
                    println("onViewZoomIn($it)")
                }
            }
        }

    internal var liveMapFigures: List = emptyList()
        private set

    var plotSize: DoubleVector = DEF_PLOT_SIZE
        private set

    private fun hasTitle(): Boolean {
        return !title.isNullOrBlank()
    }

    // ToDo: remove
    private val axisTitleLeft: String
        get() {
            require(hasAxisTitleLeft()) { "No left axis title" }
            return frameOfReferenceProvider.vAxisLabel!!
        }

    // ToDo: remove
    private val axisTitleBottom: String
        get() {
            require(hasAxisTitleBottom()) { "No bottom axis title" }
            return frameOfReferenceProvider.hAxisLabel!!
        }

    // ToDo: remove
    private fun hasAxisTitleLeft(): Boolean {
        return !frameOfReferenceProvider.vAxisLabel.isNullOrEmpty()
    }

    // ToDo: remove
    private fun hasAxisTitleBottom(): Boolean {
        return !frameOfReferenceProvider.hAxisLabel.isNullOrEmpty()
    }

    private fun tileLayers(tileIndex: Int): List {
        return layersByTile[tileIndex]
    }

    private val containsLiveMap: Boolean = layersByTile.flatten().any(GeomLayer::isLiveMap)

    override fun buildComponent() {
        try {
            buildPlot()
        } catch (e: RuntimeException) {
            LOG.error(e) { "buildPlot" }

            val rootCause = Throwables.getRootCause(e)
            val messages = arrayOf(
                "Error building plot: " + rootCause::class.simpleName, if (rootCause.message != null)
                    "'" + rootCause.message + "'"
                else
                    ""
            )
            var y = plotSize.y / 2 - 8
            for (s in messages) {
                val errorLabel = TextLabel(s)
                errorLabel.setHorizontalAnchor(HorizontalAnchor.MIDDLE)
                errorLabel.setVerticalAnchor(VerticalAnchor.CENTER)
                errorLabel.moveTo(plotSize.x / 2, y)
                rootGroup.children().add(errorLabel.rootGroup)
                y += 16.0
            }
        }
    }

    private fun buildPlot() {
        rootGroup.addClass(Style.PLOT)
        buildPlotComponents()

        reg(object : Registration() {
            override fun doRemove() {
                interactor?.dispose()
                liveMapFigures = emptyList()
            }
        })
    }

    fun resize(plotSize: DoubleVector) {
        if (plotSize.x <= 0 || plotSize.y <= 0) return
        if (plotSize == this.plotSize) return

        this.plotSize = plotSize

        // just invalidate
        clear()
    }


//    private fun rebuildPlot() {
//        clear()
//        buildPlot()
//    }


    private fun createTile(
        tilesOrigin: DoubleVector,
        tileInfo: TileLayoutInfo,
        tileLayers: List,
        theme: Theme,
    ): PlotTile {

        val frameOfReference: TileFrameOfReference = frameOfReferenceProvider.createFrameOfReference(
            tileInfo,
            DEBUG_DRAWING
        )
        val tile = PlotTile(tileLayers, tilesOrigin, tileInfo, theme, frameOfReference)
        tile.isDebugDrawing = DEBUG_DRAWING
        return tile
    }

    private fun createAxisTitle(
        text: String,
        orientation: Orientation,
        plotBounds: DoubleRectangle,
        geomBounds: DoubleRectangle,
        axisTheme: AxisTheme
    ) {
        val horizontalAnchor = HorizontalAnchor.MIDDLE
        val verticalAnchor: VerticalAnchor = when (orientation) {
            Orientation.LEFT, Orientation.RIGHT, Orientation.TOP -> VerticalAnchor.TOP
            Orientation.BOTTOM -> VerticalAnchor.BOTTOM
        }

        val titleLocation: DoubleVector
        var rotation = 0.0
        when (orientation) {
            Orientation.LEFT -> {
                titleLocation =
                    DoubleVector(plotBounds.left + PlotLayoutUtil.AXIS_TITLE_OUTER_MARGIN, geomBounds.center.y)
                rotation = -90.0
            }
            Orientation.RIGHT -> {
                titleLocation =
                    DoubleVector(plotBounds.right - PlotLayoutUtil.AXIS_TITLE_OUTER_MARGIN, geomBounds.center.y)
                rotation = 90.0
            }
            Orientation.TOP -> titleLocation =
                DoubleVector(geomBounds.center.x, plotBounds.top + PlotLayoutUtil.AXIS_TITLE_OUTER_MARGIN)
            Orientation.BOTTOM -> titleLocation =
                DoubleVector(geomBounds.center.x, plotBounds.bottom - PlotLayoutUtil.AXIS_TITLE_OUTER_MARGIN)
        }

        val titleLabel = TextLabel(text)
        titleLabel.setHorizontalAnchor(horizontalAnchor)
        titleLabel.setVerticalAnchor(verticalAnchor)
        titleLabel.textColor().set(axisTheme.titleColor())
        titleLabel.moveTo(titleLocation)
        titleLabel.rotate(rotation)

        val titleElement = titleLabel.rootGroup
        titleElement.addClass(Style.AXIS_TITLE)

        // hack: we have style: ".axis .title text" and we don't want to break backward-compatibility with 'census' charts
        val parent = SvgGElement()
        parent.addClass(Style.AXIS)

        parent.children().add(titleElement)

        add(parent)
    }

    private fun onMouseMove(e: SvgElement, message: String) {
        e.addEventHandler(SvgEventSpec.MOUSE_MOVE, object :
            SvgEventHandler {
            override fun handle(node: SvgNode, e: Event) {
                println(message)
            }
        })
    }

    private fun buildPlotComponents() {
        val overallRect = DoubleRectangle(DoubleVector.ZERO, plotSize)

        @Suppress("ConstantConditionIf")
        if (DEBUG_DRAWING) {
            val rect = SvgRectElement(overallRect)
            rect.strokeColor().set(Color.MAGENTA)
            rect.strokeWidth().set(1.0)
            rect.fillOpacity().set(0.0)
            onMouseMove(rect, "MAGENTA: preferred size: $overallRect")
            add(rect)
        }

        // compute geom bounds
        val entirePlot = if (containsLiveMap) {
            liveMapBounds(overallRect)
        } else {
            overallRect
        }

        // subtract title size
        val withoutTitle = if (hasTitle()) {
            val titleSize = PlotLayoutUtil.titleDimensions(title!!)
            DoubleRectangle(
                entirePlot.origin.add(DoubleVector(0.0, titleSize.y)),
                entirePlot.dimension.subtract(DoubleVector(0.0, titleSize.y))
            )
        } else {
            entirePlot
        }

        // adjust for legend boxes
        var boxesLayoutResult: LegendBoxesLayout.Result? = null
        val legendTheme = theme.legend()
        val withoutTitleAndLegends = if (legendTheme.position().isFixed) {
            val legendBoxesLayout =
                LegendBoxesLayout(withoutTitle, legendTheme)
            boxesLayoutResult = legendBoxesLayout.doLayout(legendBoxInfos)
            boxesLayoutResult.plotInnerBoundsWithoutLegendBoxes
        } else {
            withoutTitle
        }

        @Suppress("ConstantConditionIf")
        if (DEBUG_DRAWING) {
            val rect = SvgRectElement(withoutTitleAndLegends)
            rect.strokeColor().set(Color.BLUE)
            rect.strokeWidth().set(1.0)
            rect.fillOpacity().set(0.0)
            onMouseMove(rect, "BLUE: plot without title and legends: $withoutTitleAndLegends")
            add(rect)
        }

        // subtract left axis title width
        var geomAndAxis = withoutTitleAndLegends
        val axisEnabled = !containsLiveMap
        if (axisEnabled) {
            if (hasAxisTitleLeft()) {
                val titleSize = PlotLayoutUtil.axisTitleDimensions(axisTitleLeft)
                val thickness =
                    titleSize.y + PlotLayoutUtil.AXIS_TITLE_OUTER_MARGIN + PlotLayoutUtil.AXIS_TITLE_INNER_MARGIN
                geomAndAxis = DoubleRectangle(
                    geomAndAxis.left + thickness, geomAndAxis.top,
                    geomAndAxis.width - thickness, geomAndAxis.height
                )
            }

            // subtract bottom axis title height
            if (hasAxisTitleBottom()) {
                val titleSize = PlotLayoutUtil.axisTitleDimensions(axisTitleBottom)
                val thickness =
                    titleSize.y + PlotLayoutUtil.AXIS_TITLE_OUTER_MARGIN + PlotLayoutUtil.AXIS_TITLE_INNER_MARGIN
                geomAndAxis = DoubleRectangle(
                    geomAndAxis.left, geomAndAxis.top,
                    geomAndAxis.width, geomAndAxis.height - thickness
                )
            }
        }

        // Layout plot inners
        val plotInfo = plotLayout.doLayout(geomAndAxis.dimension)

        if (plotInfo.tiles.isEmpty()) {
            return
        }

        val geomAreaBounds = PlotLayoutUtil.absoluteGeomBounds(geomAndAxis.origin, plotInfo)
        if (legendTheme.position().isOverlay) {
            // put 'overlay' in 'geom' bounds
            val legendBoxesLayout = LegendBoxesLayout(geomAreaBounds, legendTheme)
            boxesLayoutResult = legendBoxesLayout.doLayout(legendBoxInfos)
        }

        // build tiles
        val tilesOrigin = geomAndAxis.origin
        for (tileLayoutInfo in plotInfo.tiles) {
            val tileLayersIndex = tileLayoutInfo.trueIndex

//            println("plot offset: " + tileInfo.plotOffset)
//            println("     bounds: " + tileInfo.bounds)
//            println("geom bounds: " + tileInfo.geomBounds)
//            println("clip bounds: " + tileInfo.clipBounds)
            val tile = createTile(tilesOrigin, tileLayoutInfo, tileLayers(tileLayersIndex), theme)

            val plotOriginAbsolute = tilesOrigin.add(tileLayoutInfo.plotOrigin)
            tile.moveTo(plotOriginAbsolute)

            add(tile)

            tile.liveMapFigure?.run {
                liveMapFigures = liveMapFigures + listOf(this)
            }

            val geomBoundsAbsolute = tileLayoutInfo.geomBounds.add(plotOriginAbsolute)
            val tooltipBounds = PlotTooltipBounds(
                placementArea = geomBoundsAbsolute,
                handlingArea = tile.geomDrawingBounds.add(geomBoundsAbsolute.origin)
            )
            interactor?.onTileAdded(geomBoundsAbsolute, tooltipBounds, tile.targetLocators)

            @Suppress("ConstantConditionIf")
            if (DEBUG_DRAWING) {
                val rect = SvgRectElement(tooltipBounds.handlingArea)
                rect.strokeColor().set(Color.ORANGE)
                rect.strokeWidth().set(1.0)
                rect.fillOpacity().set(0.0)
                add(rect)
            }
        }

        @Suppress("ConstantConditionIf")
        if (DEBUG_DRAWING) {
            val rect = SvgRectElement(geomAreaBounds)
            rect.strokeColor().set(Color.RED)
            rect.strokeWidth().set(1.0)
            rect.fillOpacity().set(0.0)
            add(rect)
        }

        // add plot title
        if (hasTitle()) {
            val titleLabel = TextLabel(title!!)
            titleLabel.addClassName(Style.PLOT_TITLE)
            titleLabel.textColor().set(theme.plot().titleColor())
            titleLabel.setHorizontalAnchor(HorizontalAnchor.LEFT)
            titleLabel.setVerticalAnchor(VerticalAnchor.CENTER)

            val titleSize = PlotLayoutUtil.titleDimensions(title)
            val titleBounds = DoubleRectangle(geomAreaBounds.origin.x, 0.0, titleSize.x, titleSize.y)
            titleLabel.moveTo(DoubleVector(titleBounds.left, titleBounds.center.y))
            add(titleLabel)

            @Suppress("ConstantConditionIf")
            if (DEBUG_DRAWING) {
                val rect = SvgRectElement(titleBounds)
                rect.strokeColor().set(Color.BLUE)
                rect.strokeWidth().set(1.0)
                rect.fillOpacity().set(0.0)
                add(rect)
            }
        }

        // add axis titles
        if (axisEnabled) {
            if (hasAxisTitleLeft()) {
                createAxisTitle(
                    axisTitleLeft,
                    Orientation.LEFT,
                    withoutTitleAndLegends,
                    geomAreaBounds,
                    theme.axisY(flippedAxis)
                )
            }
            if (hasAxisTitleBottom()) {
                createAxisTitle(
                    axisTitleBottom,
                    Orientation.BOTTOM,
                    withoutTitleAndLegends,
                    geomAreaBounds,
                    theme.axisX(flippedAxis)
                )
            }
        }

        // add legends
        if (boxesLayoutResult != null) {
            for (boxWithLocation in boxesLayoutResult.boxWithLocationList) {
                val legendBox = boxWithLocation.legendBox.createLegendBox()
                legendBox.moveTo(boxWithLocation.location)
                add(legendBox)
            }
        }
    }

    companion object {
        private val LOG = PortableLogging.logger(PlotSvgComponent::class)
        private const val DEBUG_DRAWING = PLOT_DEBUG_DRAWING
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy