Created
January 30, 2020 20:42
-
-
Save dm-p/f3a0470cd913350a0f29252743b16644 to your computer and use it in GitHub Desktop.
Power BI Community - re: Adding a Second Measure to Custom Visual (https://community.powerbi.com/t5/Developer/Adding-a-Second-Measure-to-Custom-Visual/m-p/916586/highlight/true#M22155)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
{ | |
"dataRoles": [ | |
{ | |
"displayName": "Bar Grouping", | |
"name": "myCategory", | |
"kind": "Grouping" | |
}, | |
{ | |
"displayName": "Actual", | |
"name": "actual", | |
"kind": "Measure" | |
}, | |
{ | |
"displayName": "Budget", | |
"name": "budget", | |
"kind": "Measure" | |
} | |
], | |
"dataViewMappings": [ | |
{ | |
"conditions": [ | |
{ | |
"myCategory": { | |
"max": 1 | |
}, | |
"actual": { | |
"max": 1 | |
}, | |
"budget": { | |
"max": 1 | |
} | |
} | |
], | |
"categorical": { | |
"categories": { | |
"for": { | |
"in": "myCategory" | |
}, | |
"dataReductionAlgorithm": { | |
"top": {} | |
} | |
}, | |
"values": { | |
"select": [ | |
{ | |
"bind": { | |
"to": "actual" | |
} | |
}, | |
{ | |
"bind": { | |
"to": "budget" | |
} | |
} | |
] | |
} | |
} | |
} | |
], | |
"objects": { | |
"barchartProperties": { | |
"displayName": "Barchart Properties", | |
"properties": { | |
"sortBySize": { | |
"displayName": "Sort by Size", | |
"type": { | |
"bool": true | |
} | |
}, | |
"barColor": { | |
"displayName": "Bar Color", | |
"type": { | |
"fill": { | |
"solid": { | |
"color": true | |
} | |
} | |
} | |
}, | |
"xAxisFontSize": { | |
"displayName": "X Axis Font Size", | |
"type": { | |
"integer": true | |
} | |
}, | |
"yAxisFontSize": { | |
"displayName": "Y Axis Font Size", | |
"type": { | |
"integer": true | |
} | |
} | |
} | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
"use strict"; | |
import { dataViewObjectsParser } from "powerbi-visuals-utils-dataviewutils"; | |
import DataViewObjectsParser = dataViewObjectsParser.DataViewObjectsParser; | |
import powerbi from "powerbi-visuals-api"; | |
import Fill = powerbi.Fill; | |
export class VisualSettings extends DataViewObjectsParser { | |
// public dataPoint: dataPointSettings = new dataPointSettings(); | |
public barchartProperties: BarchartProperties = new BarchartProperties(); | |
} | |
export class BarchartProperties { | |
sortBySize: boolean = true; | |
xAxisFontSize: number = 10; | |
yAxisFontSize: number = 10; | |
barColor: Fill = { "solid": { "color": "#018a80" } }; // default color is teal | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import "./../style/visual.less"; | |
import powerbi from "powerbi-visuals-api"; | |
import VisualConstructorOptions = powerbi.extensibility.visual.VisualConstructorOptions; | |
import VisualUpdateOptions = powerbi.extensibility.visual.VisualUpdateOptions; | |
import IVisual = powerbi.extensibility.visual.IVisual; | |
import DataView = powerbi.DataView; | |
import DataViewValueColumn = powerbi.DataViewValueColumn; | |
import DataViewCategorical = powerbi.DataViewCategorical; | |
import DataViewCategoricalColumn = powerbi.DataViewCategoricalColumn; | |
import DataViewCategoryColumn = powerbi.DataViewCategoryColumn; | |
import PrimitiveValue = powerbi.PrimitiveValue; | |
import IVisualHost = powerbi.extensibility.visual.IVisualHost; | |
import IColorPalette = powerbi.extensibility.IColorPalette; | |
import VisualObjectInstance = powerbi.VisualObjectInstance; | |
import VisualObjectInstanceEnumeration = powerbi.VisualObjectInstanceEnumeration; | |
import VisualObjectInstanceEnumerationObject = powerbi.VisualObjectInstanceEnumerationObject; | |
import EnumerateVisualObjectInstancesOptions = powerbi.EnumerateVisualObjectInstancesOptions; | |
import Fill = powerbi.Fill; | |
import VisualTooltipDataItem = powerbi.extensibility.VisualTooltipDataItem; | |
import ISelectionManager = powerbi.extensibility.ISelectionManager; | |
import { valueFormatter as vf, textMeasurementService as tms } from "powerbi-visuals-utils-formattingutils"; | |
import IValueFormatter = vf.IValueFormatter; | |
import { VisualSettings,BarchartProperties } from "./settings"; | |
import * as d3 from "d3"; | |
type Selection<T extends d3.BaseType> = d3.Selection<T, any, any, any>; | |
type DataSelection<T> = d3.Selection<d3.BaseType, T, any, any>; | |
export interface BarchartDataPoint{ | |
category: string; | |
actual: number; | |
budget: number; | |
} | |
export interface BarchartViewModel{ | |
IsNotValid: boolean; | |
DataPoints?:BarchartDataPoint[]; | |
Format?: string, | |
SortBySize?: boolean; | |
XAxisFontSize?: number; | |
YAxisFontSize?: number; | |
BarColor?: string | |
ColumnName?: string; | |
MeasureName?: string; | |
} | |
export class Barchart implements IVisual { | |
private svg: Selection<SVGElement>; | |
private barContainer: Selection<SVGGElement>; | |
private plotBackground: Selection<SVGRectElement>; | |
private barSelection: DataSelection<BarchartDataPoint>; | |
private xAxisContainer: Selection<SVGGElement>; | |
private yAxisContainer: Selection<SVGGElement>; | |
private barContainer2: Selection<SVGGElement>; | |
private plotBackground2: Selection<SVGRectElement>; | |
private barSelection2: DataSelection<BarchartDataPoint>; | |
private xAxisContainer2: Selection<SVGGElement>; | |
private yAxisContainer2: Selection<SVGGElement>; | |
private hostService: IVisualHost; | |
private settings: VisualSettings; | |
private viewModel: BarchartViewModel; | |
private static margin = { | |
top:20, | |
right: 20, | |
bottom: 20, | |
left: 50, | |
}; | |
constructor(options: VisualConstructorOptions) { | |
console.log('Constructor executing', options); | |
this.hostService = options.host; | |
this.svg = d3.select(options.element) | |
.append('svg') | |
.classed('Barchart',true); | |
this.barContainer = this.svg | |
.append('g') | |
.classed('barContainer', true); | |
this.plotBackground = this.barContainer | |
.append('rect') | |
.classed('plotBackground', true); | |
this.xAxisContainer = this.svg | |
.append('g') | |
.classed('xAxis', true); | |
this.yAxisContainer = this.svg | |
.append('g') | |
.classed('yAxis', true); | |
this.barContainer2 = this.svg | |
.append('g') | |
.classed('barContainer2', true); | |
this.plotBackground2 = this.barContainer2 | |
.append('rect') | |
.classed('plotBackground2', true); | |
this.xAxisContainer2 = this.svg | |
.append('g') | |
.classed('xAxis2', true); | |
this.yAxisContainer2 = this.svg | |
.append('g') | |
.classed('yAxis2', true); | |
this.settings = VisualSettings.getDefault() as VisualSettings; | |
} | |
public update(options: VisualUpdateOptions) { | |
var viewModel: BarchartViewModel = this.createViewModel(options.dataViews[0]); | |
if (viewModel.IsNotValid){ | |
return; | |
} | |
//set height and width of root SVG element using viewport passed by Power BI host | |
this.svg.attr("height",options.viewport.height); | |
this.svg.attr("width", options.viewport.width); | |
let marginLeft = Barchart.margin.left * (viewModel.YAxisFontSize / 10); | |
let marginBottom = Barchart.margin.bottom * (viewModel.XAxisFontSize / 10); | |
let marginTop = Barchart.margin.top; | |
let marginRight = Barchart.margin.right; | |
let plotArea = { | |
x: marginLeft, | |
y:marginTop, | |
width: (options.viewport.width - (marginLeft + Barchart.margin.right))/2, | |
height: (options.viewport.height - (marginTop + marginBottom)), | |
}; | |
let plotArea2 = { | |
x: plotArea.x + plotArea.width + 0.3, | |
y: marginTop, | |
}; | |
this.barContainer | |
.attr("transform","translate(" + plotArea.x + "," + plotArea.y + ")") | |
.attr("width",options.viewport.width) | |
.attr("height", options.viewport.height); | |
this.plotBackground | |
.attr("width", plotArea.width) | |
.attr("height", plotArea.height) | |
.style("fill","blue"); | |
this.barContainer2 | |
.attr("transform", "translate(" + plotArea.x + "," + plotArea.y + ")") | |
.attr("width", options.viewport.width) | |
.attr("height", options.viewport.height); | |
this.plotBackground2 | |
.attr("width", plotArea.width) | |
.attr("height", plotArea.height) | |
.style("fill", "none"); | |
var xScale = d3.scaleBand() | |
.rangeRound([0, plotArea.width]) | |
.padding(0.1) | |
.domain(viewModel.DataPoints.map((dataPoint:BarchartDataPoint) => dataPoint.category)); | |
this.xAxisContainer | |
.attr("class", "xAxis") | |
.attr("transform","translate(" + plotArea.x + "," + (plotArea.height + plotArea.y)+")") | |
.call(d3.axisBottom(xScale)); | |
this.xAxisContainer2 | |
.attr("class", "xAxis2") | |
.attr("transform", "translate(" + plotArea.x + "," + (plotArea.height + plotArea.y) + ")") | |
.call(d3.axisBottom(xScale)); | |
d3.select(".xAxis").selectAll("text").style("font-size",viewModel.XAxisFontSize); | |
d3.select(".xAxis2").selectAll("text").style("font-size", viewModel.XAxisFontSize); | |
let maxValueY: number = d3.max( | |
viewModel.DataPoints, | |
(dataPoint:BarchartDataPoint) => | |
/** Get the higher of either measure per group */ | |
+Math.max(dataPoint.actual, dataPoint.budget) | |
); | |
var valueFormatter = vf.create({ | |
format: viewModel.Format, | |
value: maxValueY/100, | |
cultureSelector: this.hostService.locale | |
}); | |
var yScale = d3.scaleLinear() | |
.rangeRound([plotArea.height,0]) | |
.domain([0,maxValueY * 1.02]); | |
var yAxis = d3.axisLeft(yScale) | |
.tickFormat((d) => valueFormatter.format(d)); | |
// .tickPadding(12).ticks(5); | |
this.yAxisContainer | |
.attr("class","yAxis") | |
.attr("transform", "translate(" + plotArea.x + "," + plotArea.y + ")") | |
.call(yAxis); | |
/* | |
this.yAxisContainer2 | |
.attr("class", "yAxis2") | |
.attr("transform", "translate(" + plotArea2.x + "," + plotArea.y + ")") | |
.call(yAxis); | |
*/ | |
d3.select(".yAxis").selectAll("text").style("font-size",viewModel.YAxisFontSize); | |
// d3.select(".yAxis2").selectAll("text").style("font-size", viewModel.YAxisFontSize); | |
this.barSelection2 = this.barContainer2 | |
.selectAll('.bar') | |
.data(viewModel.DataPoints); | |
this.barSelection = this.barContainer | |
.selectAll('.bar') | |
.data(viewModel.DataPoints); | |
const barSelectionMerged = this.barSelection | |
.enter() | |
.append('rect') | |
.merge(<any>this.barSelection) | |
.classed('bar',true); | |
const barSelectionMerged2 = this.barSelection2 | |
.enter() | |
.append('rect') | |
.merge(<any>this.barSelection2) | |
.classed('bar', true); | |
barSelectionMerged | |
.attr("x", (dataPoint: BarchartDataPoint) => xScale(dataPoint.category)) | |
.attr("y", (dataPoint: BarchartDataPoint) => yScale(Number(dataPoint.actual))) | |
.attr("width", xScale.bandwidth()) | |
.attr("height", (dataPoint: BarchartDataPoint) => (plotArea.height - yScale(Number(dataPoint.actual)))) | |
.style("fill",(dataPoint:BarchartDataPoint) => viewModel.BarColor); | |
barSelectionMerged2 | |
.attr("x", (dataPoint: BarchartDataPoint) => xScale(dataPoint.category) + xScale.bandwidth() / 4) | |
.attr("y", (dataPoint: BarchartDataPoint) => yScale(Number(dataPoint.budget))) | |
.attr("width", xScale.bandwidth() / 2) | |
.attr("height", (dataPoint: BarchartDataPoint) => (plotArea.height - yScale(Number(dataPoint.budget)))) | |
.style("fill", (dataPoint: BarchartDataPoint) => 'yellow') | |
.style("fill-opacity", (dataPoint: BarchartDataPoint) => 1); | |
this.barSelection | |
.exit() | |
.remove(); | |
} | |
public createViewModel(dataView: DataView): BarchartViewModel{ | |
//handle case where categorical DataView is not valid | |
if(typeof dataView === "undefined" || | |
typeof dataView.categorical === "undefined" || | |
typeof dataView.categorical.categories === "undefined" || | |
typeof dataView.categorical.values === "undefined"){ | |
return {IsNotValid: true}; | |
} | |
this.settings=VisualSettings.parse(dataView) as VisualSettings; | |
var categoricalDataView: DataViewCategorical = dataView.categorical; | |
var categoryColumn: DataViewCategoricalColumn = categoricalDataView.categories[0]; | |
var categoryNames: PrimitiveValue[] = categoricalDataView.categories[0].values; | |
var categoryValues: PrimitiveValue[] = categoricalDataView.values[0].values; | |
var BarchartDataPoints: BarchartDataPoint[] = []; | |
/** Iterate over the category values and push into the view model data points. | |
* The index is the same across categories and measures. | |
* actual = values[0] | |
* budget = values[1] | |
*/ | |
categoryNames.map((c, ci) => { /** c= category, ci = category array index */ | |
BarchartDataPoints.push({ | |
category: <string>c, | |
actual: <number>categoricalDataView.values[0].values[ci], | |
budget: <number>categoricalDataView.values[1].values[ci] | |
}); | |
}); | |
//get formatting code for the field that is the measure | |
var format: string = categoricalDataView.values[0].source.format | |
//get persistent property values | |
var SortBySize: boolean = this.settings.barchartProperties.sortBySize; | |
var xAxisFontSize: number = this.settings.barchartProperties.xAxisFontSize; | |
var yAxisFontSize: number = this.settings.barchartProperties.yAxisFontSize; | |
var barColor: string = typeof (this.settings.barchartProperties.barColor) == "string"? | |
this.settings.barchartProperties.barColor: | |
this.settings.barchartProperties.barColor.solid.color; | |
//sort dataset rows by measure value instead of cateogry value | |
if(SortBySize){ | |
BarchartDataPoints.sort((x,y) =>{return y.actual - x.actual}) | |
} | |
//return view model to upate method | |
return{ | |
IsNotValid: false, | |
DataPoints: BarchartDataPoints, | |
Format: format, | |
SortBySize: SortBySize, | |
BarColor: barColor, | |
XAxisFontSize: xAxisFontSize, | |
YAxisFontSize: yAxisFontSize, | |
ColumnName: dataView.metadata.columns[1].displayName, | |
MeasureName:dataView.metadata.columns[0].displayName | |
}; | |
} | |
public enumerateObjectInstances(options: EnumerateVisualObjectInstancesOptions): VisualObjectInstanceEnumeration { | |
var visualObjects: VisualObjectInstanceEnumerationObject = <VisualObjectInstanceEnumerationObject>VisualSettings.enumerateObjectInstances(this.settings, options); | |
visualObjects.instances[0].validValues = { | |
xAxisFontSize:{numberRange:{min: 10, max:36}}, | |
yAxisFontSize: { numberRange: { min: 10, max: 36 } }, | |
}; | |
return visualObjects | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment