Skip to content

Commit d039940

Browse files
Legend alignment (#1333)
1 parent 3f742eb commit d039940

File tree

3 files changed

+41
-77
lines changed

3 files changed

+41
-77
lines changed

future_changes.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,5 @@
1010

1111
### Fixed
1212
- 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)].
13+
- Multiline legend labels are not vertically centered with their keys [[#1331](https://github.com/JetBrains/lets-plot/issues/1331)]
14+
- Poor alignment in legend between columns [[#1332](https://github.com/JetBrains/lets-plot/issues/1332)]

plot-builder/src/commonMain/kotlin/org/jetbrains/letsPlot/core/plot/builder/guide/LegendComponent.kt

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -60,19 +60,18 @@ class LegendComponent(
6060
): SvgElement {
6161
val breakComponent = GroupComponent()
6262

63-
// key element
6463
breakComponent.add(createKeyElement(br, keySize))
6564

6665
// add label at position as was layout
6766
val label = MultilineLabel(br.label)
6867
val lineHeight = PlotLabelSpecFactory.legendItem(theme).height()
6968
label.addClassName(Style.LEGEND_ITEM)
70-
label.setHorizontalAnchor(Text.HorizontalAnchor.LEFT)
7169
label.setLineHeight(lineHeight)
72-
label.moveTo(labelBox.origin.add(DoubleVector(0.0, lineHeight * 0.35)))// centre the first line
70+
label.setHorizontalAnchor(Text.HorizontalAnchor.LEFT)
71+
label.setVerticalAnchor(Text.VerticalAnchor.CENTER)
72+
label.moveTo(labelBox.origin)
7373
breakComponent.add(label)
74-
75-
breakComponent.moveTo(keyLabelBox.origin)
74+
breakComponent.moveTo(keyLabelBox.origin.add(DoubleVector(0.0, 0.5 * (keyLabelBox.height - keySize.y))))
7675
return breakComponent.rootGroup
7776
}
7877

plot-builder/src/commonMain/kotlin/org/jetbrains/letsPlot/core/plot/builder/guide/LegendComponentLayout.kt

Lines changed: 35 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -62,53 +62,47 @@ abstract class LegendComponentLayout(
6262
}
6363
}
6464

65+
private fun indexToPosition(i: Int): Pair<Int, Int> = if (isFillByRow) {
66+
val row = i / colCount
67+
val col = i % colCount
68+
row to col
69+
} else {
70+
val col = i / rowCount
71+
val row = i % rowCount
72+
row to col
73+
}
74+
6575
private fun doLayout() {
66-
val horizontalGap = PlotLabelSpecFactory.legendItem(theme).width(PlotLabelSpecFactory.DISTANCE_TO_LABEL_IN_CHARS)
67-
val intervalBetweenLabels = DoubleVector(horizontalGap, PlotLabelSpecFactory.legendItem(theme).height() / 3)
68-
69-
val contentOrigin = DoubleVector.ZERO
70-
var breakBoxBounds: DoubleRectangle? = null
71-
for (i in breaks.indices) {
72-
val labelSize = labelSize(i).add(intervalBetweenLabels)
73-
val keySize = keySizes[i]
74-
val height = max(keySize.y, labelSize.y)
75-
val labelVOffset = keySize.y / 2
76-
val labelHOffset = keySize.x + horizontalGap / 2
77-
val breakBoxSize = DoubleVector(labelHOffset + labelSize.x, height)
78-
.let {
79-
// Not add a space for the last item in the row/column
80-
val xSpacing = if (i / rowCount != colCount - 1) {
81-
theme.keySpacing().x
82-
} else {
83-
0.0
84-
}
85-
val ySpacing = if (i % rowCount != rowCount - 1) {
86-
theme.keySpacing().y
87-
} else {
88-
0.0
89-
}
90-
it.add(DoubleVector(xSpacing, ySpacing))
91-
}
92-
93-
breakBoxBounds = DoubleRectangle(
94-
breakBoxBounds?.let { breakBoxOrigin(i, it) } ?: contentOrigin,
95-
breakBoxSize
96-
)
76+
val labelSpec = PlotLabelSpecFactory.legendItem(theme)
77+
val keyLabelGap = labelSpec.width(PlotLabelSpecFactory.DISTANCE_TO_LABEL_IN_CHARS) / 2.0
78+
val defaultSpacing = DoubleVector(keyLabelGap, labelSpec.height() / 3.0)
79+
val spacingBetweenLabels = theme.keySpacing().add(defaultSpacing)
80+
81+
val colWidths = DoubleArray(colCount)
82+
val rowHeights = DoubleArray(rowCount)
83+
84+
keySizes.forEachIndexed { i, keySize ->
85+
val (row, col) = indexToPosition(i)
86+
val labelSize = labelSize(i)
87+
val labelOffset = DoubleVector(keySize.x + keyLabelGap, keySize.y / 2)
88+
myLabelBoxes += DoubleRectangle(labelOffset, labelSize)
9789

90+
colWidths[col] = maxOf(colWidths[col], labelOffset.x + labelSize.x)
91+
rowHeights[row] = maxOf(rowHeights[row], keySize.y, labelSize.y)
92+
}
93+
94+
val colX = colWidths.runningFold(0.0) { acc, w -> acc + w + spacingBetweenLabels.x }
95+
val rowY = rowHeights.runningFold(0.0) { acc, h -> acc + h + spacingBetweenLabels.y }
96+
97+
breaks.indices.forEach { i ->
98+
val (row, col) = indexToPosition(i)
99+
val breakBoxBounds = DoubleRectangle(colX[col], rowY[row], colWidths[col], rowHeights[row])
98100
myKeyLabelBoxes.add(breakBoxBounds)
99-
myLabelBoxes.add(
100-
DoubleRectangle(
101-
labelHOffset, labelVOffset,
102-
labelSize.x, labelSize.y
103-
)
104-
)
105101
}
106102

107-
myContentSize = GeometryUtil.union(DoubleRectangle(contentOrigin, DoubleVector.ZERO), myKeyLabelBoxes).dimension
103+
myContentSize = GeometryUtil.union(DoubleRectangle.ZERO, myKeyLabelBoxes).dimension
108104
}
109105

110-
protected abstract fun breakBoxOrigin(index: Int, prevBreakBoxBounds: DoubleRectangle): DoubleVector
111-
112106
protected abstract fun labelSize(index: Int): DoubleVector
113107

114108
private class MyHorizontal internal constructor(
@@ -126,10 +120,6 @@ abstract class LegendComponentLayout(
126120
rowCount = 1
127121
}
128122

129-
override fun breakBoxOrigin(index: Int, prevBreakBoxBounds: DoubleRectangle): DoubleVector {
130-
return DoubleVector(prevBreakBoxBounds.right, 0.0)
131-
}
132-
133123
override fun labelSize(index: Int): DoubleVector {
134124
val label = breaks[index].label
135125
return PlotLayoutUtil.textDimensions(label, PlotLabelSpecFactory.legendItem(theme))
@@ -175,36 +165,9 @@ abstract class LegendComponentLayout(
175165
legendDirection: LegendDirection,
176166
theme: LegendTheme
177167
) : LegendComponentLayout(title, breaks, keySizes, legendDirection, theme) {
178-
private var myMaxLabelWidth = 0.0
179-
180-
init {
181-
for (br in breaks) {
182-
myMaxLabelWidth = max(
183-
myMaxLabelWidth,
184-
PlotLayoutUtil.textDimensions(br.label, PlotLabelSpecFactory.legendItem(theme)).x
185-
)
186-
}
187-
}
188-
189-
override fun breakBoxOrigin(index: Int, prevBreakBoxBounds: DoubleRectangle): DoubleVector {
190-
if (isFillByRow) {
191-
return if (index % colCount == 0) {
192-
DoubleVector(0.0, prevBreakBoxBounds.bottom)
193-
} else DoubleVector(prevBreakBoxBounds.right, prevBreakBoxBounds.top)
194-
}
195-
196-
// fill by column
197-
return if (index % rowCount == 0) {
198-
DoubleVector(prevBreakBoxBounds.right, 0.0)
199-
} else DoubleVector(prevBreakBoxBounds.left, prevBreakBoxBounds.bottom)
200-
201-
}
202168

203169
override fun labelSize(index: Int): DoubleVector {
204-
return DoubleVector(
205-
myMaxLabelWidth,
206-
PlotLayoutUtil.textDimensions(breaks[index].label, PlotLabelSpecFactory.legendItem(theme)).y
207-
)
170+
return PlotLayoutUtil.textDimensions(breaks[index].label, PlotLabelSpecFactory.legendItem(theme))
208171
}
209172
}
210173

0 commit comments

Comments
 (0)