commonMain.jetbrains.datalore.plot.builder.assemble.LegendAssembler.kt Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of lets-plot-common Show documentation
Show all versions of lets-plot-common Show documentation
Lets-Plot JVM package without rendering part
/*
* 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.assemble
import jetbrains.datalore.base.gcommon.base.Preconditions.checkState
import jetbrains.datalore.base.gcommon.collect.ClosedRange
import jetbrains.datalore.base.geometry.DoubleVector
import jetbrains.datalore.plot.base.Aes
import jetbrains.datalore.plot.base.Aesthetics
import jetbrains.datalore.plot.base.aes.AestheticsDefaults
import jetbrains.datalore.plot.base.render.LegendKeyElementFactory
import jetbrains.datalore.plot.base.scale.ScaleUtil
import jetbrains.datalore.plot.base.scale.breaks.ScaleBreaksUtil
import jetbrains.datalore.plot.builder.VarBinding
import jetbrains.datalore.plot.builder.assemble.LegendAssemblerUtil.mapToAesthetics
import jetbrains.datalore.plot.builder.guide.*
import jetbrains.datalore.plot.builder.layout.LegendBoxInfo
import jetbrains.datalore.plot.builder.theme.LegendTheme
import kotlin.math.ceil
import kotlin.math.floor
import kotlin.math.min
class LegendAssembler(
private val legendTitle: String,
private val guideOptionsMap: Map, GuideOptions>,
private val theme: LegendTheme
) {
private val myLegendLayers = ArrayList()
fun addLayer(
keyFactory: LegendKeyElementFactory,
varBindings: List,
constantByAes: Map, Any>,
aestheticsDefaults: AestheticsDefaults,
scaleByAes: TypedScaleMap,
dataRangeByAes: Map, ClosedRange>
) {
myLegendLayers.add(
LegendLayer(
keyFactory,
varBindings,
constantByAes,
aestheticsDefaults,
scaleByAes,
dataRangeByAes
)
)
}
fun createLegend(): LegendBoxInfo {
val legendBreaksByLabel = LinkedHashMap()
for (legendLayer in myLegendLayers) {
val keyElementFactory = legendLayer.keyElementFactory
val dataPoints = legendLayer.keyAesthetics!!.dataPoints().iterator()
for (label in legendLayer.keyLabels!!) {
if (!legendBreaksByLabel.containsKey(label)) {
legendBreaksByLabel[label] =
LegendBreak(label)
}
legendBreaksByLabel[label]!!.addLayer(dataPoints.next(), keyElementFactory)
}
}
val legendBreaks = ArrayList()
for (legendBreak in legendBreaksByLabel.values) {
if (legendBreak.isEmpty) {
continue
}
legendBreaks.add(legendBreak)
}
if (legendBreaks.isEmpty()) {
return LegendBoxInfo.EMPTY
}
// legend options
val legendOptionsList = ArrayList()
for (legendLayer in myLegendLayers) {
val aesList = legendLayer.aesList
for (aes in aesList) {
if (guideOptionsMap[aes] is LegendOptions) {
legendOptionsList.add(guideOptionsMap[aes] as LegendOptions)
}
}
}
val spec =
createLegendSpec(
legendTitle, legendBreaks, theme,
LegendOptions.combine(
legendOptionsList
)
)
return object : LegendBoxInfo(spec.size) {
override fun createLegendBox(): LegendBox {
val c = LegendComponent(spec)
c.debug =
DEBUG_DRAWING
return c
}
}
}
private class LegendLayer(
internal val keyElementFactory: LegendKeyElementFactory,
private val varBindings: List,
private val constantByAes: Map, Any>,
private val aestheticsDefaults: AestheticsDefaults,
private val scaleMap: TypedScaleMap,
dataRangeByAes: Map, ClosedRange>
) {
internal var keyAesthetics: Aesthetics? = null
internal var keyLabels: List? = null
internal val aesList: List>
get() {
val result = ArrayList>()
for (binding in varBindings) {
result.add(binding.aes)
}
return result
}
init {
init(dataRangeByAes)
}
private fun init(dataRangeByAes: Map, ClosedRange>) {
val aesValuesByLabel = LinkedHashMap, Any>>()
for (varBinding in varBindings) {
val aes = varBinding.aes
var scale = scaleMap[aes]
if (!scale.hasBreaks()) {
if (dataRangeByAes.containsKey(aes)) {
scale = ScaleBreaksUtil.withBreaks(scale, dataRangeByAes[aes]!!, 5)
} else {
// skip this scale
// (we should never get here)
continue
}
}
checkState(scale.hasBreaks(), "No breaks were defined for scale $aes")
val values = ScaleUtil.breaksAesthetics(scale).iterator()
val labels = ScaleUtil.labels(scale)
for (label in labels) {
if (!aesValuesByLabel.containsKey(label)) {
aesValuesByLabel[label] = HashMap()
}
val value = values.next()
@Suppress("ReplacePutWithAssignment")
aesValuesByLabel[label]!!.put(aes, value!!)
}
}
// build 'key' aesthetics
keyAesthetics = mapToAesthetics(aesValuesByLabel.values, constantByAes, aestheticsDefaults)
keyLabels = ArrayList(aesValuesByLabel.keys)
}
}
companion object {
private const val DEBUG_DRAWING = jetbrains.datalore.plot.FeatureSwitch.LEGEND_DEBUG_DRAWING
fun createLegendSpec(
title: String,
breaks: List,
theme: LegendTheme,
options: LegendOptions = LegendOptions()
): LegendComponentSpec {
val legendDirection =
LegendAssemblerUtil.legendDirection(theme)
// key size
fun pretty(v: DoubleVector): DoubleVector {
val margin = 1.0
return DoubleVector(
floor(v.x / 2) * 2 + 1.0 + margin,
floor(v.y / 2) * 2 + 1.0 + margin
)
}
var keySize = DoubleVector(theme.keySize(), theme.keySize())
for (br in breaks) {
val minimumKeySize = br.minimumKeySize
keySize = keySize.max(pretty(minimumKeySize))
}
// row, col count
val breakCount = breaks.size
val colCount: Int
val rowCount: Int
if (options.isByRow) {
colCount = when {
options.hasColCount() -> min(options.colCount, breakCount)
options.hasRowCount() -> ceil(breakCount / options.rowCount.toDouble()).toInt()
legendDirection === LegendDirection.HORIZONTAL -> breakCount
else -> 1
}
rowCount = ceil(breakCount / colCount.toDouble()).toInt()
} else {
// by column
rowCount = when {
options.hasRowCount() -> min(options.rowCount, breakCount)
options.hasColCount() -> ceil(breakCount / options.colCount.toDouble()).toInt()
legendDirection !== LegendDirection.HORIZONTAL -> breakCount
else -> 1
}
colCount = ceil(breakCount / rowCount.toDouble()).toInt()
}
val layout: LegendComponentLayout
@Suppress("LiftReturnOrAssignment")
if (legendDirection === LegendDirection.HORIZONTAL) {
if (options.hasRowCount() || options.hasColCount() && options.colCount < breakCount) {
layout = LegendComponentLayout.horizontalMultiRow(
title,
breaks,
keySize
)
} else {
layout =
LegendComponentLayout.horizontal(title, breaks, keySize)
}
} else {
layout = LegendComponentLayout.vertical(title, breaks, keySize)
}
layout.colCount = colCount
layout.rowCount = rowCount
layout.isFillByRow = options.isByRow
return LegendComponentSpec(
title,
breaks,
theme,
layout
)
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy