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