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

commonMain.jetbrains.datalore.plot.builder.guide.AxisComponent.kt Maven / Gradle / Ivy

There is a newer version: 4.5.3-alpha1
Show newest version
/*
 * Copyright (c) 2020. 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.guide

import jetbrains.datalore.base.geometry.DoubleVector
import jetbrains.datalore.base.observable.event.EventSource
import jetbrains.datalore.base.observable.event.EventSources
import jetbrains.datalore.base.observable.property.Property
import jetbrains.datalore.base.observable.property.PropertyBinding.bindOneWay
import jetbrains.datalore.base.observable.property.PropertyChangeEvent
import jetbrains.datalore.base.observable.property.ValueProperty
import jetbrains.datalore.base.values.Color
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.presentation.Style
import jetbrains.datalore.vis.svg.SvgGElement
import jetbrains.datalore.vis.svg.SvgLineElement
import jetbrains.datalore.vis.svg.SvgUtils.transformTranslate

class AxisComponent(length: Double, orientation: Orientation) : SvgComponent() {

    val breaks: Property?> = ValueProperty(null)
    val labels: Property?> = ValueProperty(null)
    // layout
    val tickLabelRotationDegree: Property = ValueProperty(0.0)
    val tickLabelHorizontalAnchor: Property
    // todo: minorBreaks
    val tickLabelVerticalAnchor: Property
    val tickLabelSmallFont: Property = ValueProperty(false)
    val tickLabelOffsets: Property?> = ValueProperty(null)  // optional
    val gridLineColor: Property = ValueProperty(Color.LIGHT_GRAY)
    val lineWidth: Property = ValueProperty(1.0)
    val gridLineWidth: Property = ValueProperty(1.0)
    val gridLineLength: Property = ValueProperty(0.0)
    val tickMarkWidth: Property = ValueProperty(1.0)
    val tickMarkLength: Property = ValueProperty(6.0)
    val tickMarkPadding: Property = ValueProperty(3.0)
    private val length = ValueProperty(null)
    private val orientation = ValueProperty(null)
    // theme
    private val myTickMarksEnabled = ValueProperty(true)
    private val myTickLabelsEnabled = ValueProperty(true)
    private val myAxisLineEnabled = ValueProperty(true)
    private val lineColor = ValueProperty(Color.BLACK)
    private val tickColor = ValueProperty(Color.BLACK)

    private fun defTickLabelHorizontalAnchor(orientation: Orientation): TextLabel.HorizontalAnchor {
        return when (orientation) {
            Orientation.LEFT -> RIGHT
            Orientation.RIGHT -> LEFT
            Orientation.TOP, Orientation.BOTTOM -> MIDDLE
        }
    }

    private fun defTickLabelVerticalAnchor(orientation: Orientation): TextLabel.VerticalAnchor {
        when (orientation) {
            Orientation.LEFT, Orientation.RIGHT -> return CENTER
            Orientation.TOP -> return BOTTOM
            Orientation.BOTTOM -> return TOP
            else -> throw RuntimeException("Unexpected orientation:$orientation")
        }
    }

    init {
        this.length.set(length)
        this.orientation.set(orientation)

        tickLabelHorizontalAnchor = ValueProperty(defTickLabelHorizontalAnchor(orientation))
        tickLabelVerticalAnchor = ValueProperty(defTickLabelVerticalAnchor(orientation))

        @Suppress("UNCHECKED_CAST")
        fun  EventSource>.asPropertyChangedEventSource() = this as EventSource>

        EventSources.composite(
            this.length.asPropertyChangedEventSource(),
            this.orientation.asPropertyChangedEventSource(),
            breaks.asPropertyChangedEventSource(),
            labels.asPropertyChangedEventSource(),
            gridLineLength.asPropertyChangedEventSource(),
            tickLabelOffsets.asPropertyChangedEventSource(),
            tickLabelHorizontalAnchor.asPropertyChangedEventSource(),
            tickLabelVerticalAnchor.asPropertyChangedEventSource(),
            tickLabelRotationDegree.asPropertyChangedEventSource(),
            tickLabelSmallFont.asPropertyChangedEventSource()
        ).addHandler(rebuildHandler())
    }

    override fun buildComponent() {
        buildAxis()
    }

    private fun buildAxis() {
        val rootElement = rootGroup
        rootElement.addClass(Style.AXIS)
        if (tickLabelSmallFont.get()) {
            rootElement.addClass(Style.SMALL_TICK_FONT)
        }

        val l = length.get()!!
        val x1: Double
        val y1: Double
        val x2: Double
        val y2: Double
        val start: Double
        val end: Double
        when (orientation.get()) {
            Orientation.LEFT, Orientation.RIGHT -> {
                x2 = 0.0
                x1 = x2
                start = 0.0
                y1 = start
                end = l
                y2 = end
            }
            Orientation.TOP, Orientation.BOTTOM -> {
                start = 0.0
                x1 = start
                end = l
                x2 = end
                y2 = 0.0
                y1 = y2
            }
            else -> throw RuntimeException("Unexpected orientation:" + orientation.get())
        }

        var axisLine: SvgLineElement? = null
        if (axisLineEnabled().get()) {
            axisLine = SvgLineElement(x1, y1, x2, y2)
            reg(bindOneWay(lineWidth, axisLine.strokeWidth()))
            reg(bindOneWay(lineColor, axisLine.strokeColor()))
        }

        // do not draw grid lines then it's too close to axis ends.
        val gridLineMinPos = start + 3
        val gridLineMaxPos = end - 3

        if (breaksEnabled()) {
            // add ticks before axis line
            val breaks = this.breaks.get()
            if (!(breaks == null || breaks.isEmpty())) {

                var labels: List? = this.labels.get()
                if (labels == null || labels.isEmpty()) {
                    labels = ArrayList()
                    for (i in breaks.indices) {
                        labels.add("")
                    }
                }

                var i = 0
                for (br in breaks) {
                    val addGridLine = br >= gridLineMinPos && br <= gridLineMaxPos
                    val label = labels[i % labels.size]
                    val labelOffset = tickLabelOffset(i)
                    i++
                    val group = buildTick(
                            label,
                            labelOffset,
                            if (addGridLine) gridLineLength.get() else 0.0)

                    when (orientation.get()) {
                        Orientation.LEFT, Orientation.RIGHT -> transformTranslate(group, 0.0, br)
                        Orientation.TOP, Orientation.BOTTOM -> transformTranslate(group, br, 0.0)
                        else -> throw RuntimeException("Unexpected orientation:" + orientation.get())
                    }

                    rootElement.children().add(group)
                }
            }
        }

        // axis line
        if (axisLine != null) {
            rootElement.children().add(axisLine)
        }
    }

    private fun buildTick(label: String, labelOffset: DoubleVector, gridLineLength: Double): SvgGElement {

        var tickMark: SvgLineElement? = null
        if (tickMarksEnabled().get()) {
            tickMark = SvgLineElement()
            reg(bindOneWay(tickMarkWidth, tickMark.strokeWidth()))
            reg(bindOneWay(tickColor, tickMark.strokeColor()))
        }

        var tickLabel: TextLabel? = null
        if (tickLabelsEnabled().get()) {
            tickLabel = TextLabel(label)
            reg(bindOneWay(tickColor, tickLabel.textColor()))
        }

        var gridLine: SvgLineElement? = null // optional;
        if (gridLineLength > 0) {
            gridLine = SvgLineElement()
            reg(bindOneWay(gridLineColor, gridLine.strokeColor()))
            reg(bindOneWay(gridLineWidth, gridLine.strokeWidth()))
        }

        val markLength = tickMarkLength.get()
        when (orientation.get()) {
            Orientation.LEFT -> {
                if (tickMark != null) {
                    tickMark.x2().set(-markLength)
                    tickMark.y2().set(0.0)
                }
                if (gridLine != null) {
                    gridLine.x2().set(gridLineLength)
                    gridLine.y2().set(0.0)
                }
            }
            Orientation.RIGHT -> {
                if (tickMark != null) {
                    tickMark.x2().set(markLength)
                    tickMark.y2().set(0.0)
                }
                if (gridLine != null) {
                    gridLine.x2().set(-gridLineLength)
                    gridLine.y2().set(0.0)
                }
            }
            Orientation.TOP -> {
                if (tickMark != null) {
                    tickMark.x2().set(0.0)
                    tickMark.y2().set(-markLength)
                }
                if (gridLine != null) {
                    gridLine.x2().set(0.0)
                    gridLine.y2().set(gridLineLength)
                }
            }
            Orientation.BOTTOM -> {
                if (tickMark != null) {
                    tickMark.x2().set(0.0)
                    tickMark.y2().set(markLength)
                }
                if (gridLine != null) {
                    gridLine.x2().set(0.0)
                    gridLine.y2().set(-gridLineLength)
                }
            }
            else -> throw RuntimeException("Unexpected orientation:" + orientation.get())
        }

        val g = SvgGElement()
        if (gridLine != null) {
            g.children().add(gridLine)
        }

        if (tickMark != null) {
            g.children().add(tickMark)
        }

        if (tickLabel != null) {
            tickLabel.moveTo(labelOffset.x, labelOffset.y)
            tickLabel.setHorizontalAnchor(tickLabelHorizontalAnchor.get())
            tickLabel.setVerticalAnchor(tickLabelVerticalAnchor.get())
            tickLabel.rotate(tickLabelRotationDegree.get())
            g.children().add(tickLabel.rootGroup)
        }

        g.addClass(Style.TICK)
        return g
    }

    private fun tickMarkLength(): Double {
        return if (myTickMarksEnabled.get()) {
            tickMarkLength.get()
        } else {
            0.0
        }
    }

    private fun tickLabelDistance(): Double {
        return tickMarkLength() + tickMarkPadding.get()
    }

    private fun tickLabelBaseOffset(): DoubleVector {
        val distance = tickLabelDistance()
        return when (orientation.get()) {
            Orientation.LEFT -> DoubleVector(-distance, 0.0)
            Orientation.RIGHT -> DoubleVector(distance, 0.0)
            Orientation.TOP -> DoubleVector(0.0, -distance)
            Orientation.BOTTOM -> DoubleVector(0.0, distance)
            else -> throw RuntimeException("Unexpected orientation:" + orientation.get())
        }
    }

    private fun tickLabelOffset(tickIndex: Int): DoubleVector {
        val additionalOffsets = tickLabelOffsets.get()
        val additionalOffset = if (additionalOffsets != null) additionalOffsets[tickIndex] else DoubleVector.ZERO
        return tickLabelBaseOffset().add(additionalOffset)
    }

    private fun breaksEnabled(): Boolean {
        return myTickMarksEnabled.get() || myTickLabelsEnabled.get()
    }

    fun tickMarksEnabled(): Property {
        return myTickMarksEnabled
    }

    fun tickLabelsEnabled(): Property {
        return myTickLabelsEnabled
    }

    fun axisLineEnabled(): Property {
        return myAxisLineEnabled
    }
}





© 2015 - 2025 Weber Informatics LLC | Privacy Policy