Skip to content

Commit 6bb2988

Browse files
authored
Added new legend positions (#136)
* Added TopRight and BottomRight positions for Legend * Bump version * Added unittests * Fix comment * Added JSDC doc to new positions
1 parent 9256874 commit 6bb2988

9 files changed

Lines changed: 230 additions & 40 deletions

File tree

.github/workflows/release.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11

2-
name: Release Build
2+
name: release
33

44
on:
55
release:

CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,13 @@
11
## Changelog
22

3+
## 8.3.0
4+
5+
### Module `legend`
6+
* Added `LegendPosition.TopRight` and `LegendPosition.BottomRight` — horizontal legend with items right-aligned to the chart area; falls back to left-aligned with a navigation arrow on overflow.
7+
* Added matching `legendPosition` string constants `topRight` and `bottomRight`.
8+
* Exported orientation helpers: `isLeft`, `isRight`, `isTop`, `isBottom`, `isTopOrBottom`, `isCentered`, `isRightAligned`. `isTop`/`isBottom` now also match the new right-aligned variants.
9+
* Fixed clipping of vertical centered legends (`LeftCenter`/`RightCenter`) when items overflowed (missing `Math.max(0, ...)` clamp).
10+
311
## 8.2.2
412

513
* Updated packages

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "powerbi-visuals-utils-chartutils",
3-
"version": "8.2.2",
3+
"version": "8.3.0",
44
"description": "ChartUtils",
55
"main": "lib/index.js",
66
"module": "lib/index.js",

src/legend/legend.ts

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,10 +46,58 @@ export function isLeft(orientation: LegendPosition): boolean {
4646
}
4747
}
4848

49+
export function isRight(orientation: LegendPosition): boolean {
50+
switch (orientation) {
51+
case LegendPosition.Right:
52+
case LegendPosition.RightCenter:
53+
return true;
54+
default:
55+
return false;
56+
}
57+
}
58+
4959
export function isTop(orientation: LegendPosition): boolean {
5060
switch (orientation) {
5161
case LegendPosition.Top:
5262
case LegendPosition.TopCenter:
63+
case LegendPosition.TopRight:
64+
return true;
65+
default:
66+
return false;
67+
}
68+
}
69+
70+
export function isBottom(orientation: LegendPosition): boolean {
71+
switch (orientation) {
72+
case LegendPosition.Bottom:
73+
case LegendPosition.BottomCenter:
74+
case LegendPosition.BottomRight:
75+
return true;
76+
default:
77+
return false;
78+
}
79+
}
80+
81+
export function isTopOrBottom(orientation: LegendPosition): boolean {
82+
return isTop(orientation) || isBottom(orientation);
83+
}
84+
85+
export function isCentered(orientation: LegendPosition): boolean {
86+
switch (orientation) {
87+
case LegendPosition.TopCenter:
88+
case LegendPosition.BottomCenter:
89+
case LegendPosition.LeftCenter:
90+
case LegendPosition.RightCenter:
91+
return true;
92+
default:
93+
return false;
94+
}
95+
}
96+
97+
export function isRightAligned(orientation: LegendPosition): boolean {
98+
switch (orientation) {
99+
case LegendPosition.TopRight:
100+
case LegendPosition.BottomRight:
53101
return true;
54102
default:
55103
return false;

src/legend/legendInterfaces.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,20 @@ export enum LegendPosition {
3939
BottomCenter,
4040
RightCenter,
4141
LeftCenter,
42+
/**
43+
* Horizontal legend above the chart, items right-aligned to the chart area.
44+
* On overflow falls back to left-aligned layout with a navigation arrow
45+
* (when the legend is scrollable), instead of keeping right alignment with
46+
* horizontal clipping/scroll. This mirrors the native Power BI legend behavior.
47+
*/
48+
TopRight,
49+
/**
50+
* Horizontal legend below the chart, items right-aligned to the chart area.
51+
* On overflow falls back to left-aligned layout with a navigation arrow
52+
* (when the legend is scrollable), instead of keeping right alignment with
53+
* horizontal clipping/scroll. This mirrors the native Power BI legend behavior.
54+
*/
55+
BottomRight,
4256
}
4357

4458
export interface ISelectableDataPoint{

src/legend/legendPosition.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,3 +32,5 @@ export const topCenter: string = "TopCenter";
3232
export const bottomCenter: string = "BottomCenter";
3333
export const leftCenter: string = "LeftCenter";
3434
export const rightCenter: string = "RightCenter";
35+
export const topRight: string = "TopRight";
36+
export const bottomRight: string = "BottomRight";

src/legend/svgLegend.ts

Lines changed: 33 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ import {
3939
} from "powerbi-visuals-utils-svgutils";
4040

4141
import { ILegend, LegendData, LegendDataPoint, LegendPosition } from "./legendInterfaces";
42+
import { isBottom, isRight, isTopOrBottom, isCentered, isRightAligned } from "./legend";
4243

4344
import * as Markers from "./markers";
4445

@@ -168,14 +169,11 @@ export class SVGLegend implements ILegend {
168169
"width", legendViewport.width || (orientation === LegendPosition.None ? 0 : this.parentViewport.width)
169170
);
170171

171-
const isRight = orientation === LegendPosition.Right || orientation === LegendPosition.RightCenter,
172-
isBottom = orientation === LegendPosition.Bottom || orientation === LegendPosition.BottomCenter;
173-
174172
this.svg.style(
175-
"margin-left", isRight ? (this.parentViewport.width - legendViewport.width) + "px" : null
173+
"margin-left", isRight(orientation) ? (this.parentViewport.width - legendViewport.width) + "px" : null
176174
);
177175
this.svg.style(
178-
"margin-top", isBottom ? (this.parentViewport.height - legendViewport.height) + "px" : null,
176+
"margin-top", isBottom(orientation) ? (this.parentViewport.height - legendViewport.height) + "px" : null,
179177
);
180178
}
181179

@@ -185,6 +183,8 @@ export class SVGLegend implements ILegend {
185183
case LegendPosition.Bottom:
186184
case LegendPosition.TopCenter:
187185
case LegendPosition.BottomCenter:
186+
case LegendPosition.TopRight:
187+
case LegendPosition.BottomRight:
188188
const pixelHeight = PixelConverter.fromPointToPixel(this.data && this.data.fontSize
189189
? this.data.fontSize
190190
: SVGLegend.DefaultFontSizeInPt);
@@ -259,7 +259,7 @@ export class SVGLegend implements ILegend {
259259

260260
// Adding back the workaround for Legend Left/Right position for Map
261261
const mapControls = this.element.getElementsByClassName("mapControl");
262-
if (mapControls.length > 0 && !this.isTopOrBottom(this.orientation)) {
262+
if (mapControls.length > 0 && !isTopOrBottom(this.orientation)) {
263263
for (let i = 0; i < mapControls.length; ++i) {
264264
const element = <HTMLElement>mapControls[i];
265265
element.style.display = "inline-block";
@@ -274,18 +274,22 @@ export class SVGLegend implements ILegend {
274274

275275
const group = this.group;
276276

277-
// transform the wrapping group if position is centered
278-
if (this.isCentered(this.orientation)) {
277+
// transform the wrapping group if position is centered or right-aligned
278+
if (isCentered(this.orientation)) {
279279
let centerOffset = 0;
280-
if (this.isTopOrBottom(this.orientation)) {
280+
if (isTopOrBottom(this.orientation)) {
281281
centerOffset = Math.max(0, (this.parentViewport.width - this.visibleLegendWidth) / 2);
282282
group.attr("transform", svgManipulation.translate(centerOffset, 0));
283283
}
284284
else {
285-
centerOffset = Math.max((this.parentViewport.height - this.visibleLegendHeight) / 2);
285+
centerOffset = Math.max(0, (this.parentViewport.height - this.visibleLegendHeight) / 2);
286286
group.attr("transform", svgManipulation.translate(0, centerOffset));
287287
}
288288
}
289+
else if (isRightAligned(this.orientation)) {
290+
const rightOffset = Math.max(0, this.parentViewport.width - this.visibleLegendWidth);
291+
group.attr("transform", svgManipulation.translate(rightOffset, 0));
292+
}
289293
else {
290294
group.attr("transform", null);
291295
}
@@ -444,7 +448,7 @@ export class SVGLegend implements ILegend {
444448
const hasTitle = !!title;
445449

446450
if (hasTitle) {
447-
const isHorizontal = this.isTopOrBottom(this.orientation);
451+
const isHorizontal = isTopOrBottom(this.orientation);
448452

449453
const textProperties = SVGLegend.getTextProperties(title, this.data.fontSize, this.data.fontFamily);
450454
let text = title;
@@ -498,7 +502,7 @@ export class SVGLegend implements ILegend {
498502
let navArrows: NavigationArrow[];
499503
let numberOfItems: number;
500504

501-
if (this.isTopOrBottom(this.orientation)) {
505+
if (isTopOrBottom(this.orientation)) {
502506
navArrows = this.isScrollable ? this.calculateHorizontalNavigationArrowsLayout(title) : [];
503507
numberOfItems = this.calculateHorizontalLayout(dataPoints, title, navArrows);
504508
}
@@ -786,6 +790,23 @@ export class SVGLegend implements ILegend {
786790
}
787791

788792
this.visibleLegendWidth = occupiedWidth;
793+
794+
// When the legend is right-aligned (TopRight/BottomRight):
795+
// - If everything fits, the group is translated to the right edge in
796+
// drawLegendInternal and items render right-aligned.
797+
// - If items overflow, we unconditionally fall back to left-aligned
798+
// rendering by setting visibleLegendWidth to the full parent width so
799+
// the translation in drawLegendInternal becomes 0. This matches the
800+
// standard Top/Bottom overflow layout.
801+
// When scrolling is enabled (isScrollable), updateNavigationArrowLayout
802+
// will additionally place the "next" arrow at the right edge of the
803+
// viewport (parentViewport.width - LegendArrowWidth); when scrolling
804+
// is disabled, navigationArrows is empty and no arrow is rendered, but
805+
// the left-aligned fallback still applies.
806+
if (isRightAligned(this.orientation) && numberOfItems !== dataPointsLength) {
807+
this.visibleLegendWidth = this.parentViewport.width;
808+
}
809+
789810
this.updateNavigationArrowLayout(navigationArrows, dataPointsLength, numberOfItems);
790811

791812
return numberOfItems;
@@ -955,30 +976,6 @@ export class SVGLegend implements ILegend {
955976
.attr("transform", (d: NavigationArrow) => d.rotateTransform);
956977
}
957978

958-
private isTopOrBottom(orientation: LegendPosition): boolean {
959-
switch (orientation) {
960-
case LegendPosition.Top:
961-
case LegendPosition.Bottom:
962-
case LegendPosition.BottomCenter:
963-
case LegendPosition.TopCenter:
964-
return true;
965-
default:
966-
return false;
967-
}
968-
}
969-
970-
private isCentered(orientation: LegendPosition): boolean {
971-
switch (orientation) {
972-
case LegendPosition.BottomCenter:
973-
case LegendPosition.LeftCenter:
974-
case LegendPosition.RightCenter:
975-
case LegendPosition.TopCenter:
976-
return true;
977-
default:
978-
return false;
979-
}
980-
}
981-
982979
public reset(): void { }
983980

984981
private static getTextProperties(

0 commit comments

Comments
 (0)