Skip to content

Legend alignment #1333

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Apr 25, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions future_changes.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Original file line number Diff line number Diff line change
Expand Up @@ -60,19 +60,18 @@ class LegendComponent(
): SvgElement {
val breakComponent = GroupComponent()

// key element
breakComponent.add(createKeyElement(br, keySize))

// add label at position as was layout
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, 0.5 * (keyLabelBox.height - keySize.y))))
return breakComponent.rootGroup
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,53 +62,47 @@ abstract class LegendComponentLayout(
}
}

private fun indexToPosition(i: Int): Pair<Int, Int> = 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(
Expand All @@ -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))
Expand Down Expand Up @@ -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))
}
}

Expand Down