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.gcommon.collect.ClosedRange
import jetbrains.datalore.base.geometry.DoubleVector
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.Defaults
import jetbrains.datalore.plot.builder.presentation.PlotLabelSpec
import jetbrains.datalore.plot.builder.presentation.Style
import jetbrains.datalore.plot.builder.theme.AxisTheme
import jetbrains.datalore.plot.builder.theme.PanelGridTheme
import jetbrains.datalore.vis.svg.SvgGElement
import jetbrains.datalore.vis.svg.SvgLineElement
import jetbrains.datalore.vis.svg.SvgUtils.transformTranslate

class AxisComponent(
    private val length: Double,
    private val orientation: Orientation,
    private val breaksData: BreaksData,
    private val labelAdjustments: TickLabelAdjustments = TickLabelAdjustments(orientation),
    private val gridLineLength: Double,
    private val axisTheme: AxisTheme,
    private val gridTheme: PanelGridTheme,
    private val useSmallFont: Boolean = false,
    private val hideAxisBreaks: Boolean = false
) : SvgComponent() {

    private val tickMarkPadding = Defaults.Plot.Axis.TICK_MARK_PADDING

    override fun buildComponent() {
        buildAxis()
    }

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

        val l = length
        val x1: Double
        val y1: Double
        val x2: Double
        val y2: Double
        val start: Double
        val end: Double
        when (orientation) {
            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
            }
        }

        var axisLine: SvgLineElement? = null
        if (!hideAxisBreaks && axisTheme.showLine()) {
            axisLine = SvgLineElement(x1, y1, x2, y2)
            axisLine.strokeWidth().set(axisTheme.lineWidth())
            axisLine.strokeColor().set(axisTheme.lineColor())
        }

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

        // Minor grid.
        if (gridTheme.showMinor()) {
            for (br in breaksData.minorBreaks) {
                if (br >= gridLineMinPos && br <= gridLineMaxPos) {
                    val elem = buildGridLine(br, gridTheme.minorLineWidth(), gridTheme.minorLineColor())
                    rootElement.children().add(elem)
                }
            }
        }

        // Major grid.
        if (gridTheme.showMajor()) {
            for (br in breaksData.majorBreaks) {
                if (br >= gridLineMinPos && br <= gridLineMaxPos) {
                    val elem = buildGridLine(br, gridTheme.majorLineWidth(), gridTheme.majorLineColor())
                    rootElement.children().add(elem)
                }
            }
        }

        if (!hideAxisBreaks && (axisTheme.showLabels() || axisTheme.showTickMarks())) {
            val labelsCleaner = TickLabelsCleaner(orientation.isHorizontal)

            for ((i, br) in breaksData.majorBreaks.withIndex()) {
                val label = breaksData.majorLabels[i % breaksData.majorLabels.size]
                val labelOffset = tickLabelBaseOffset().add(labelAdjustments.additionalOffset(i))
                val group = buildTick(
                    label,
                    labelOffset,
                    skipLabel = !labelsCleaner.beforeAddLabel(br, labelAdjustments.rotationDegree),
                    axisTheme
                )

                when (orientation) {
                    Orientation.LEFT, Orientation.RIGHT -> transformTranslate(group, 0.0, br)
                    Orientation.TOP, Orientation.BOTTOM -> transformTranslate(group, br, 0.0)
                }

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

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

    private fun buildGridLine(br: Double, width: Double, color: Color): SvgLineElement {
        val elem = when (orientation) {
            Orientation.LEFT -> SvgLineElement(0.0, 0.0, gridLineLength, 0.0)
            Orientation.RIGHT -> SvgLineElement(0.0, 0.0, -gridLineLength, 0.0)
            Orientation.TOP -> SvgLineElement(0.0, 0.0, 0.0, gridLineLength)
            Orientation.BOTTOM -> SvgLineElement(0.0, 0.0, 0.0, -gridLineLength)
        }
        elem.strokeColor().set(color)
        elem.strokeWidth().set(width)

        when (orientation) {
            Orientation.LEFT, Orientation.RIGHT -> {
                elem.y1().set(br)
                elem.y2().set(br)
            }
            Orientation.TOP, Orientation.BOTTOM -> {
                elem.x1().set(br)
                elem.x2().set(br)
            }
        }
        return elem
    }

    private fun buildTick(
        label: String,
        labelOffset: DoubleVector,
        skipLabel: Boolean,
        axisTheme: AxisTheme
    ): SvgGElement {

        var tickMark: SvgLineElement? = null
        if (axisTheme.showTickMarks()) {
            tickMark = SvgLineElement()
            tickMark.strokeWidth().set(axisTheme.tickMarkWidth())
            tickMark.strokeColor().set(axisTheme.tickMarkColor())
        }

        var tickLabel: TextLabel? = null
        if (!skipLabel && axisTheme.showLabels()) {
            tickLabel = TextLabel(label)
            tickLabel.textColor().set(axisTheme.labelColor())
        }

        val markLength = axisTheme.tickMarkLength()
        when (orientation) {
            Orientation.LEFT -> {
                if (tickMark != null) {
                    tickMark.x2().set(-markLength)
                    tickMark.y2().set(0.0)
                }
            }
            Orientation.RIGHT -> {
                if (tickMark != null) {
                    tickMark.x2().set(markLength)
                    tickMark.y2().set(0.0)
                }
            }
            Orientation.TOP -> {
                if (tickMark != null) {
                    tickMark.x2().set(0.0)
                    tickMark.y2().set(-markLength)
                }
            }
            Orientation.BOTTOM -> {
                if (tickMark != null) {
                    tickMark.x2().set(0.0)
                    tickMark.y2().set(markLength)
                }
            }
        }

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

        if (tickLabel != null) {
            tickLabel.moveTo(labelOffset.x, labelOffset.y)
            tickLabel.setHorizontalAnchor(labelAdjustments.horizontalAnchor)
            tickLabel.setVerticalAnchor(labelAdjustments.verticalAnchor)
            tickLabel.rotate(labelAdjustments.rotationDegree)
            g.children().add(tickLabel.rootGroup)
        }

        g.addClass(Style.TICK)
        return g
    }


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

    private fun tickLabelBaseOffset(): DoubleVector {
        val distance = axisTheme.tickLabelDistance()
        return when (orientation) {
            Orientation.LEFT -> DoubleVector(-distance, 0.0)
            Orientation.RIGHT -> DoubleVector(distance, 0.0)
            Orientation.TOP -> DoubleVector(0.0, -distance)
            Orientation.BOTTOM -> DoubleVector(0.0, distance)
        }
    }


    companion object {
    }

    class BreaksData constructor(
        val majorBreaks: List,
        val majorLabels: List,
        minorBreaks: List? = null,
    ) {
        val minorBreaks: List = minorBreaks ?: let {
            if (majorBreaks.size <= 1) {
                emptyList()
            } else {
                // Default minor grid: a minor line in the middle between each pair of major lines.
                val minorBreaks: MutableList = majorBreaks.subList(0, majorBreaks.size - 1)
                    .zip(majorBreaks.subList(1, majorBreaks.size))
                    .fold(ArrayList()) { l, pair ->
                        l.add((pair.second - pair.first) / 2 + pair.first)
                        l
                    }

                // Add one in the front
                majorBreaks.take(2).reduce { first, second -> second - first }.run {
                    minorBreaks.add(0, minorBreaks.first() - this)
                }

                // Add one in the back.
                majorBreaks.takeLast(2).reduce { first, second -> second - first }.run {
                    minorBreaks.add(0, minorBreaks.last() + this)
                }

                minorBreaks
            }
        }
    }

    class TickLabelAdjustments(
        orientation: Orientation,
        horizontalAnchor: TextLabel.HorizontalAnchor? = null,
        verticalAnchor: TextLabel.VerticalAnchor? = null,
        val rotationDegree: Double = 0.0,
        private val additionalOffsets: List? = null
    ) {
        val horizontalAnchor: TextLabel.HorizontalAnchor = horizontalAnchor ?: when (orientation) {
            Orientation.LEFT -> RIGHT
            Orientation.RIGHT -> LEFT
            Orientation.TOP, Orientation.BOTTOM -> MIDDLE
        }
        val verticalAnchor: TextLabel.VerticalAnchor = verticalAnchor ?: when (orientation) {
            Orientation.LEFT, Orientation.RIGHT -> CENTER
            Orientation.TOP -> BOTTOM
            Orientation.BOTTOM -> TOP
        }

        fun additionalOffset(tickIndex: Int): DoubleVector {
            return additionalOffsets?.get(tickIndex) ?: DoubleVector.ZERO
        }
    }

    private class TickLabelsCleaner(val horizontalAxis: Boolean) {
        private val filledRanges = ArrayList>()

        fun beforeAddLabel(loc: Double, rotationDegree: Double): Boolean {
            if (!isRelevant(rotationDegree)) return true

            val len = PlotLabelSpec.AXIS_TICK.height()

            // find overlap
            if (filledRanges.any { it.contains(loc) || it.contains(loc + len) }) {
                // overlap - don't add this label
                return false
            }

            filledRanges.add(ClosedRange(loc, loc + len))
            return true
        }

        private fun isRelevant(rotationDegree: Double): Boolean {
            return when {
                horizontalAxis -> isVertical(rotationDegree)
                else -> isHorizontal(rotationDegree)
            }
        }

        private fun isHorizontal(rotationDegree: Double): Boolean {
            return rotationDegree % 180 == 0.0
        }

        private fun isVertical(rotationDegree: Double): Boolean {
            return (rotationDegree / 90) % 2 == 1.0
        }
    }
}





© 2015 - 2025 Weber Informatics LLC | Privacy Policy