How the Calendar Visual might look in a few files
.day { | |
fill: #fff; | |
stroke: #ccc; | |
} | |
.month { | |
fill: none; | |
stroke-width: 2px; | |
} |
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() | |
}; | |
} |
This comment has been minimized.
This comment has been minimized.
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
This comment has been minimized.
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