Please wait. This can take some minutes ...
Many resources are needed to download a project. Please understand that we have to compensate our server costs. Thank you in advance.
Project price only 1 $
You can buy this project and download/modify it how often you want.
commonMain.jetbrains.datalore.plot.builder.PlotSvgComponent.kt Maven / Gradle / Ivy
/*
* 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.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.PlotContext
import jetbrains.datalore.plot.base.render.svg.MultilineLabel
import jetbrains.datalore.plot.base.render.svg.SvgComponent
import jetbrains.datalore.plot.base.render.svg.Text.HorizontalAnchor
import jetbrains.datalore.plot.base.render.svg.Text.VerticalAnchor
import jetbrains.datalore.plot.base.render.svg.TextLabel
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.PlotInteractor
import jetbrains.datalore.plot.builder.layout.*
import jetbrains.datalore.plot.builder.layout.TextJustification.Companion.TextRotation
import jetbrains.datalore.plot.builder.layout.TextJustification.Companion.applyJustification
import jetbrains.datalore.plot.builder.layout.figure.plot.PlotFigureLayoutInfo
import jetbrains.datalore.plot.builder.presentation.Defaults
import jetbrains.datalore.plot.builder.presentation.LabelSpec
import jetbrains.datalore.plot.builder.presentation.Style
import jetbrains.datalore.plot.builder.theme.Theme
import jetbrains.datalore.plot.builder.tooltip.HorizontalAxisTooltipPosition
import jetbrains.datalore.plot.builder.tooltip.VerticalAxisTooltipPosition
import jetbrains.datalore.vis.StyleSheet
import jetbrains.datalore.vis.svg.SvgElement
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
class PlotSvgComponent constructor(
private val title: String?,
private val subtitle: String?,
private val caption: String?,
private val coreLayersByTile: List>,
private val marginalLayersByTile: List>,
private val figureLayoutInfo: PlotFigureLayoutInfo,
private val frameProviderByTile: List,
private val coordProvider: CoordProvider,
val interactionsEnabled: Boolean,
val theme: Theme,
val styleSheet: StyleSheet,
val plotContext: PlotContext
) : SvgComponent() {
val figureSize: DoubleVector = figureLayoutInfo.figureSize
val flippedAxis = frameProviderByTile[0].flipAxis
val mouseEventPeer = MouseEventPeer()
var interactor: PlotInteractor? = null
set(value) {
check(field == null) { "interactor can be initialize only once." }
check(!isBuilt) { "Can't change interactor after plot has already been built." }
field = value
}
internal var liveMapFigures: List = emptyList()
private set
val containsLiveMap: Boolean = coreLayersByTile.flatten().any(GeomLayer::isLiveMap)
private val hAxisTitle: String? = frameProviderByTile[0].hAxisLabel
private val vAxisTitle: String? = frameProviderByTile[0].vAxisLabel
private var isDisposed = false
override fun clear() {
// Effectivly disposes the plot component
// because "interactor" is likely got disposed too,
// and "interactor" can't be reset.
isDisposed = true
super.clear()
}
override fun buildComponent() {
check(!isDisposed) { "Plot can't be rebuild after it was disposed." }
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 = figureSize.y / 2 - 8
for (s in messages) {
val errorLabel = TextLabel(s)
val textColor = when {
theme.plot().showBackground() -> theme.plot().textColor()
else -> Defaults.TEXT_COLOR
}
errorLabel.textColor().set(textColor)
errorLabel.setFontWeight("normal")
errorLabel.setFontStyle("normal")
errorLabel.setHorizontalAnchor(HorizontalAnchor.MIDDLE)
errorLabel.setVerticalAnchor(VerticalAnchor.CENTER)
errorLabel.moveTo(figureSize.x / 2, y)
rootGroup.children().add(errorLabel.rootGroup)
y += 16.0
}
}
}
private fun buildPlot() {
buildPlotComponents()
reg(object : Registration() {
override fun doRemove() {
interactor?.dispose()
liveMapFigures = emptyList()
}
})
}
private fun buildPlotComponents() {
val overallRect = DoubleRectangle(DoubleVector.ZERO, figureSize)
val plotTheme = theme.plot()
if (plotTheme.showBackground()) {
add(SvgRectElement(overallRect).apply {
strokeColor().set(plotTheme.backgroundColor())
strokeWidth().set(plotTheme.backgroundStrokeWidth())
fillColor().set(plotTheme.backgroundFill())
if (containsLiveMap) {
// Don't fill rect over livemap figure.
fillOpacity().set(0.0)
} else {
// Previously there was a fix for JFX here:
// if the background color has no transparency - set its opacity to 0.99.
// Now jfx-mapper will fix it in SvgShapeMapping.
}
})
}
if (DEBUG_DRAWING) {
drawDebugRect(overallRect, Color.MAGENTA, "MAGENTA: overallRect")
}
// -------------
val axisEnabled = !containsLiveMap
val layoutInfo = figureLayoutInfo.plotLayoutInfo
val plotOuterBounds = figureLayoutInfo.figureLayoutedBounds
if (DEBUG_DRAWING) {
drawDebugRect(plotOuterBounds, Color.BLUE, "BLUE: plotOuterBounds")
}
val plotOuterBoundsWithoutTitleAndCaption = figureLayoutInfo.figureBoundsWithoutTitleAndCaption
if (DEBUG_DRAWING) {
drawDebugRect(
plotOuterBoundsWithoutTitleAndCaption,
Color.BLUE,
"BLUE: plotOuterBoundsWithoutTitleAndCaption"
)
}
val plotAreaOrigin = figureLayoutInfo.plotAreaOrigin
// build tiles
@Suppress("UnnecessaryVariable")
val tilesOrigin = plotAreaOrigin
for (tileLayoutInfo in layoutInfo.tiles) {
val tileIndex = tileLayoutInfo.trueIndex
// Create a plot tile.
val tileFrameProvider = frameProviderByTile[tileIndex]
val tileFrame = tileFrameProvider.createTileFrame(
tileLayoutInfo,
coordProvider,
DEBUG_DRAWING
)
val marginalFrameByMargin: Map = tileFrameProvider
.createMarginalFrames(
tileLayoutInfo,
coordProvider,
DEBUG_DRAWING
)
val tile = PlotTile(
coreLayers = coreLayersByTile[tileIndex],
marginalLayers = marginalLayersByTile[tileIndex],
tilesOrigin, tileLayoutInfo, theme,
tileFrame,
marginalFrameByMargin
)
val plotOriginAbsolute = tilesOrigin.add(tileLayoutInfo.offset)
tile.moveTo(plotOriginAbsolute)
add(tile)
tile.liveMapFigure?.run {
liveMapFigures = liveMapFigures + listOf(this)
}
val geomOuterBoundsAbsolute = tileLayoutInfo.geomOuterBounds.add(plotOriginAbsolute)
val geomInnerBoundsAbsolute = tileLayoutInfo.geomInnerBounds.add(plotOriginAbsolute)
// axis tooltip should appear on 'outer' bounds:
val axisOrigin = DoubleVector(
x = if (layoutInfo.hasLeftAxis) geomOuterBoundsAbsolute.left else geomOuterBoundsAbsolute.right,
y = if (layoutInfo.hasBottomAxis) geomOuterBoundsAbsolute.bottom else geomOuterBoundsAbsolute.top
)
interactor?.onTileAdded(
geomInnerBoundsAbsolute,
tile.targetLocators,
tile.layerYOrientations,
axisOrigin,
hAxisTooltipPosition = if (layoutInfo.hasBottomAxis) HorizontalAxisTooltipPosition.BOTTOM else HorizontalAxisTooltipPosition.TOP,
vAxisTooltipPosition = if (layoutInfo.hasLeftAxis) VerticalAxisTooltipPosition.LEFT else VerticalAxisTooltipPosition.RIGHT
)
if (DEBUG_DRAWING) {
drawDebugRect(geomInnerBoundsAbsolute, Color.ORANGE, "ORANGE: geomInnerBoundsAbsolute")
}
}
val geomAreaBounds = figureLayoutInfo.geomAreaBounds
if (DEBUG_DRAWING) {
drawDebugRect(geomAreaBounds, Color.RED, "RED: geomAreaBounds")
}
// plot title, subtitle, caption rectangles:
// xxxElementRect - rectangle for element, including margins
// xxxTextRect - for text only
fun textRectangle(elementRect: DoubleRectangle, margins: Margins) = createTextRectangle(
elementRect,
topMargin = margins.top,
bottomMargin = margins.bottom
)
val plotTitleElementRect = title?.let {
DoubleRectangle(
geomAreaBounds.left,
plotOuterBounds.top,
geomAreaBounds.width,
PlotLayoutUtil.titleThickness(
title,
PlotLabelSpecFactory.plotTitle(plotTheme),
theme.plot().titleMargins()
)
)
}
val plotTitleTextRect = plotTitleElementRect?.let { textRectangle(it, theme.plot().titleMargins()) }
if (DEBUG_DRAWING) {
plotTitleTextRect?.let { drawDebugRect(it, Color.LIGHT_BLUE) }
plotTitleElementRect?.let { drawDebugRect(it, Color.GRAY) }
plotTitleTextRect?.let {
drawDebugRect(
textBoundingBox(title!!, it, PlotLabelSpecFactory.plotTitle(plotTheme), align = -1),
Color.DARK_GREEN
)
}
}
val subtitleElementRect = subtitle?.let {
DoubleRectangle(
geomAreaBounds.left,
plotTitleElementRect?.bottom ?: plotOuterBounds.top,
geomAreaBounds.width,
PlotLayoutUtil.titleThickness(
subtitle,
PlotLabelSpecFactory.plotSubtitle(plotTheme),
theme.plot().subtitleMargins()
)
)
}
val subtitleTextRect = subtitleElementRect?.let { textRectangle(it, theme.plot().subtitleMargins()) }
if (DEBUG_DRAWING) {
subtitleTextRect?.let { drawDebugRect(it, Color.LIGHT_BLUE) }
subtitleElementRect?.let { drawDebugRect(it, Color.GRAY) }
subtitleTextRect?.let {
drawDebugRect(
textBoundingBox(subtitle!!, it, PlotLabelSpecFactory.plotTitle(plotTheme), align = -1),
Color.DARK_GREEN
)
}
}
val captionElementRect = caption?.let {
val captionRectHeight = PlotLayoutUtil.titleThickness(
caption,
PlotLabelSpecFactory.plotCaption(plotTheme),
theme.plot().captionMargins()
)
DoubleRectangle(
geomAreaBounds.left,
plotOuterBounds.bottom - captionRectHeight,
geomAreaBounds.width,
captionRectHeight
)
}
val captionTextRect = captionElementRect?.let { textRectangle(it, theme.plot().captionMargins()) }
if (DEBUG_DRAWING) {
captionTextRect?.let { drawDebugRect(it, Color.LIGHT_BLUE) }
captionElementRect?.let { drawDebugRect(it, Color.GRAY) }
captionTextRect?.let {
drawDebugRect(
textBoundingBox(caption!!, it, PlotLabelSpecFactory.plotTitle(plotTheme), align = 1),
Color.DARK_GREEN
)
}
}
// add plot title
plotTitleTextRect?.let {
addTitle(
title,
labelSpec = PlotLabelSpecFactory.plotTitle(plotTheme),
justification = plotTheme.titleJustification(),
boundRect = it,
className = Style.PLOT_TITLE
)
}
// add plot subtitle
subtitleTextRect?.let {
addTitle(
subtitle,
labelSpec = PlotLabelSpecFactory.plotSubtitle(plotTheme),
justification = plotTheme.subtitleJustification(),
boundRect = it,
className = Style.PLOT_SUBTITLE
)
}
val overallTileBounds = PlotLayoutUtil.overallTileBounds(layoutInfo)
.add(plotAreaOrigin)
if (DEBUG_DRAWING) {
drawDebugRect(overallTileBounds, Color.DARK_MAGENTA, "DARK_MAGENTA: overallTileBounds")
}
// add axis titles
if (axisEnabled) {
if (vAxisTitle != null) {
val titleOrientation = layoutInfo.tiles.first().axisInfos.vAxisTitleOrientation
addAxisTitle(
vAxisTitle,
titleOrientation,
overallTileBounds,
geomAreaBounds,
labelSpec = PlotLabelSpecFactory.axisTitle(theme.verticalAxis(flippedAxis)),
justification = theme.verticalAxis(flippedAxis).titleJustification(),
margins = theme.verticalAxis(flippedAxis).titleMargins(),
className = "${Style.AXIS_TITLE}-${theme.verticalAxis(flippedAxis).axis}"
)
}
if (hAxisTitle != null) {
val titleOrientation = layoutInfo.tiles.first().axisInfos.hAxisTitleOrientation
addAxisTitle(
hAxisTitle,
titleOrientation,
overallTileBounds,
geomAreaBounds,
labelSpec = PlotLabelSpecFactory.axisTitle(theme.horizontalAxis(flippedAxis)),
justification = theme.horizontalAxis(flippedAxis).titleJustification(),
margins = theme.horizontalAxis(flippedAxis).titleMargins(),
className = "${Style.AXIS_TITLE}-${theme.horizontalAxis(flippedAxis).axis}"
)
}
}
// add legends
val legendTheme = theme.legend()
val legendsBlockInfo = figureLayoutInfo.legendsBlockInfo
if (!legendTheme.position().isHidden) {
val legendsBlockInfoLayouted = LegendBoxesLayout(
outerBounds = plotOuterBoundsWithoutTitleAndCaption,
innerBounds = geomAreaBounds,
legendTheme
).doLayout(legendsBlockInfo)
for (boxWithLocation in legendsBlockInfoLayouted.boxWithLocationList) {
val legendBox = boxWithLocation.legendBox.createLegendBox()
legendBox.moveTo(boxWithLocation.location)
add(legendBox)
}
}
// add caption
captionTextRect?.let {
addTitle(
title = caption,
labelSpec = PlotLabelSpecFactory.plotCaption(plotTheme),
justification = plotTheme.captionJustification(),
boundRect = it,
className = Style.PLOT_CAPTION
)
}
}
private fun createTextRectangle(
elementRect: DoubleRectangle,
topMargin: Double = 0.0,
rightMargin: Double = 0.0,
bottomMargin: Double = 0.0,
leftMargin: Double = 0.0
) = DoubleRectangle(
elementRect.left + leftMargin,
elementRect.top + topMargin,
elementRect.width - (rightMargin + leftMargin),
elementRect.height - (topMargin + bottomMargin)
)
private fun addAxisTitle(
text: String,
orientation: Orientation,
overallTileBounds: DoubleRectangle, // tiles union bounds
overallGeomBounds: DoubleRectangle, // geom bounds union
labelSpec: LabelSpec,
justification: TextJustification,
margins: Margins,
className: String
) {
val referenceRect = when (orientation) {
Orientation.LEFT,
Orientation.RIGHT ->
DoubleRectangle(
overallTileBounds.left, overallGeomBounds.top,
overallTileBounds.width, overallGeomBounds.height
)
Orientation.TOP,
Orientation.BOTTOM ->
DoubleRectangle(
overallGeomBounds.left, overallTileBounds.top,
overallGeomBounds.width, overallTileBounds.height
)
}
val rotation = when (orientation) {
Orientation.LEFT -> TextRotation.ANTICLOCKWISE
Orientation.RIGHT -> TextRotation.ANTICLOCKWISE
else -> null
}
val textHeight = PlotLayoutUtil.textDimensions(text, labelSpec).y
// rectangle for element, including margins
val axisTitleElementRect = when (orientation) {
Orientation.LEFT ->
DoubleRectangle(
referenceRect.left - textHeight - margins.width(),
referenceRect.top,
textHeight + margins.width(),
referenceRect.height
)
Orientation.RIGHT ->
DoubleRectangle(
referenceRect.right,
referenceRect.top,
textHeight + margins.width(),
referenceRect.height
)
Orientation.TOP -> DoubleRectangle(
referenceRect.left,
referenceRect.top - textHeight - margins.height(),
referenceRect.width,
textHeight + margins.height()
)
Orientation.BOTTOM -> DoubleRectangle(
referenceRect.left,
referenceRect.bottom,
referenceRect.width,
textHeight + margins.height()
)
}
// rectangle for text (without margins)
val axisTitleTextRect = when {
orientation.isHorizontal -> {
createTextRectangle(
axisTitleElementRect,
topMargin = margins.top,
bottomMargin = margins.bottom
)
}
else -> {
createTextRectangle(
axisTitleElementRect,
rightMargin = margins.right,
leftMargin = margins.left
)
}
}
addTitle(
text,
labelSpec,
justification,
axisTitleTextRect,
rotation,
className
)
if (DEBUG_DRAWING) {
drawDebugRect(axisTitleTextRect, Color.LIGHT_BLUE)
drawDebugRect(axisTitleElementRect, Color.GRAY)
drawDebugRect(textBoundingBox(text, axisTitleTextRect, labelSpec, orientation), Color.DARK_GREEN)
}
}
private fun textBoundingBox(
text: String,
boundRect: DoubleRectangle,
labelSpec: LabelSpec,
orientation: Orientation = Orientation.TOP,
align: Int = 0 // < 0 - to left; > 0 - to right; 0 - centered
): DoubleRectangle {
val d = PlotLayoutUtil.textDimensions(text, labelSpec)
return if (orientation in listOf(Orientation.TOP, Orientation.BOTTOM)) {
val x = when {
align > 0 -> boundRect.right - d.x
align < 0 -> boundRect.left
else -> boundRect.center.x - d.x / 2
}
DoubleRectangle(x, boundRect.center.y - d.y / 2, d.x, d.y)
} else {
val y = when {
align > 0 -> boundRect.bottom - d.x
align < 0 -> boundRect.top
else -> boundRect.center.y - d.x / 2
}
DoubleRectangle(boundRect.center.x - d.y / 2, y, d.y, d.x)
}
}
private fun addTitle(
title: String?,
labelSpec: LabelSpec,
justification: TextJustification,
boundRect: DoubleRectangle,
rotation: TextRotation? = null,
className: String
) {
if (title == null) return
val lineHeight = labelSpec.height()
val titleLabel = MultilineLabel(title)
titleLabel.addClassName(className)
val (position, hAnchor) = applyJustification(
boundRect,
textSize = PlotLayoutUtil.textDimensions(title, labelSpec),
lineHeight,
justification,
rotation
)
titleLabel.setLineHeight(lineHeight)
titleLabel.setHorizontalAnchor(hAnchor)
titleLabel.moveTo(position)
rotation?.angle?.let(titleLabel::rotate)
add(titleLabel)
}
private fun drawDebugRect(r: DoubleRectangle, color: Color, message: String? = null) {
val rect = SvgRectElement(r)
rect.strokeColor().set(color)
rect.strokeWidth().set(1.0)
rect.fillOpacity().set(0.0)
message?.run {
onMouseMove(rect, "$message: $r")
}
add(rect)
}
/**
* Only used when DEBUG_DRAWING is ON.
*
* Doesn't seem to work any longer
*/
private fun onMouseMove(e: SvgElement, message: String) {
e.addEventHandler(SvgEventSpec.MOUSE_MOVE, object :
SvgEventHandler {
override fun handle(node: SvgNode, e: Event) {
println(message)
}
})
}
companion object {
private val LOG = PortableLogging.logger(PlotSvgComponent::class)
private const val DEBUG_DRAWING = PLOT_DEBUG_DRAWING
}
}