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

commonMain.jetbrains.datalore.plot.builder.Plot.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.observable.event.EventHandler
import jetbrains.datalore.base.observable.property.PropertyChangeEvent
import jetbrains.datalore.base.observable.property.ReadableProperty
import jetbrains.datalore.base.observable.property.ValueProperty
import jetbrains.datalore.base.observable.property.WritableProperty
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.CoordinateSystem
import jetbrains.datalore.plot.base.Scale
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.coord.CoordProvider
import jetbrains.datalore.plot.builder.event.MouseEventPeer
import jetbrains.datalore.plot.builder.guide.Orientation
import jetbrains.datalore.plot.builder.interact.TooltipSpec
import jetbrains.datalore.plot.builder.layout.*
import jetbrains.datalore.plot.builder.layout.PlotLayoutUtil.liveMapBounds
import jetbrains.datalore.plot.builder.presentation.Style
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

abstract class Plot(private val theme: Theme) : SvgComponent() {

    private val myPreferredSize = ValueProperty(DEF_PLOT_SIZE)
    private val myLaidOutSize = ValueProperty(DoubleVector.ZERO)
    private val myTooltipHelper = PlotTooltipHelper()
    private val myLiveMapFigures = ArrayList()

    val mouseEventPeer = MouseEventPeer()

    protected abstract val scaleXProto: Scale

    protected abstract val scaleYProto: Scale

    protected abstract val title: String

    protected abstract val axisTitleLeft: String

    protected abstract val axisTitleBottom: String

    protected abstract val coordProvider: CoordProvider

    protected abstract val legendBoxInfos: List

    protected abstract val isAxisEnabled: Boolean

    abstract val isInteractionsEnabled: Boolean

    internal val liveMapFigures: List
        get() = myLiveMapFigures

    internal fun preferredSize(): WritableProperty {
        return myPreferredSize
    }

    fun laidOutSize(): ReadableProperty {
        return myLaidOutSize
    }

    protected abstract fun hasTitle(): Boolean

    protected abstract fun hasAxisTitleLeft(): Boolean

    protected abstract fun hasAxisTitleBottom(): Boolean

    protected abstract fun hasLiveMap(): Boolean

    protected abstract fun tileLayers(tileIndex: Int): List

    protected abstract fun plotLayout(): PlotLayout

    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 = myPreferredSize.get().y / 2 - 8
            for (s in messages) {
                val errorLabel = TextLabel(s)
                errorLabel.setHorizontalAnchor(HorizontalAnchor.MIDDLE)
                errorLabel.setVerticalAnchor(VerticalAnchor.CENTER)
                errorLabel.moveTo(myPreferredSize.get().x / 2, y)
                rootGroup.children().add(errorLabel.rootGroup)
                y += 16.0
            }
        }
    }

    private fun buildPlot() {
        rootGroup.addClass(Style.PLOT)
        buildPlotComponents()
        reg(myPreferredSize.addHandler(object : EventHandler> {
            override fun onEvent(event: PropertyChangeEvent) {
                val newValue = event.newValue
                if (newValue!!.x > 0 && newValue.y > 0) {
                    rebuildPlot()
                }
            }
        }))

        reg(object : Registration() {
            override fun doRemove() {
                myTooltipHelper.removeAllTileInfos()
                myLiveMapFigures.clear()
            }
        })
    }

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


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

        val xScale: Scale
        val yScale: Scale
        val coord: CoordinateSystem
        if (tileInfo.xAxisInfo != null && tileInfo.yAxisInfo != null) {
            val xDomain = tileInfo.xAxisInfo.axisDomain!!
            val xAxisLength = tileInfo.xAxisInfo.axisLength

            val yDomain = tileInfo.yAxisInfo.axisDomain!!
            val yAxisLength = tileInfo.yAxisInfo.axisLength

            // set-up scales and coordinate system
            xScale = coordProvider.buildAxisScaleX(scaleXProto, xDomain, xAxisLength, tileInfo.xAxisInfo.axisBreaks!!)
            yScale = coordProvider.buildAxisScaleY(scaleYProto, yDomain, yAxisLength, tileInfo.yAxisInfo.axisBreaks!!)
            coord = coordProvider.createCoordinateSystem(xDomain, xAxisLength, yDomain, yAxisLength)
        } else {
            // bogus scales and coordinate system (live map doesn't need them)
            xScale = BogusScale()
            yScale = BogusScale()
            coord = BogusCoordinateSystem()
        }

        val tile = PlotTile(tileLayers, xScale, yScale, tilesOrigin, tileInfo, coord, theme)
        tile.setShowAxis(isAxisEnabled)
        tile.debugDrawing().set(DEBUG_DRAWING)

        return tile
    }

    private fun createAxisTitle(
        text: String,
        orientation: Orientation,
        plotBounds: DoubleRectangle,
        geomBounds: DoubleRectangle
    ) {
        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.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 preferredSize = myPreferredSize.get()
        val overallRect = DoubleRectangle(DoubleVector.ZERO, preferredSize)

        @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 (hasLiveMap()) {
            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
        if (isAxisEnabled) {
            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 plotLayout = plotLayout()
        val plotInfo = plotLayout.doLayout(geomAndAxis.dimension)
        this.myLaidOutSize.set(preferredSize)

        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 tileTheme = if(plotInfo.tiles.size > 1) {
            theme.multiTile()
        } else {
            theme
        }

        val tilesOrigin = geomAndAxis.origin
        for (tileLayoutInfo in plotInfo.tiles) {
//        for (i in plotInfo.tiles.indices) {
//            val tileLayoutInfo = plotInfo.tiles[i]
            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), tileTheme)

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

            add(tile)

            tile.liveMapFigure?.let(myLiveMapFigures::add)

            val geomBoundsAbsolute = tileLayoutInfo.geomBounds.add(plotOriginAbsolute)
            myTooltipHelper.addTileInfo(geomBoundsAbsolute, tile.targetLocators)
        }

        @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.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 (isAxisEnabled) {
            if (hasAxisTitleLeft()) {
                createAxisTitle(
                    axisTitleLeft,
                    Orientation.LEFT,
                    withoutTitleAndLegends,
                    geomAreaBounds
                )
            }
            if (hasAxisTitleBottom()) {
                createAxisTitle(
                    axisTitleBottom,
                    Orientation.BOTTOM,
                    withoutTitleAndLegends,
                    geomAreaBounds
                )
            }
        }

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

    fun createTooltipSpecs(plotCoord: DoubleVector): List {
        return myTooltipHelper.createTooltipSpecs(plotCoord)
    }

    fun getGeomBounds(plotCoord: DoubleVector): DoubleRectangle? {
        return myTooltipHelper.getGeomBounds(plotCoord)
    }

    companion object {
        private val LOG = PortableLogging.logger(Plot::class)

        private val DEF_PLOT_SIZE = DoubleVector(600.0, 400.0)
        private const val DEBUG_DRAWING = PLOT_DEBUG_DRAWING
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy