Created
October 11, 2015 09:01
-
-
Save AndyCross/a7ab5e403ac681a0cd93 to your computer and use it in GitHub Desktop.
How the Calendar Visual might look in a few files
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
.day { | |
fill: #fff; | |
stroke: #ccc; | |
} | |
.month { | |
fill: none; | |
stroke-width: 2px; | |
} |
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
declare module D3 { | |
export module Time { | |
export interface Time { | |
weekOfYear(x: any): any;//this is missin from d3.d.ts | |
} | |
} | |
} | |
module powerbi.visuals { | |
export interface DateValue { | |
date: Date; | |
value: number; | |
}; | |
export interface CalendarViewModel { | |
values: DateValue[]; | |
}; | |
export class CalendarVisual implements IVisual { | |
public static capabilities: VisualCapabilities = { | |
dataRoles: [ | |
{ | |
name: 'Category', | |
kind: VisualDataRoleKind.Grouping, | |
}, | |
{ | |
name: 'Y', | |
kind: VisualDataRoleKind.Measure, | |
}, | |
], | |
dataViewMappings: [{ | |
categorical: { | |
categories: { | |
for: { in: 'Category' }, | |
}, | |
values: { | |
for: { in: 'Y' } | |
}, | |
rowCount: { preferred: { max: 2 } } | |
}, | |
}], | |
dataPoint: { | |
displayName: data.createDisplayNameGetter('Visual_DataPoint'), | |
properties: { | |
fill: { | |
displayName: data.createDisplayNameGetter('Visual_Fill'), | |
type: { fill: { solid: { color: true } } } | |
}, | |
} | |
}, | |
labels: { | |
displayName: data.createDisplayNameGetter('Visual_DataPointsLabels'), | |
properties: { | |
show: { | |
displayName: data.createDisplayNameGetter('Visual_Show'), | |
type: { bool: true } | |
}, | |
color: { | |
displayName: data.createDisplayNameGetter('Visual_LabelsFill'), | |
type: { fill: { solid: { color: true } } } | |
}, | |
labelDisplayUnits: { | |
displayName: data.createDisplayNameGetter('Visual_DisplayUnits'), | |
type: { formatting: { labelDisplayUnits: true } } | |
} | |
} | |
} | |
}; | |
private drawMonthPath = false; | |
private drawLegend = false; | |
private drawLabels = true; | |
private width = 1016; | |
private height = 144; | |
private cellSize = 18; // cell size | |
private element: HTMLElement; | |
private rect: D3.Selection; | |
constructor(cellSizeOpt?: number) | |
{ | |
if (cellSizeOpt) { | |
this.cellSize = cellSizeOpt; | |
} | |
} | |
public init(options: VisualInitOptions) { | |
this.element = options.element.get(0); | |
} | |
public update(options: VisualUpdateOptions) { | |
d3.select(this.element).selectAll("*").remove(); | |
var viewModel = this.convert(options.dataViews[0]); | |
if (viewModel == null) return; | |
var maxDomain = Math.max.apply(Math, | |
viewModel.values.map((v) => { | |
return v.value; | |
}) | |
); | |
this.draw(this.element, options.viewport.width, options.viewport.height, this.getYears(viewModel), maxDomain); | |
this.apply(viewModel, maxDomain); | |
} | |
private draw(element, itemWidth: number, itemHeight: number, range: number[], maxDomain: number) | |
{ | |
var format = d3.time.format("%Y-%m-%d"); | |
var svg = d3.select(element).selectAll("svg") | |
.data(range) | |
.enter().append("svg") | |
.attr("width", itemWidth) | |
.attr("height", itemWidth / 7) | |
.attr("viewBox", "0 0 " + this.width + " " + this.height) | |
.append("g") | |
.attr("transform", "translate(" + ((this.width - this.cellSize * 52) / 2) + "," + (this.height - this.cellSize * 7 - 1) + ")"); | |
if (this.drawLabels) { | |
var textGroup = svg.append("g").attr("fill", "#cccccc"); | |
textGroup.append("text") | |
.attr("transform", "translate(" + this.cellSize * -1.5 + "," + this.cellSize * 3.5 + ")rotate(-90)") | |
.style("text-anchor", "middle") | |
.text(function (d) { return d; }); | |
textGroup.append("text") | |
.style("text-anchor", "middle") | |
.text("M") | |
.attr("transform", "translate(" + this.cellSize * -0.75 + ")") | |
.attr("x", 0) | |
.attr("y", 2 * this.cellSize); | |
textGroup.append("text") | |
.style("text-anchor", "middle") | |
.text("W") | |
.attr("transform", "translate(" + this.cellSize * -0.75 + ")") | |
.attr("x", 0) | |
.attr("y", 4 * this.cellSize); | |
textGroup.append("text") | |
.style("text-anchor", "middle") | |
.text("F") | |
.attr("transform", "translate(" + this.cellSize * -0.75 + ")") | |
.attr("x", 0) | |
.attr("y", 6 * this.cellSize); | |
textGroup.append("text") | |
.attr("transform", "translate(" + (this.width - (3 * this.cellSize)) + "," + this.cellSize * 3.5 + ")rotate(90)") | |
.style("text-anchor", "middle") | |
.text(function (d) { return d; }); | |
textGroup.selectAll(".month") | |
.data((d) => { return d3.time.months(new Date(d, 0, 1), new Date(d + 1, 0, 1)); }) | |
.enter() | |
.append("text") | |
.attr("transform", (d) => { return "translate(" + d3.time.weekOfYear(d) * this.cellSize + ", -5)"; }) | |
.text((d) => { return d3.time.format("%b")(d); }); | |
} | |
this.rect = svg.selectAll(".day") | |
.data(this.getDaysOfYear) | |
.enter().append("rect") | |
.attr("width", this.cellSize) | |
.attr("height", this.cellSize) | |
.attr("class", "day") | |
.attr("style", "fill: #eeeeee; stroke-width: 2px; stroke: #ffffff") | |
.attr("x", this.getXPosition) | |
.attr("y", this.getYPosition) | |
.datum(format); | |
this.rect.append("title") | |
.text(function (d) { return d; }); | |
if (this.drawMonthPath) { | |
svg.selectAll(".month") | |
.data(function (d) { return d3.time.months(new Date(d, 0, 1), new Date(d + 1, 0, 1)); }) | |
.enter().append("path") | |
.attr("class", "month") | |
.attr("d", this.monthPath) | |
.attr("stroke", "#cccccc"); | |
} | |
if (this.drawLegend) { | |
var legendGroup = d3.select(this.element).insert("svg", ":first-child") | |
.attr("width", itemWidth) | |
.attr("height", itemWidth / 17.5) | |
.attr("viewBox", "0 0 " + this.width + " " + this.height / 7) | |
.attr("preserveAspectRatio", "xMinYMin") | |
.append("g"); | |
legendGroup.append("rect") | |
.attr("width", this.cellSize) | |
.attr("height", this.cellSize) | |
.attr("x", 0).attr("y", 0) | |
.attr("fill", "#000000"); | |
legendGroup.append("rect") | |
.attr("width", this.cellSize) | |
.attr("height", this.cellSize) | |
.attr("x", 0).attr("y", this.cellSize * 1.5) | |
.attr("fill", "#00ff00"); | |
legendGroup | |
.append("text").text(0) | |
.attr("x", this.cellSize * 2).attr("y", this.cellSize); | |
legendGroup | |
.append("text").text(d3.format(".4r")(maxDomain)) | |
.attr("x", this.cellSize * 2).attr("y", this.cellSize * 2.5); | |
} | |
} | |
private apply(viewModel: CalendarViewModel, maxDomain: number) | |
{ | |
var pad = (n: any) => { | |
if (n.toString().length === 1) { | |
return "0" + n; | |
} | |
return n.toString(); | |
}; | |
var quantizeColor = | |
d3.scale.quantize() | |
.domain([0, maxDomain]) | |
.range(d3.range(256).map(function (d) { return "#00" + pad(d.toString(16)) + "00"; })); | |
var data = d3.nest() | |
.key(function (d: DateValue) { return d.date.getFullYear() + "-" + pad(d.date.getMonth()) + "-" + pad(d.date.getDate()); }) | |
.rollup(function (d: DateValue[]) { return d.map((dateValue) => { return dateValue.value; }).reduce((prev, curr) => prev + curr); }) | |
.map(viewModel.values); | |
this.rect.filter(function (d) { return d in data; }) | |
.attr("style", function (d) { return "fill:" + quantizeColor(data[d]); }) | |
.select("title") | |
.text(function (d) { return d + ": " + d3.format(".6f")(data[d]); }); | |
} | |
private convert(dataView: DataView): CalendarViewModel { | |
if (dataView.categorical.categories == null) { | |
window.console.log("no categoricals"); return; | |
} | |
var returnSet = dataView.categorical.categories[0].values.map( | |
(v, i) => { | |
return <DateValue> { | |
date: v, | |
value: dataView.categorical.values.map((val) => { return val.values[i]; }) | |
.reduce((prev, curr) => { return prev + curr; }) | |
}; | |
}); | |
return <CalendarViewModel> { | |
values: returnSet | |
}; | |
} | |
public getYears(viewModel: CalendarViewModel) { | |
var allYears = viewModel.values.map((value) => { | |
if (value.date == null || isNaN(Date.parse(value.date.toString()))) | |
{ | |
return 1900; | |
}; | |
return value.date.getFullYear(); | |
}); | |
var uniqueYears = {}, a = []; | |
for (var i = 0, l = allYears.length; i < l; ++i) { | |
if (uniqueYears.hasOwnProperty(allYears[i].toString())) { | |
continue; | |
} | |
a.push(allYears[i]); | |
uniqueYears[allYears[i].toString()] = 1; | |
} | |
return a.sort(); | |
} | |
private getDaysOfYear = (year: number) => { return d3.time.days(new Date(year, 0, 1), new Date(year + 1, 0, 1)); }; | |
public getXPosition = (date: Date) => { return d3.time.weekOfYear(date) * this.cellSize; }; | |
public getYPosition = (date: Date) => { return date.getDay() * this.cellSize; }; | |
private monthPath = (t0) => { | |
var t1 = new Date(t0.getFullYear(), t0.getMonth() + 1, 0), d0 = t0.getDay(), w0 = d3.time.weekOfYear(t0), d1 = t1.getDay(), w1 = d3.time.weekOfYear(t1); | |
return "M" + (w0 + 1) * this.cellSize + "," + d0 * this.cellSize + "H" + w0 * this.cellSize + "V" + 7 * this.cellSize + "H" + w1 * this.cellSize + "V" + (d1 + 1) * this.cellSize + "H" + (w1 + 1) * this.cellSize + "V" + 0 + "H" + (w0 + 1) * this.cellSize + "Z"; | |
}; | |
} | |
} | |
module powerbi.visuals.plugins { | |
export var _CalendarVisual: IVisualPlugin = { | |
name: '_CalendarVisual', | |
class: '_CalendarVisual', | |
capabilities: CalendarVisual.capabilities, | |
create: () => new CalendarVisual() | |
}; | |
} | |
Hi @AndyCross, PowerBI is warning that this visual will be depreciated. Can you can update it to the newest API and/or submit it to be certified? Thanks again for the effort so far.
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Hi Andy,
May I ask a question here ?
As I would like to replicate whether your code is functional or not,
I just pasted your code into " https://app.powerbi.com/devTools ".
However, when I press the compile button, this custom visual cannot be created.
Normally, when one press the compile button, the new code will be compiled and saved with a name given.
Since I cannot see the code being saved, I thought probably the compile did not succeed.
Could you give me some advice ?
Thank you very much.
Sorry about I am still new here (still reading the materials in " https://github.com/Microsoft/PowerBI-visuals.)
HuangKY