commonMain.jetbrains.datalore.plot.builder.assemble.PlotAssemblerUtil.kt Maven / Gradle / Ivy
/*
* 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.collect.ClosedRange
import jetbrains.datalore.base.gcommon.collect.Iterables
import jetbrains.datalore.base.values.Color
import jetbrains.datalore.base.values.Pair
import jetbrains.datalore.plot.base.Aes
import jetbrains.datalore.plot.base.Aesthetics
import jetbrains.datalore.plot.base.Scale
import jetbrains.datalore.plot.base.scale.ScaleUtil
import jetbrains.datalore.plot.builder.GeomLayer
import jetbrains.datalore.plot.builder.PlotUtil
import jetbrains.datalore.plot.builder.PlotUtil.computeLayerDryRunXYRanges
import jetbrains.datalore.plot.builder.VarBinding
import jetbrains.datalore.plot.builder.assemble.PlotGuidesAssemblerUtil.checkFitsColorBar
import jetbrains.datalore.plot.builder.assemble.PlotGuidesAssemblerUtil.createColorBarAssembler
import jetbrains.datalore.plot.builder.assemble.PlotGuidesAssemblerUtil.fitsColorBar
import jetbrains.datalore.plot.builder.assemble.PlotGuidesAssemblerUtil.guideDataRangeByAes
import jetbrains.datalore.plot.builder.assemble.PlotGuidesAssemblerUtil.mappedRenderedAesToCreateGuides
import jetbrains.datalore.plot.builder.layout.*
import jetbrains.datalore.plot.builder.theme.LegendTheme
import jetbrains.datalore.plot.common.data.SeriesUtil
internal object PlotAssemblerUtil {
private fun updateAesRangeMap(
aes: Aes<*>,
range: ClosedRange?,
rangeByAes: MutableMap, ClosedRange>
) {
@Suppress("NAME_SHADOWING")
var range = range
if (range != null) {
val wasRange = rangeByAes[aes]
if (wasRange != null) {
range = wasRange.span(range)
}
rangeByAes[aes] = range
}
}
private fun updateRange(range: ClosedRange?, wasRange: ClosedRange?): ClosedRange? {
@Suppress("NAME_SHADOWING")
var range = range
if (range != null) {
if (wasRange != null) {
range = wasRange.span(range)
}
return range
}
return wasRange
}
private fun updateRange(values: Iterable, wasRange: ClosedRange?): ClosedRange? {
if (!Iterables.isEmpty(values)) {
var newRange = ClosedRange.encloseAll(values)
if (wasRange != null) {
newRange = wasRange.span(newRange)
}
return newRange
}
return wasRange
}
fun createLegends(
layersByPanel: List>,
guideOptionsMap: Map, GuideOptions>,
theme: LegendTheme
): List {
// stitch together layers from all panels
var planeCount = 0
if (layersByPanel.isNotEmpty()) {
planeCount = layersByPanel[0].size
}
val stitchedLayersList = ArrayList()
for (i in 0 until planeCount) {
val layersOnPlane = ArrayList()
// collect layer[i] chunks from all panels
for (panelLayers in layersByPanel) {
layersOnPlane.add(panelLayers[i])
}
stitchedLayersList.add(
StitchedPlotLayers(
layersOnPlane
)
)
}
val dataRangeByAes = HashMap, ClosedRange>()
for (stitchedPlotLayers in stitchedLayersList) {
val layerDataRangeByAes = guideDataRangeByAes(stitchedPlotLayers, guideOptionsMap)
for (aes in layerDataRangeByAes.keys) {
val range = layerDataRangeByAes[aes]
updateAesRangeMap(
aes,
range,
dataRangeByAes
)
}
}
return createLegends(
stitchedLayersList,
dataRangeByAes,
guideOptionsMap,
theme
)
}
private fun createLegends(
stitchedLayersList: List,
dataRangeByAes: Map, ClosedRange>,
guideOptionsMap: Map, GuideOptions>,
theme: LegendTheme
): List {
val legendAssemblerByTitle = LinkedHashMap()
val colorBarAssemblerByTitle = LinkedHashMap()
for (stitchedLayers in stitchedLayersList) {
val layerConstantByAes = HashMap, Any>()
for (aes in stitchedLayers.renderedAes()) {
if (stitchedLayers.hasConstant(aes)) {
layerConstantByAes[aes] = stitchedLayers.getConstant(aes)!!
}
}
val layerBindingsByScaleName = LinkedHashMap>()
val aesList = mappedRenderedAesToCreateGuides(stitchedLayers, guideOptionsMap)
for (aes in aesList) {
var colorBar = false
val binding = stitchedLayers.getBinding(aes)
val scale = stitchedLayers.getScale(aes)
val scaleName = scale.name
if (guideOptionsMap.containsKey(aes)) {
val guideOptions = guideOptionsMap[aes]
if (guideOptions is ColorBarOptions) {
checkFitsColorBar(binding.aes, scale)
colorBar = true
@Suppress("UNCHECKED_CAST")
val colorScale = scale as Scale
colorBarAssemblerByTitle[scaleName] = createColorBarAssembler(
scaleName, binding.aes,
dataRangeByAes, colorScale, guideOptions, theme
)
}
} else if (fitsColorBar(binding.aes, scale)) {
colorBar = true
@Suppress("UNCHECKED_CAST")
val colorScale = scale as Scale
colorBarAssemblerByTitle[scaleName] = createColorBarAssembler(
scaleName, binding.aes,
dataRangeByAes, colorScale, null, theme
)
}
if (!colorBar) {
if (!layerBindingsByScaleName.containsKey(scaleName)) {
layerBindingsByScaleName[scaleName] = ArrayList()
}
layerBindingsByScaleName[scaleName]!!.add(binding)
}
}
for (scaleName in layerBindingsByScaleName.keys) {
if (!legendAssemblerByTitle.containsKey(scaleName)) {
legendAssemblerByTitle[scaleName] =
LegendAssembler(
scaleName,
guideOptionsMap,
theme
)
}
val varBindings = layerBindingsByScaleName[scaleName]!!
val legendKeyFactory = stitchedLayers.legendKeyElementFactory
val aestheticsDefaults = stitchedLayers.aestheticsDefaults
val legendAssembler = legendAssemblerByTitle[scaleName]!!
legendAssembler.addLayer(
legendKeyFactory,
varBindings,
layerConstantByAes,
aestheticsDefaults,
stitchedLayers.getScaleMap(),
dataRangeByAes
)
}
}
val legendBoxInfos = ArrayList()
for (legendTitle in colorBarAssemblerByTitle.keys) {
val boxInfo = colorBarAssemblerByTitle[legendTitle]!!.createColorBar()
if (!boxInfo.isEmpty) {
legendBoxInfos.add(boxInfo)
}
}
for (legendTitle in legendAssemblerByTitle.keys) {
val boxInfo = legendAssemblerByTitle[legendTitle]!!.createLegend()
if (!boxInfo.isEmpty) {
legendBoxInfos.add(boxInfo)
}
}
return legendBoxInfos
}
fun createPlotLayout(tileLayout: TileLayout, facets: PlotFacets): PlotLayout {
if (!facets.isDefined) {
return SingleTilePlotLayout(tileLayout)
}
return FacetGridPlotLayout(
facets,
tileLayout
)
}
fun computePlotDryRunXYRanges(layersByTile: List>): Pair, ClosedRange> {
// 'dry run' aesthetics use 'identity' mappers for positional aes (because the plot size is not yet determined)
val dryRunAestheticsByTileLayer = HashMap()
for (tileLayers in layersByTile) {
for (layer in tileLayers) {
val aesthetics = PlotUtil.createLayerDryRunAesthetics(layer)
dryRunAestheticsByTileLayer[layer] = aesthetics
}
}
fun initialRange(scale: Scale): ClosedRange? {
var initialRange: ClosedRange? = null
// Take in account:
// - scales domain if defined
// - scales breaks if defined
if (scale.isContinuousDomain) {
initialRange = updateRange(
ScaleUtil.transformedDefinedLimits(scale),
initialRange
)
}
if (scale.hasBreaks()) {
initialRange = updateRange(
ScaleUtil.breaksTransformed(scale),
initialRange
)
}
return initialRange
}
// // the "scale map" is shared by all layers.
val scaleMap = layersByTile[0][0].scaleMap
var xRangeOverall: ClosedRange? = initialRange(scaleMap[Aes.X])
var yRangeOverall: ClosedRange? = initialRange(scaleMap[Aes.Y])
fun completeOverallRange(
layer: GeomLayer,
aes: Aes,
initialRange: ClosedRange?,
aestheticsRange: ClosedRange?,
): ClosedRange? {
var range: ClosedRange? = updateRange(aestheticsRange, initialRange)
range = PlotUtil.rangeWithExpand(layer, aes, range)
// include zero if necessary
if (layer.rangeIncludesZero(aes)) {
range = updateRange(ClosedRange.singleton(0.0), range)
}
return range
}
for (layers in layersByTile) {
for (layer in layers) {
// use dry-run aesthetics to estimate ranges
val aesthetics = dryRunAestheticsByTileLayer.getValue(layer)
// adjust X/Y range with 'pos adjustment' and 'expands'
val xyRanges = computeLayerDryRunXYRanges(layer, aesthetics)
xRangeOverall = completeOverallRange(layer, Aes.X, xRangeOverall, xyRanges.first)
yRangeOverall = completeOverallRange(layer, Aes.Y, yRangeOverall, xyRanges.second)
}
}
// validate XY ranges
xRangeOverall = SeriesUtil.ensureApplicableRange(xRangeOverall)
yRangeOverall = SeriesUtil.ensureApplicableRange(yRangeOverall)
return Pair(
xRangeOverall,
yRangeOverall
)
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy