Skip to content

Instantly share code, notes, and snippets.

@noorrocks
Forked from spatney/Thermometer.ts
Created June 2, 2018 17:29
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save noorrocks/3a2b8c0c20cc8db42ef7cf8c648d86a8 to your computer and use it in GitHub Desktop.
Save noorrocks/3a2b8c0c20cc8db42ef7cf8c648d86a8 to your computer and use it in GitHub Desktop.
Custom Visual for Power BI: Thermometer
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