Skip to content

Commit

Permalink
Merge pull request #66 from starschema/#32-More-formatting-options-fo…
Browse files Browse the repository at this point in the history
…r-legend

#32 more formatting options for legend
  • Loading branch information
szilardhuber authored May 19, 2023
2 parents 9ea1df8 + 2ece8ae commit f6bb77a
Show file tree
Hide file tree
Showing 8 changed files with 239 additions and 29 deletions.
100 changes: 100 additions & 0 deletions capabilities.json
Original file line number Diff line number Diff line change
Expand Up @@ -1817,6 +1817,22 @@
"legends": {
"displayName": "Legend Customization",
"properties": {
"legendWidth": {
"displayName": "Width (px)",
"displayNameKey": "legendWidth",
"description": "Set the width of the legend box",
"type": {
"numeric": true
}
},
"legendHeight": {
"displayName": "Height (px)",
"displayNameKey": "legendHeight",
"description": "Set the height of the legend box",
"type": {
"numeric": true
}
},
"opacity": {
"displayName": "Opacity",
"displayNameKey": "opacity",
Expand All @@ -1825,6 +1841,35 @@
"numeric": true
}
},
"orientation": {
"displayName": "Orientation",
"displayNameKey": "orientation",
"description": "Set orientation for the legend elements",
"type": {
"enumeration": [
{
"value": "column",
"displayName": "Vertical",
"displayNameKey": "vertical"
},
{
"value": "row",
"displayName": "Horizontal",
"displayNameKey": "horizontal"
}
]
}
},
"alignment": {
"displayName": "Alignment",
"displayNameKey": "alignment",
"description": "Set alignment for the legend elements",
"type": {
"formatting": {
"alignment": true
}
}
},
"fontSize": {
"displayName": "Font Size",
"displayNameKey": "fontSize",
Expand Down Expand Up @@ -1937,6 +1982,61 @@
}
}
}
},
"borderWidth": {
"displayName": "Border Width",
"displayNameKey": "borderWidth",
"description": "Set the border width of the legend box",
"type": {
"numeric": true
}
},
"borderRadius": {
"displayName": "Border Radius (px)",
"displayNameKey": "borderRadius",
"description": "Set the border radius of the legend box",
"type": {
"numeric": true
}
},
"borderOpacity": {
"displayName": "Border Opacity",
"displayNameKey": "borderOpacity",
"description": "Set opacity for the legend border",
"type": {
"numeric": true
}
},
"borderColor": {
"displayName": "Border Color",
"displayNameKey": "borderColor",
"description": "Set the color for the border",
"type": {
"fill": {
"solid": {
"color": true
}
}
}
},
"order": {
"displayName": "Order",
"displayNameKey": "order",
"description": "Set the order of the legend elements",
"type": {
"enumeration": [
{
"value": "desc",
"displayName": "Descending",
"displayNameKey": "desc"
},
{
"value": "asc",
"displayName": "Ascending",
"displayNameKey": "asc"
}
]
}
}
}
},
Expand Down
4 changes: 4 additions & 0 deletions src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,8 @@ export module constants {
export const MAX_SELECTION_COUNT = 1000;
export const MAPBOX_CTRL_ICON_CLASS = 'mapboxgl-ctrl-icon';
export const CONTROL_BUTTON_LASSO = 'mapbox-gl-draw_lasso';
export const DEFAULT_HOR_LEGEND_WIDTH = 550
export const DEFAULT_HOR_LEGEND_HEIGHT = 50
export const DEFAULT_VER_LEGEND_WIDTH = 124
export const DEFAULT_VER_LEGEND_HEIGHT = 180
}
2 changes: 1 addition & 1 deletion src/layers/cluster.ts
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,7 @@ export class Cluster extends Layer {
const colorStops = this.getColorStops();
const format = LegendControl.DEFAULT_NUMBER_FORMAT;
const legendPosition = settings[this.id].legendPosition
legend.addLegend(id, title, colorStops, format, legendPosition);
legend.addLegend(id, title, colorStops, format, legendPosition, settings);
}

hasTooltip(tooltips) {
Expand Down
2 changes: 1 addition & 1 deletion src/layers/layer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -298,6 +298,6 @@ export abstract class Layer {
const colorStops = this.getColorStops();
const format = roleMap.getColumn('color', this.getId(), settings[this.id].colorField - 1)?.format;
const legendPosition = settings[this.id].legendPosition
legend.addLegend(id, title, colorStops, format, legendPosition);
legend.addLegend(id, title, colorStops, format, legendPosition, settings);
}
}
129 changes: 108 additions & 21 deletions src/legendControl.ts
Original file line number Diff line number Diff line change
@@ -1,32 +1,39 @@
import powerbiVisualsApi from "powerbi-visuals-api";
import IVisualHost = powerbiVisualsApi.extensibility.visual.IVisualHost;
import VisualObjectInstancesToPersist = powerbiVisualsApi.VisualObjectInstancesToPersist;
import * as formatting from "powerbi-visuals-utils-formattingutils"
import { dragElement } from "./mapboxUtils"
import { LegendContainer } from "./legendContainer"
import { LegendSettings } from "./settings";
import { LegendSettings, MapboxSettings } from "./settings";
import { constants } from "./constants"
const valueFormatter = formatting.valueFormatter;

export type ColorStops = { colorStop: number | string, color: string }[];

export class LegendControl {
private map: mapboxgl.Map;
private settings: LegendSettings;
private legendContainer: { [legendPosition: string]: LegendContainer } = {};
private legends: { [legendPosition: string]: { [key: string]: HTMLElement } } = {};
private opacity: number;
private isNewLegendCreated: boolean;
private positions: any[] = ["bottom-right", "bottom-left", "top-right", "top-left"];
private orientation: string = "column";
private legendHeight: number;
private legendWidth: number;
private host: IVisualHost;

public static readonly DEFAULT_NUMBER_FORMAT = "0.##"

constructor(map) {
constructor(map, host) {
this.map = map;
this.host = host
}

addControl(settings: LegendSettings) {
this.positions.forEach(legendPosition => {
this.legendContainer[legendPosition] = new LegendContainer(legendPosition)
this.map.addControl(this.legendContainer[legendPosition], legendPosition)
})
this.settings = settings
}

removeControl() {
Expand All @@ -50,7 +57,7 @@ export class LegendControl {
this.legends = {}
}

addLegend(key: string, title: string, data: ColorStops, format: string, legendPosition: string) {
addLegend(key: string, title: string, data: ColorStops, format: string, legendPosition: string, mapBoxSettings: MapboxSettings) {
if (data) {
if (!this.legends[legendPosition]) {
this.legends[legendPosition] = {}
Expand All @@ -60,10 +67,10 @@ export class LegendControl {
this.legends[legendPosition][key].firstChild.remove()
}
this.isNewLegendCreated = false
this.addValuesToLegend(title, data, format, this.legends[legendPosition][key])
this.addValuesToLegend(title, data, format, this.legends[legendPosition][key], mapBoxSettings.legends)
} else {
this.isNewLegendCreated = true
this.legends[legendPosition][key] = this.createLegendElement(title, data, format)
this.legends[legendPosition][key] = this.createLegendElement(title, data, format, mapBoxSettings.legends)
}
}
}
Expand All @@ -82,49 +89,98 @@ export class LegendControl {
this.opacity = opacity / 100
}

createLegendElement(title: string, data: ColorStops, format: string): HTMLElement {
createLegendElement(title: string, data: ColorStops, format: string, settings: LegendSettings): HTMLElement {
if (this.hasOrientationChanged(settings)) {
this.persistSizeProperties(settings)
}

const d = document;
const legend = d.createElement('div');
legend.setAttribute("class", "mapbox-legend mapboxgl-ctrl-group");
legend.setAttribute("style", `
opacity: ${this.settings.opacity / 100};
font-size: ${this.settings.fontSize}px;
font-family: ${this.settings.font == "other" ? this.settings.customFont : this.settings.font};
font-weight: ${this.settings.fontWeight};
color: ${this.settings.fontColor};
background-color: ${this.settings.backgroundColor};
border-color: ${this.settings.backgroundColor};
font-size: ${settings.fontSize}px;
font-family: ${settings.font == "other" ? settings.customFont : settings.font};
font-weight: ${settings.fontWeight};
color: ${this.hexToRgb(settings.fontColor, settings.opacity / 100)};
background-color: ${this.hexToRgb(settings.backgroundColor, settings.opacity / 100)};
width: ${settings.legendWidth - 24}px;
height: ${settings.legendHeight - 30}px;
display: flex;
flex-direction: ${settings.orientation};
justify-content: space-around;
border: ${settings.borderWidth}px solid ${this.hexToRgb(settings.borderColor, settings.borderOpacity / 100)};
box-shadow: none;
border-radius: ${settings.borderRadius}px;
text-align: ${settings.alignment};
`);

this.orientation = settings.orientation
const legendContainer = document.querySelector('.mapbox-legend-container') as HTMLElement;
legendContainer.style.setProperty('--legendWidth', `${settings.legendWidth - 24}px`);

dragElement(legend)
this.addValuesToLegend(title, data, format, legend)
this.addValuesToLegend(title, data, format, legend, settings)

return legend
}

addValuesToLegend(title: string, data: ColorStops, format: string, legend: HTMLElement) {
addValuesToLegend(title: string, data: ColorStops, format: string, legend: HTMLElement, settings: LegendSettings) {
const d = document
const titleElement = d.createElement('div');
const titleText = d.createTextNode(title);
titleElement.className = 'mapbox-legend-title';
titleElement.appendChild(titleText);
if (settings.orientation === 'column') {
titleElement.removeAttribute("style")
} else {
titleElement.setAttribute("style", `
display: flex;
align-items: center;
`);
}
legend.appendChild(titleElement)

if (data.some(item => typeof item["colorStop"] === "string")) {
data.sort(this.compareStringValues)
data.sort((a, b) => this.compareStringValues(a, b, settings.order))
}

let minWidth = "unset"

if (settings.alignment === 'center') {
const colorStops = data.map(el => Number(el.colorStop).toFixed(2).toString().length);
const longestValue = Math.max(...colorStops) + 1
minWidth = (settings.fontSize/2) * longestValue + 'px'
}

data.sort((a, b) => settings.order === "asc" ? this.getColorStopValue(a) - this.getColorStopValue(b) : this.getColorStopValue(b) - this.getColorStopValue(a))

data.forEach(({colorStop, color}) => {
// Create line item
const item = d.createElement('div');
item.setAttribute("style", `
display: flex;
flex-direction: ${settings.alignment === 'right' ? 'row-reverse' : 'row'};
align-items: center;
justify-content: ${settings.alignment === 'right' ? 'end' : settings.alignment === 'left' ? 'start' : 'center'};
`);

// Create color element and add to item
const colorElement = d.createElement('span');
colorElement.setAttribute("class", "mapbox-legend-color middle");
colorElement.setAttribute("style", `background-color: ${color};`);
colorElement.setAttribute("style", `
background-color: ${this.hexToRgb(color, settings.opacity / 100)};
width: ${settings.fontSize}px;
height: ${settings.fontSize}px;
margin-left: 5px;
`);

item.appendChild(colorElement);

// Create value element and add to item
const valueElement = document.createElement('span');
valueElement.setAttribute("style", `
min-width: ${minWidth};
`);
valueElement.setAttribute("class", "mapbox-legend-value middle");
if (typeof colorStop === "number") {
const valueText = d.createTextNode(valueFormatter.format(colorStop, format || LegendControl.DEFAULT_NUMBER_FORMAT));
Expand All @@ -140,10 +196,41 @@ export class LegendControl {
})
}

compareStringValues(a: object, b: object): number {
compareStringValues(a: object, b: object, order: string): number {
const pattern = /^\d+/;
const aNum = parseInt((a["colorStop"].match(pattern) ?? '0') as string);
const bNum = parseInt((b["colorStop"].match(pattern) ?? '0') as string);
return aNum - bNum;
return order === "asc" ? aNum - bNum : bNum - aNum;
}

getColorStopValue(obj: Object) {
return Number(obj["colorStop"]) || 0;
}

hexToRgb(hex: string, op: number) {
hex = hex.replace('#', '');

const r = parseInt(hex.substring(0, 2), 16);
const g = parseInt(hex.substring(2, 4), 16);
const b = parseInt(hex.substring(4, 6), 16);

return `rgba(${r}, ${g}, ${b}, ${op})`;
};

hasOrientationChanged(settings: LegendSettings): boolean {
return this.orientation !== settings.orientation
}

persistSizeProperties(settings: LegendSettings) {
this.host.persistProperties(<VisualObjectInstancesToPersist>{
merge: [{
objectName: "legends",
selector: null,
properties: {
legendWidth: settings.orientation === "row" ? constants.DEFAULT_HOR_LEGEND_WIDTH : constants.DEFAULT_VER_LEGEND_WIDTH,
legendHeight: settings.orientation === "row" ? constants.DEFAULT_HOR_LEGEND_HEIGHT : constants.DEFAULT_VER_LEGEND_HEIGHT,
}
}]
})
}
}
Loading

0 comments on commit f6bb77a

Please sign in to comment.