From ea797bd185a2b6332fd414a61f0b156e5f6e5928 Mon Sep 17 00:00:00 2001 From: Mikhail Koroteev Date: Thu, 24 Apr 2025 01:56:23 +0300 Subject: [PATCH 1/4] Fix legend alignment, issues #1331 and #1332 --- .../plot/builder/guide/LegendComponent.kt | 6 +- .../builder/guide/LegendComponentLayout.kt | 107 ++++++------------ 2 files changed, 39 insertions(+), 74 deletions(-) diff --git a/plot-builder/src/commonMain/kotlin/org/jetbrains/letsPlot/core/plot/builder/guide/LegendComponent.kt b/plot-builder/src/commonMain/kotlin/org/jetbrains/letsPlot/core/plot/builder/guide/LegendComponent.kt index 9a866664240..239fc381027 100644 --- a/plot-builder/src/commonMain/kotlin/org/jetbrains/letsPlot/core/plot/builder/guide/LegendComponent.kt +++ b/plot-builder/src/commonMain/kotlin/org/jetbrains/letsPlot/core/plot/builder/guide/LegendComponent.kt @@ -67,12 +67,14 @@ class LegendComponent( val label = MultilineLabel(br.label) val lineHeight = PlotLabelSpecFactory.legendItem(theme).height() label.addClassName(Style.LEGEND_ITEM) - label.setHorizontalAnchor(Text.HorizontalAnchor.LEFT) label.setLineHeight(lineHeight) - label.moveTo(labelBox.origin.add(DoubleVector(0.0, lineHeight * 0.35)))// centre the first line + label.setHorizontalAnchor(Text.HorizontalAnchor.LEFT) + label.setVerticalAnchor(Text.VerticalAnchor.CENTER) + label.moveTo(labelBox.origin) breakComponent.add(label) breakComponent.moveTo(keyLabelBox.origin) + breakComponent.moveTo(keyLabelBox.origin.add(DoubleVector(0.0,keyLabelBox.height * 0.5 - keySize.y * 0.5))) return breakComponent.rootGroup } diff --git a/plot-builder/src/commonMain/kotlin/org/jetbrains/letsPlot/core/plot/builder/guide/LegendComponentLayout.kt b/plot-builder/src/commonMain/kotlin/org/jetbrains/letsPlot/core/plot/builder/guide/LegendComponentLayout.kt index d4ef1a3c474..abb015b4f15 100644 --- a/plot-builder/src/commonMain/kotlin/org/jetbrains/letsPlot/core/plot/builder/guide/LegendComponentLayout.kt +++ b/plot-builder/src/commonMain/kotlin/org/jetbrains/letsPlot/core/plot/builder/guide/LegendComponentLayout.kt @@ -62,53 +62,47 @@ abstract class LegendComponentLayout( } } + private fun indexToPosition(i: Int): Pair = if (isFillByRow) { + val row = i / colCount + val col = i % colCount + row to col + } else { + val col = i / rowCount + val row = i % rowCount + row to col + } + private fun doLayout() { - val horizontalGap = PlotLabelSpecFactory.legendItem(theme).width(PlotLabelSpecFactory.DISTANCE_TO_LABEL_IN_CHARS) - val intervalBetweenLabels = DoubleVector(horizontalGap, PlotLabelSpecFactory.legendItem(theme).height() / 3) - - val contentOrigin = DoubleVector.ZERO - var breakBoxBounds: DoubleRectangle? = null - for (i in breaks.indices) { - val labelSize = labelSize(i).add(intervalBetweenLabels) - val keySize = keySizes[i] - val height = max(keySize.y, labelSize.y) - val labelVOffset = keySize.y / 2 - val labelHOffset = keySize.x + horizontalGap / 2 - val breakBoxSize = DoubleVector(labelHOffset + labelSize.x, height) - .let { - // Not add a space for the last item in the row/column - val xSpacing = if (i / rowCount != colCount - 1) { - theme.keySpacing().x - } else { - 0.0 - } - val ySpacing = if (i % rowCount != rowCount - 1) { - theme.keySpacing().y - } else { - 0.0 - } - it.add(DoubleVector(xSpacing, ySpacing)) - } - - breakBoxBounds = DoubleRectangle( - breakBoxBounds?.let { breakBoxOrigin(i, it) } ?: contentOrigin, - breakBoxSize - ) + val labelSpec = PlotLabelSpecFactory.legendItem(theme) + val keyLabelGap = labelSpec.width(PlotLabelSpecFactory.DISTANCE_TO_LABEL_IN_CHARS) / 2.0 + val defaultSpacing = DoubleVector(keyLabelGap, labelSpec.height() / 3.0) + val spacingBetweenLabels = theme.keySpacing().add(defaultSpacing) + + val colWidths = DoubleArray(colCount) + val rowHeights = DoubleArray(rowCount) + + keySizes.forEachIndexed { i, keySize -> + val (row, col) = indexToPosition(i) + val labelSize = labelSize(i) + val labelOffset = DoubleVector(keySize.x + keyLabelGap, keySize.y / 2) + myLabelBoxes += DoubleRectangle(labelOffset, labelSize) + colWidths[col] = maxOf(colWidths[col], labelOffset.x + labelSize.x) + rowHeights[row] = maxOf(rowHeights[row], keySize.y, labelSize.y) + } + + val colX = colWidths.runningFold(0.0) { acc, w -> acc + w + spacingBetweenLabels.x } + val rowY = rowHeights.runningFold(0.0) { acc, h -> acc + h + spacingBetweenLabels.y } + + breaks.indices.forEach { i -> + val (row, col) = indexToPosition(i) + val breakBoxBounds = DoubleRectangle(colX[col], rowY[row], colWidths[col], rowHeights[row]) myKeyLabelBoxes.add(breakBoxBounds) - myLabelBoxes.add( - DoubleRectangle( - labelHOffset, labelVOffset, - labelSize.x, labelSize.y - ) - ) } - myContentSize = GeometryUtil.union(DoubleRectangle(contentOrigin, DoubleVector.ZERO), myKeyLabelBoxes).dimension + myContentSize = GeometryUtil.union(DoubleRectangle.ZERO, myKeyLabelBoxes).dimension } - protected abstract fun breakBoxOrigin(index: Int, prevBreakBoxBounds: DoubleRectangle): DoubleVector - protected abstract fun labelSize(index: Int): DoubleVector private class MyHorizontal internal constructor( @@ -126,10 +120,6 @@ abstract class LegendComponentLayout( rowCount = 1 } - override fun breakBoxOrigin(index: Int, prevBreakBoxBounds: DoubleRectangle): DoubleVector { - return DoubleVector(prevBreakBoxBounds.right, 0.0) - } - override fun labelSize(index: Int): DoubleVector { val label = breaks[index].label return PlotLayoutUtil.textDimensions(label, PlotLabelSpecFactory.legendItem(theme)) @@ -175,36 +165,9 @@ abstract class LegendComponentLayout( legendDirection: LegendDirection, theme: LegendTheme ) : LegendComponentLayout(title, breaks, keySizes, legendDirection, theme) { - private var myMaxLabelWidth = 0.0 - - init { - for (br in breaks) { - myMaxLabelWidth = max( - myMaxLabelWidth, - PlotLayoutUtil.textDimensions(br.label, PlotLabelSpecFactory.legendItem(theme)).x - ) - } - } - - override fun breakBoxOrigin(index: Int, prevBreakBoxBounds: DoubleRectangle): DoubleVector { - if (isFillByRow) { - return if (index % colCount == 0) { - DoubleVector(0.0, prevBreakBoxBounds.bottom) - } else DoubleVector(prevBreakBoxBounds.right, prevBreakBoxBounds.top) - } - - // fill by column - return if (index % rowCount == 0) { - DoubleVector(prevBreakBoxBounds.right, 0.0) - } else DoubleVector(prevBreakBoxBounds.left, prevBreakBoxBounds.bottom) - - } override fun labelSize(index: Int): DoubleVector { - return DoubleVector( - myMaxLabelWidth, - PlotLayoutUtil.textDimensions(breaks[index].label, PlotLabelSpecFactory.legendItem(theme)).y - ) + return PlotLayoutUtil.textDimensions(breaks[index].label, PlotLabelSpecFactory.legendItem(theme)) } } From 4323a23c904f3660693467266119458354eef1aa Mon Sep 17 00:00:00 2001 From: Mikhail Koroteev Date: Thu, 24 Apr 2025 02:06:31 +0300 Subject: [PATCH 2/4] Clean up the code --- .../letsPlot/core/plot/builder/guide/LegendComponent.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/plot-builder/src/commonMain/kotlin/org/jetbrains/letsPlot/core/plot/builder/guide/LegendComponent.kt b/plot-builder/src/commonMain/kotlin/org/jetbrains/letsPlot/core/plot/builder/guide/LegendComponent.kt index 239fc381027..c3d7cd71621 100644 --- a/plot-builder/src/commonMain/kotlin/org/jetbrains/letsPlot/core/plot/builder/guide/LegendComponent.kt +++ b/plot-builder/src/commonMain/kotlin/org/jetbrains/letsPlot/core/plot/builder/guide/LegendComponent.kt @@ -74,6 +74,7 @@ class LegendComponent( breakComponent.add(label) breakComponent.moveTo(keyLabelBox.origin) + breakComponent.moveTo(keyLabelBox.origin.add(DoubleVector(0.0,keyLabelBox.height * 0.5 - keySize.y * 0.5))) return breakComponent.rootGroup } From 376ef7eb5b0d4df303c3052894c3c7943dd9188a Mon Sep 17 00:00:00 2001 From: Mikhail Koroteev Date: Thu, 24 Apr 2025 02:14:17 +0300 Subject: [PATCH 3/4] Clean up the code --- .../letsPlot/core/plot/builder/guide/LegendComponent.kt | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/plot-builder/src/commonMain/kotlin/org/jetbrains/letsPlot/core/plot/builder/guide/LegendComponent.kt b/plot-builder/src/commonMain/kotlin/org/jetbrains/letsPlot/core/plot/builder/guide/LegendComponent.kt index c3d7cd71621..e41bb762b66 100644 --- a/plot-builder/src/commonMain/kotlin/org/jetbrains/letsPlot/core/plot/builder/guide/LegendComponent.kt +++ b/plot-builder/src/commonMain/kotlin/org/jetbrains/letsPlot/core/plot/builder/guide/LegendComponent.kt @@ -60,7 +60,6 @@ class LegendComponent( ): SvgElement { val breakComponent = GroupComponent() - // key element breakComponent.add(createKeyElement(br, keySize)) // add label at position as was layout @@ -72,10 +71,7 @@ class LegendComponent( label.setVerticalAnchor(Text.VerticalAnchor.CENTER) label.moveTo(labelBox.origin) breakComponent.add(label) - - breakComponent.moveTo(keyLabelBox.origin) - - breakComponent.moveTo(keyLabelBox.origin.add(DoubleVector(0.0,keyLabelBox.height * 0.5 - keySize.y * 0.5))) + breakComponent.moveTo(keyLabelBox.origin.add(DoubleVector(0.0, 0.5 * (keyLabelBox.height - keySize.y)))) return breakComponent.rootGroup } From 09bdc4eb9a9cbe42b63be9a9108096bf729e5171 Mon Sep 17 00:00:00 2001 From: Mikhail Koroteev Date: Thu, 24 Apr 2025 04:17:08 +0300 Subject: [PATCH 4/4] Update future_changes (legend alignment) --- future_changes.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/future_changes.md b/future_changes.md index f06ec299647..20acb93becb 100644 --- a/future_changes.md +++ b/future_changes.md @@ -6,3 +6,5 @@ ### Fixed - Can't add layer which uses continuous data to a plot where other layers use discrete input [[#1323](https://github.com/JetBrains/lets-plot/issues/1323)]. +- Multiline legend labels are not vertically centered with their keys [[#1331](https://github.com/JetBrains/lets-plot/issues/1331)] +- Poor alignment in legend between columns [[#1332](https://github.com/JetBrains/lets-plot/issues/1332)]