Last active
October 30, 2021 16:43
-
-
Save spatney/4eb52930075a5e5be9af to your computer and use it in GitHub Desktop.
Custom Visual for Power BI: Thermometer
This file contains hidden or 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
module powerbi.visuals { | |
export interface ViewModel { | |
value: number; | |
color?: string; | |
min?: number; | |
max?: number; | |
} | |
export class Thermometer implements IVisual { | |
public static capabilities: VisualCapabilities = { | |
dataRoles: [ | |
{ | |
name: 'Category', | |
kind: powerbi.VisualDataRoleKind.Grouping, | |
displayName: 'Time' | |
}, | |
{ | |
name: 'Y', | |
kind: powerbi.VisualDataRoleKind.Measure, | |
displayName: 'Temperature' | |
}, | |
], | |
dataViewMappings: [{ | |
categorical: { | |
categories: { | |
for: { in: 'Category' }, | |
dataReductionAlgorithm: { bottom: {} } | |
}, | |
values: { | |
select: [{ for: { in: 'Y' } }], | |
dataReductionAlgorithm: { bottom: {} } | |
}, | |
} | |
}], | |
objects: { | |
general: { | |
displayName: 'General', | |
properties: { | |
fill: { | |
type: { fill: { solid: { color: true } } }, | |
displayName: 'Fill' | |
}, | |
max: { | |
type: { numeric: true }, | |
displayName: 'Max' | |
}, | |
min: { | |
type: { numeric: true }, | |
displayName: 'Min' | |
} | |
}, | |
} | |
}, | |
}; | |
public static converter(dataView: DataView, colors: IDataColorPalette): ViewModel { | |
var series = dataView.categorical.values; | |
return { value: series[0].values[series[0].values.length-1]} | |
} | |
private svg: D3.Selection; | |
private backCircle: D3.Selection; | |
private backRect: D3.Selection; | |
private fillCircle: D3.Selection; | |
private fillRect: D3.Selection; | |
private tempMarkings: D3.Selection; | |
private text: D3.Selection; | |
private data: ViewModel; | |
private dataView: DataView; | |
/** This is called once when the visual is initialially created */ | |
public init(options: VisualInitOptions): void { | |
var svg = this.svg = d3.select(options.element.get(0)) | |
.append('svg') | |
.classed('thermometer', true); | |
var mainGroup = svg.append('g'); | |
this.backRect = mainGroup.append('rect'); | |
this.backCircle = mainGroup.append('circle'); | |
this.fillRect = mainGroup.append('rect'); | |
this.fillCircle = mainGroup.append('circle'); | |
this.text = mainGroup.append('text'); | |
this.tempMarkings = svg.append("g") | |
.attr("class", "y axis"); | |
} | |
/** Update is called for data updates, resizes & formatting changes */ | |
public update(options: VisualUpdateOptions) { | |
if(!options.dataViews) return; | |
window.console.log('has data') | |
var dataView = this.dataView = options.dataViews[0]; | |
this.data = Thermometer.converter(options.dataViews[0],null); | |
this.data.max = Thermometer.getValue(dataView,'max',90); | |
this.data.min = Thermometer.getValue(dataView,'min',28); | |
var viewport = options.viewport; | |
var height = viewport.height; | |
var width = viewport.width; | |
var duration = options.suppressAnimations?0: 1000; | |
this.svg.attr({ 'height': height, 'width': width }); | |
this.draw(width, height, duration); | |
} | |
public draw(width: number, height: number, duration: number) { | |
var radius = height * 0.1; | |
var padding = radius * 0.25; | |
this.drawBack(width, height, radius); | |
this.drawFill(width, height, radius, padding, duration); | |
this.drawTicks(width,height,radius, padding); | |
this.drawText(width, height, radius, padding); | |
} | |
public drawBack(width: number, height: number, radius: number){ | |
var rectHeight = height - radius; | |
var fill = 'D3C8B4'; | |
this.backCircle | |
.attr({ | |
'cx': width / 2, | |
'cy': rectHeight, | |
'r': radius | |
}) | |
.style({ | |
'fill': fill | |
}); | |
this.backRect | |
.attr({ | |
'x': (width - radius) / 2, | |
'y': 0, | |
'width': radius, | |
'height': rectHeight | |
}) | |
.style({ | |
'fill': fill | |
}) | |
} | |
public drawFill(width: number, height: number, radius: number, padding: number, duration: number) { | |
var innerRadius = radius * 0.8; | |
var fillWidth = innerRadius * 0.7; | |
var ZeroValue = height - (radius * 2) - padding; | |
var fill = Thermometer.getFill(this.dataView).solid.color; | |
var min = this.data.min; | |
var max = this.data.max; | |
var value = this.data.value > max ? max : this.data.value; | |
var percentage = (ZeroValue - padding) * ((value - min)/(max-min)) | |
var rectHeight = height - radius; | |
this.fillCircle.attr({ | |
'cx': width / 2, | |
'cy': rectHeight, | |
'r': innerRadius | |
}).style({ | |
'fill': fill | |
}); | |
this.fillRect | |
.style({ | |
'fill': fill | |
}) | |
.attr({ | |
'x': (width - fillWidth) / 2, | |
'width': fillWidth, | |
}) | |
.transition() | |
.duration(duration) | |
.attr({ | |
'y': ZeroValue - percentage, | |
'height': rectHeight - ZeroValue +percentage | |
}) | |
} | |
private drawTicks(width: number, height: number, radius: number, padding: number){ | |
var y = d3.scale.linear().range([height - (radius * 2) - padding, padding]); | |
var yAxis = d3.svg.axis().scale(y).ticks(4).orient("right"); | |
y.domain([this.data.min, this.data.max]).nice(); | |
this.tempMarkings | |
.attr("transform", "translate(" + ((width + radius) / 2 + (radius * 0.15)) + ",0)") | |
.style({ | |
'font-size':(radius * 0.03) + 'em', | |
'font-family': 'Tahoma', | |
'stroke':'none', | |
'fill': '#333' | |
}) | |
.call(yAxis); | |
this.tempMarkings.selectAll('.axis line, .axis path') | |
.style({'stroke': '#333', 'fill': 'none'}); | |
} | |
private drawText(width: number, height: number, radius: number, padding: number){ | |
this.text | |
.text((this.data.value > this.data.max ? this.data.max : this.data.value)|0) | |
.attr({ 'x': width / 2, y: height - radius, 'dy': '.35em' }) | |
.style({ | |
'fill': 'white', | |
'text-anchor': 'middle', | |
'font-family': 'impact', | |
'font-size': (radius * 0.055) + 'em' }) | |
} | |
public enumerateObjectInstances(options: EnumerateVisualObjectInstancesOptions): VisualObjectInstance[] { | |
var instances: VisualObjectInstance[] = []; | |
var dataView = this.dataView; | |
switch (options.objectName) { | |
case 'general': | |
var general: VisualObjectInstance = { | |
objectName: 'general', | |
displayName: 'General', | |
selector: null, | |
properties: { | |
fill: Thermometer.getFill(dataView), | |
max: Thermometer.getValue(dataView,'max',90), | |
min: Thermometer.getValue(dataView,'min',28) | |
} | |
}; | |
instances.push(general); | |
break; | |
} | |
return instances; | |
} | |
private static getFill(dataView: DataView): Fill { | |
if (dataView) { | |
var objects = dataView.metadata.objects; | |
if (objects) { | |
var general = objects['general']; | |
if (general) { | |
var fill = <Fill>general['fill']; | |
if (fill) | |
return fill; | |
} | |
} | |
} | |
return { solid: { color: '#C02942'} }; | |
} | |
private static getValue(dataView: DataView, key: string, defaultValue: number): number { | |
if (dataView) { | |
var objects = dataView.metadata.objects; | |
if (objects) { | |
var general = objects['general']; | |
if (general) { | |
var size = <number>general[key]; | |
if (size != null) | |
return size; | |
} | |
} | |
} | |
return defaultValue; | |
} | |
} | |
} |
Hi,
How can i using this ts file for CUSTOM VISUALS in power bi ?
they only accept .pbiviz file.
Open developer tools from the site's main page or navigate directly to https://app.powerbi.com/devTools.cshtml. Paste the code in, you can now run it or export it into a pbiviz package.
Hi,
How can I implement drill down functionality in our own visuals.
Please help me.
Here we are in 2019... how do you create a pbviz file from this file now?
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Sachin Thanks for this piece of code. This is a simple code that people can use as a reference and develop their own.