-
-
Save panesofglass/7efb0a6c63c2c6fa2e5697084bb4f7ba 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; | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment