Created
May 10, 2016 06:47
-
-
Save spatney/a7ec0b42ae0b9132bcbb5be9fc31bae5 to your computer and use it in GitHub Desktop.
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
/* | |
* Power BI Visualizations | |
* | |
* Copyright (c) Microsoft Corporation | |
* All rights reserved. | |
* MIT License | |
* | |
* Permission is hereby granted, free of charge, to any person obtaining a copy | |
* of this software and associated documentation files (the ""Software""), to deal | |
* in the Software without restriction, including without limitation the rights | |
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | |
* copies of the Software, and to permit persons to whom the Software is | |
* furnished to do so, subject to the following conditions: | |
* | |
* The above copyright notice and this permission notice shall be included in | |
* all copies or substantial portions of the Software. | |
* | |
* THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | |
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | |
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | |
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN | |
* THE SOFTWARE. | |
*/ | |
module powerbi.visuals { | |
import ArcDescriptor = D3.Layout.ArcDescriptor; | |
import ClassAndSelector = jsCommon.CssConstants.ClassAndSelector; | |
import createClassAndSelector = jsCommon.CssConstants.createClassAndSelector; | |
import PixelConverter = jsCommon.PixelConverter; | |
import ValueFormatter = powerbi.visuals.valueFormatter; | |
var AsterPlotVisualClassName: string = 'asterPlot'; | |
var AsterPlotLegendObjectName: string = 'legend'; | |
var AsterDefaultOuterLineThickness: number = 1; | |
var AsterDefaultLabelFill: Fill = { solid: { color: '#333' } }; | |
var AsterDefaultLegendFontSize: number = 8; | |
var AsterRadiusRatio: number = 0.9; | |
var AsterConflictRatio = 0.9; | |
var MaxPrecision: number = 17; | |
export interface AsterData { | |
dataPoints: AsterDataPoint[]; | |
highlightedDataPoints?: AsterDataPoint[]; | |
legendData: LegendData; | |
valueFormatter: IValueFormatter; | |
legendSettings: AsterPlotLegendSettings; | |
labelSettings: VisualDataLabelsSettings; | |
showOuterLine: boolean; | |
outerLineThickness: number; | |
} | |
export interface AsterPlotLegendSettings { | |
show: boolean; | |
position: string; | |
showTitle: boolean; | |
labelColor: string; | |
titleText: string; | |
fontSize: number; | |
} | |
export interface AsterArcDescriptor extends ArcDescriptor { | |
isLabelHasConflict?: boolean; | |
} | |
export interface AsterDataPoint extends SelectableDataPoint { | |
color: string; | |
sliceHeight: number; | |
sliceWidth: number; | |
label: string; | |
highlight?: boolean; | |
tooltipInfo: TooltipDataItem[]; | |
} | |
export interface AsterPlotBehaviorOptions { | |
selection: D3.Selection; | |
highlightedSelection: D3.Selection; | |
clearCatcher: D3.Selection; | |
interactivityService: IInteractivityService; | |
} | |
class AsterPlotWebBehavior implements IInteractiveBehavior { | |
private selection: D3.Selection; | |
private highlightedSelection: D3.Selection; | |
private clearCatcher: D3.Selection; | |
private interactivityService: IInteractivityService; | |
public bindEvents(options: AsterPlotBehaviorOptions, selectionHandler: ISelectionHandler) { | |
this.selection = options.selection; | |
this.highlightedSelection = options.highlightedSelection; | |
this.clearCatcher = options.clearCatcher; | |
this.interactivityService = options.interactivityService; | |
this.selection.on('click', (d, i: number) => { | |
selectionHandler.handleSelection(d.data, d3.event.ctrlKey); | |
}); | |
if (this.highlightedSelection) | |
this.highlightedSelection.on('click', (d, i: number) => { | |
selectionHandler.handleSelection(d.data, d3.event.ctrlKey); | |
}); | |
this.clearCatcher.on('click', () => { | |
selectionHandler.handleClearSelection(); | |
}); | |
} | |
public renderSelection(hasSelection: boolean) { | |
var hasHighlights = this.interactivityService.hasSelection(); | |
this.selection.style("fill-opacity", (d) => { | |
return ColumnUtil.getFillOpacity(d.data.selected, d.data.highlight, !d.data.highlight && hasSelection, !d.data.selected && hasHighlights); | |
}); | |
} | |
} | |
export class AsterPlotWarning implements IVisualWarning { | |
private message: string; | |
constructor(message: string) { | |
this.message = message; | |
} | |
public get code(): string { | |
return "AsterPlotWarning"; | |
} | |
public getMessages(resourceProvider: jsCommon.IStringResourceProvider): IVisualErrorMessage { | |
return { | |
message: this.message, | |
title: resourceProvider.get(""), | |
detail: resourceProvider.get("") | |
}; | |
} | |
} | |
export class AsterPlot implements IVisual { | |
public static capabilities: VisualCapabilities = { | |
dataRoles: [ | |
{ | |
displayName: 'Category', | |
name: 'Category', | |
kind: powerbi.VisualDataRoleKind.Grouping, | |
}, | |
{ | |
displayName: 'Y Axis', | |
name: 'Y', | |
kind: powerbi.VisualDataRoleKind.Measure, | |
}, | |
], | |
dataViewMappings: [{ | |
conditions: [ | |
{ 'Category': { max: 1 }, 'Y': { max: 2 } } | |
], | |
categorical: { | |
categories: { | |
for: { in: 'Category' }, | |
dataReductionAlgorithm: { top: {} } | |
}, | |
values: { | |
select: [{ bind: { to: 'Y' } }] | |
}, | |
} | |
}], | |
objects: { | |
general: { | |
displayName: data.createDisplayNameGetter('Visual_General'), | |
properties: { | |
formatString: { | |
type: { formatting: { formatString: true } }, | |
}, | |
}, | |
}, | |
legend: { | |
displayName: 'Legend', | |
description: 'Display legend options', | |
properties: { | |
show: { | |
displayName: 'Show', | |
type: { bool: true } | |
}, | |
position: { | |
displayName: 'Position', | |
description: 'Select the location for the legend', | |
type: { enumeration: legendPosition.type } | |
}, | |
showTitle: { | |
displayName: 'Title', | |
description: 'Display a title for legend symbols', | |
type: { bool: true } | |
}, | |
titleText: { | |
displayName: 'Legend Name', | |
description: 'Title text', | |
type: { text: true }, | |
suppressFormatPainterCopy: true | |
}, | |
labelColor: { | |
displayName: 'Color', | |
type: { fill: { solid: { color: true } } } | |
}, | |
fontSize: { | |
displayName: 'Text Size', | |
type: { formatting: { fontSize: true } } | |
} | |
} | |
}, | |
label: { | |
displayName: 'Center Label', | |
properties: { | |
fill: { | |
displayName: 'Fill', | |
type: { fill: { solid: { color: true } } } | |
} | |
} | |
}, | |
labels: { | |
displayName: 'Detail Labels', | |
properties: { | |
show: { | |
type: { bool: true } | |
}, | |
color: { | |
displayName: 'Color', | |
type: { fill: { solid: { color: true } } } | |
}, | |
labelDisplayUnits: { | |
displayName: 'Display Units', | |
type: { formatting: { labelDisplayUnits: true } }, | |
}, | |
labelPrecision: { | |
displayName: 'Decimal Places', | |
placeHolderText: 'Auto', | |
type: { numeric: true }, | |
}, | |
fontSize: { | |
displayName: 'Text Size', | |
type: { formatting: { fontSize: true } }, | |
}, | |
}, | |
}, | |
outerLine: { | |
displayName: 'Outer line', | |
properties: { | |
show: { | |
displayName: 'Show', | |
type: { bool: true } | |
}, | |
thickness: { | |
displayName: 'Thickness', | |
type: { numeric: true } | |
} | |
} | |
} | |
}, | |
supportsHighlight: true, | |
}; | |
private static Properties: any = { | |
general: { | |
formatString: <DataViewObjectPropertyIdentifier>{ objectName: 'general', propertyName: 'formatString' }, | |
}, | |
dataPoint: { | |
fill: <DataViewObjectPropertyIdentifier>{ objectName: 'dataPoint', propertyName: 'fill' }, | |
}, | |
legend: { | |
show: <DataViewObjectPropertyIdentifier>{ objectName: AsterPlotLegendObjectName, propertyName: 'show' }, | |
position: <DataViewObjectPropertyIdentifier>{ objectName: AsterPlotLegendObjectName, propertyName: 'position' }, | |
showTitle: <DataViewObjectPropertyIdentifier>{ objectName: AsterPlotLegendObjectName, propertyName: 'showTitle' }, | |
titleText: <DataViewObjectPropertyIdentifier>{ objectName: AsterPlotLegendObjectName, propertyName: 'titleText' }, | |
labelColor: <DataViewObjectPropertyIdentifier>{ objectName: AsterPlotLegendObjectName, propertyName: 'labelColor' }, | |
fontSize: <DataViewObjectPropertyIdentifier>{ objectName: AsterPlotLegendObjectName, propertyName: 'fontSize' }, | |
}, | |
label: { | |
fill: <DataViewObjectPropertyIdentifier>{ objectName: 'label', propertyName: 'fill' }, | |
}, | |
labels: { | |
show: <DataViewObjectPropertyIdentifier>{ objectName: 'labels', propertyName: 'show' }, | |
color: <DataViewObjectPropertyIdentifier>{ objectName: 'labels', propertyName: 'color' }, | |
labelDisplayUnits: <DataViewObjectPropertyIdentifier>{ objectName: 'labels', propertyName: 'labelDisplayUnits' }, | |
labelPrecision: <DataViewObjectPropertyIdentifier>{ objectName: 'labels', propertyName: 'labelPrecision' }, | |
fontSize: <DataViewObjectPropertyIdentifier>{ objectName: 'labels', propertyName: 'fontSize' }, | |
}, | |
outerLine: { | |
show: <DataViewObjectPropertyIdentifier>{ objectName: 'outerLine', propertyName: 'show' }, | |
thickness: <DataViewObjectPropertyIdentifier>{ objectName: 'outerLine', propertyName: 'thickness' }, | |
} | |
}; | |
private static AsterSlice: ClassAndSelector = createClassAndSelector('asterSlice'); | |
private static AsterHighlightedSlice: ClassAndSelector = createClassAndSelector('asterHighlightedSlice'); | |
private static OuterLine: ClassAndSelector = createClassAndSelector('outerLine'); | |
private static labelGraphicsContextClass: ClassAndSelector = createClassAndSelector('labels'); | |
private static linesGraphicsContextClass: ClassAndSelector = createClassAndSelector('lines'); | |
private static CenterLabelClass: ClassAndSelector = createClassAndSelector('centerLabel'); | |
private static CenterTextFontHeightCoefficient = 0.4; | |
private static CenterTextFontWidthCoefficient = 1.9; | |
private margin: IMargin = { | |
top: 10, | |
right: 10, | |
bottom: 15, | |
left: 10 | |
}; | |
private svg: D3.Selection; | |
private mainGroupElement: D3.Selection; | |
private mainLabelsElement: D3.Selection; | |
private centerText: D3.Selection; | |
private clearCatcher: D3.Selection; | |
private colors: IDataColorPalette; | |
private dataView: DataView; | |
private hostService: IVisualHostServices; | |
private interactivityService: IInteractivityService; | |
private legend: ILegend; | |
private data: AsterData; | |
private currentViewport: IViewport; | |
private behavior: IInteractiveBehavior; | |
private hasHighlights: boolean; | |
private getDefaultAsterData(): AsterData { | |
return <AsterData>{ | |
dataPoints: [], | |
highlightedDataPoints: [], | |
legendData: <LegendData>{ | |
dataPoints: [], | |
title: null, | |
fontSize: AsterDefaultLegendFontSize, | |
labelColor: LegendData.DefaultLegendLabelFillColor | |
}, | |
legendSettings: { | |
show: false, | |
position: 'Top', | |
showTitle: true, | |
labelColor: LegendData.DefaultLegendLabelFillColor, | |
titleText: '', | |
fontSize: AsterDefaultLegendFontSize, | |
}, | |
valueFormatter: null, | |
labelSettings: { | |
show: false, | |
displayUnits: 0, | |
precision: dataLabelUtils.defaultLabelPrecision, | |
labelColor: dataLabelUtils.defaultLabelColor, | |
fontSize: dataLabelUtils.DefaultFontSizeInPt, | |
}, | |
showOuterLine: false, | |
outerLineThickness: AsterDefaultOuterLineThickness, | |
}; | |
} | |
public converter(dataView: DataView, colors: IDataColorPalette): AsterData { | |
var asterDataResult: AsterData = this.getDefaultAsterData(); | |
if (!this.dataViewContainsCategory(dataView) || dataView.categorical.categories.length !== 1) | |
return asterDataResult; | |
var catDv: DataViewCategorical = dataView.categorical; | |
var cat = catDv.categories[0]; | |
var catSource = cat.source; | |
var catValues = cat.values; | |
var values = catDv.values; | |
var catObjects: DataViewObjects[] = cat.objects; | |
var colorHelper: ColorHelper = new ColorHelper(colors, AsterPlot.Properties.dataPoint.fill); | |
var hasHighlights: boolean = this.hasHighlights = !!(values && values.length > 0 && values[0].highlights); | |
if (dataView.metadata || dataView.metadata.objects) { | |
var objects: DataViewObjects = dataView.metadata.objects; | |
asterDataResult.labelSettings = this.getLabelSettings(objects, asterDataResult.labelSettings); | |
this.updateLegendSettings(objects, catSource, asterDataResult.legendSettings); | |
asterDataResult.showOuterLine = DataViewObjects.getValue<boolean>(objects, AsterPlot.Properties.outerLine.show, asterDataResult.showOuterLine); | |
asterDataResult.outerLineThickness = DataViewObjects.getValue<number>(objects, AsterPlot.Properties.outerLine.thickness, AsterDefaultOuterLineThickness); | |
} | |
var labelSettings: VisualDataLabelsSettings = asterDataResult.labelSettings; | |
if (!catValues || catValues.length < 1 || !values || values.length < 1) | |
return asterDataResult; | |
var formatStringProp = AsterPlot.Properties.general.formatString; | |
var maxValue: number = Math.max(d3.min(values[0].values)); | |
var minValue: number = Math.min(0, d3.min(values[0].values)); | |
var labelFormatter: IValueFormatter = ValueFormatter.create({ | |
format: ValueFormatter.getFormatString(values[0].source, formatStringProp), | |
precision: labelSettings.precision, | |
value: (labelSettings.displayUnits === 0) && (maxValue != null) ? maxValue : labelSettings.displayUnits, | |
}); | |
var categorySourceFormatString = valueFormatter.getFormatString(catSource, formatStringProp); | |
var fontSizeInPx: string = PixelConverter.fromPoint(labelSettings.fontSize); | |
for (var i = 0; i < catValues.length; i++) { | |
var formattedCategoryValue = valueFormatter.format(catValues[i], categorySourceFormatString); | |
var currentValue = values[0].values[i]; | |
var tooltipInfo: TooltipDataItem[] = TooltipBuilder.createTooltipInfo( | |
formatStringProp, | |
catDv, | |
formattedCategoryValue, | |
currentValue, | |
null, | |
null, | |
0); | |
if (values.length > 1) { | |
var toolTip: TooltipDataItem = TooltipBuilder.createTooltipInfo( | |
formatStringProp, | |
catDv, | |
formattedCategoryValue, | |
values[1].values[i], | |
null, | |
null, | |
1)[1]; | |
if (toolTip) | |
tooltipInfo.push(toolTip); | |
currentValue += values[1].values[i]; | |
} | |
var identity: DataViewScopeIdentity = cat.identity[i]; | |
var color: string = colorHelper.getColorForMeasure(catObjects && catObjects[i], identity.key); | |
var selector: SelectionId = SelectionId.createWithId(identity); | |
var sliceWidth: number = Math.max(0, values.length > 1 ? values[1].values[i] : 1); | |
asterDataResult.dataPoints.push({ | |
sliceHeight: values[0].values[i] - minValue, | |
sliceWidth: sliceWidth, | |
label: labelFormatter.format(currentValue), | |
color: color, | |
identity: selector, | |
selected: false, | |
tooltipInfo: tooltipInfo, | |
labelFontSize: fontSizeInPx, | |
highlight: false, | |
}); | |
// Handle legend data | |
if (asterDataResult.legendSettings.show) | |
asterDataResult.legendData.dataPoints.push({ | |
label: catValues[i], | |
color: color, | |
icon: LegendIcon.Box, | |
selected: false, | |
identity: selector | |
}); | |
// Handle highlights | |
if (hasHighlights) { | |
var highlightIdentity: SelectionId = SelectionId.createWithHighlight(selector); | |
var notNull: boolean = values[0].highlights[i] != null; | |
currentValue = notNull ? values[0].highlights[i] : 0; | |
tooltipInfo = TooltipBuilder.createTooltipInfo( | |
formatStringProp, | |
catDv, | |
formattedCategoryValue, | |
currentValue, | |
null, | |
null, | |
0); | |
if (values.length > 1) { | |
var toolTip: TooltipDataItem = TooltipBuilder.createTooltipInfo( | |
formatStringProp, | |
catDv, | |
formattedCategoryValue, | |
values[1].highlights[i], | |
null, | |
null, | |
1)[1]; | |
if (toolTip) | |
tooltipInfo.push(toolTip); | |
currentValue += values[1].highlights[i] !== null ? values[1].highlights[i] : 0; | |
} | |
asterDataResult.highlightedDataPoints.push({ | |
sliceHeight: notNull ? values[0].highlights[i] - minValue : null, | |
sliceWidth: Math.max(0, (values.length > 1 && values[1].highlights[i] !== null) ? values[1].highlights[i] : sliceWidth), | |
label: labelFormatter.format(currentValue), | |
color: color, | |
identity: highlightIdentity, | |
selected: false, | |
tooltipInfo: tooltipInfo, | |
labelFontSize: fontSizeInPx, | |
highlight: true, | |
}); | |
} | |
} | |
return asterDataResult; | |
} | |
private dataViewContainsCategory(dataView: DataView) { | |
return dataView && | |
dataView.categorical && | |
dataView.categorical.values && | |
dataView.categorical.categories && | |
dataView.categorical.categories[0]; | |
} | |
private getLabelSettings(objects: DataViewObjects, labelSettings: VisualDataLabelsSettings): VisualDataLabelsSettings { | |
var asterPlotLabelsProperties = AsterPlot.Properties; | |
var precision = DataViewObjects.getValue<number>(objects, asterPlotLabelsProperties.labels.labelPrecision, labelSettings.precision); | |
labelSettings.precision = precision === undefined ? precision : Math.min(precision, MaxPrecision); | |
labelSettings.show = DataViewObjects.getValue<boolean>(objects, asterPlotLabelsProperties.labels.show, labelSettings.show); | |
labelSettings.fontSize = DataViewObjects.getValue<number>(objects, asterPlotLabelsProperties.labels.fontSize, labelSettings.fontSize); | |
labelSettings.displayUnits = DataViewObjects.getValue<number>(objects, asterPlotLabelsProperties.labels.labelDisplayUnits, labelSettings.displayUnits); | |
var colorHelper: ColorHelper = new ColorHelper(this.colors, asterPlotLabelsProperties.labels.color, labelSettings.labelColor); | |
labelSettings.labelColor = colorHelper.getColorForMeasure(objects, ""); | |
return labelSettings; | |
} | |
private updateLegendSettings(objects: DataViewObjects, catSource: DataViewMetadataColumn, legendSettings: AsterPlotLegendSettings): void { | |
var legendProperties = AsterPlot.Properties.legend; | |
legendSettings.show = DataViewObjects.getValue<boolean>(objects, legendProperties.show, legendSettings.show); | |
legendSettings.position = DataViewObjects.getValue<string>(objects, legendProperties.position, legendSettings.position); | |
legendSettings.showTitle = DataViewObjects.getValue<boolean>(objects, legendProperties.showTitle, legendSettings.showTitle); | |
var titleText = DataViewObjects.getValue<string>(objects, legendProperties.titleText, ''); | |
legendSettings.titleText = _.isEmpty(titleText) && catSource ? catSource.displayName : titleText; | |
legendSettings.labelColor = <string>DataViewObjects.getFillColor(objects, legendProperties.labelColor, legendSettings.labelColor); | |
legendSettings.fontSize = DataViewObjects.getValue<number>(objects, legendProperties.fontSize, legendSettings.fontSize); | |
} | |
public init(options: VisualInitOptions): void { | |
this.hostService = options.host; | |
var element: JQuery = options.element; | |
var svg: D3.Selection = this.svg = d3.select(element.get(0)) | |
.append('svg') | |
.classed(AsterPlotVisualClassName, true) | |
.style('position', 'absolute'); | |
this.colors = options.style.colorPalette.dataColors; | |
this.mainGroupElement = svg.append('g'); | |
this.mainLabelsElement = svg.append('g'); | |
this.behavior = new AsterPlotWebBehavior(); | |
this.clearCatcher = appendClearCatcher(this.mainGroupElement); | |
var interactivity = options.interactivity; | |
this.interactivityService = createInteractivityService(this.hostService); | |
this.legend = createLegend(element, interactivity && interactivity.isInteractiveLegend, this.interactivityService, true); | |
} | |
public update(options: VisualUpdateOptions) { | |
if (!options.dataViews || !options.dataViews[0]) return; // or clear the view, display an error, etc. | |
var duration = options.suppressAnimations ? 0 : AnimatorCommon.MinervaAnimationDuration; | |
this.currentViewport = { | |
height: Math.max(0, options.viewport.height), | |
width: Math.max(0, options.viewport.width) | |
}; | |
var dataView: DataView = this.dataView = options.dataViews[0]; | |
var convertedData: AsterData = this.data = this.converter(dataView, this.colors); | |
if (!convertedData || !convertedData.dataPoints || convertedData.dataPoints.length === 0) { | |
this.clearData(); | |
return; | |
} | |
if (this.interactivityService) { | |
this.interactivityService.applySelectionStateToData(convertedData.dataPoints); | |
this.interactivityService.applySelectionStateToData(convertedData.highlightedDataPoints); | |
} | |
this.renderLegend(convertedData); | |
this.updateViewPortAccordingToLegend(); | |
this.svg | |
.attr({ | |
height: Math.max(0, this.currentViewport.height), | |
width: Math.max(0, this.currentViewport.width) | |
}); | |
var margin: IMargin = this.margin; | |
var transformX: number = (this.currentViewport.width - margin.left) / 2; | |
var transformY: number = (this.currentViewport.height - margin.top) / 2; | |
this.mainGroupElement.attr('transform', SVGUtil.translate(transformX, transformY)); | |
this.mainLabelsElement.attr('transform', SVGUtil.translate(transformX, transformY)); | |
// Move back the clearCatcher | |
this.clearCatcher.attr('transform', SVGUtil.translate(-transformX, -transformY)); | |
// Clear previous data | |
this.mainGroupElement.selectAll(AsterPlot.AsterSlice.selector).remove(); | |
this.mainGroupElement.selectAll(AsterPlot.AsterHighlightedSlice.selector).remove(); | |
dataLabelUtils.cleanDataLabels(this.mainLabelsElement, true); | |
var dataPoints = convertedData.dataPoints; | |
if (!dataPoints || dataPoints.length === 0) | |
return; | |
var selection: D3.UpdateSelection = this.renderArcsAndLabels(dataPoints, duration, convertedData.labelSettings); | |
var highlightedSelection: D3.UpdateSelection; | |
if (this.hasHighlights) | |
highlightedSelection = this.renderArcsAndLabels(convertedData.highlightedDataPoints, duration, convertedData.labelSettings, true); | |
var interactivityService = this.interactivityService; | |
if (interactivityService) { | |
var behaviorOptions: AsterPlotBehaviorOptions = { | |
selection: selection, | |
highlightedSelection: highlightedSelection, | |
clearCatcher: this.clearCatcher, | |
interactivityService: this.interactivityService, | |
}; | |
interactivityService.bind(convertedData.dataPoints.concat(convertedData.highlightedDataPoints), this.behavior, behaviorOptions); | |
} | |
} | |
private renderArcsAndLabels(dataPoints: AsterDataPoint[], duration: number, labelSettings: VisualDataLabelsSettings, isHighlight: boolean = false): D3.UpdateSelection { | |
var margin: IMargin = this.margin; | |
var width: number = this.currentViewport.width - margin.left - margin.right; | |
var height: number = this.currentViewport.height - margin.top - margin.bottom; | |
var radius: number = Math.min(width, height) / 2; | |
var innerRadius: number = 0.3 * (labelSettings.show ? radius * AsterRadiusRatio : radius); | |
var maxScore: number = d3.max(dataPoints, d => d.sliceHeight); | |
var totalWeight: number = d3.sum(dataPoints, d => d.sliceWidth); | |
var hasSelection: boolean = this.interactivityService && this.interactivityService.hasSelection(); | |
var hasHighlights: boolean = this.hasHighlights; | |
var pie: D3.Layout.PieLayout = d3.layout.pie() | |
.sort(null) | |
.value(d => (d && !isNaN(d.sliceWidth) ? d.sliceWidth : 0) / totalWeight); | |
var arc: D3.Svg.Arc = d3.svg.arc() | |
.innerRadius(innerRadius) | |
.outerRadius(d => { | |
var height: number = (radius - innerRadius) * (d && d.data && !isNaN(d.data.sliceHeight) ? d.data.sliceHeight : 1) / maxScore; | |
//The chart should shrink if data labels are on | |
var heightIsLabelsOn = innerRadius + (labelSettings.show ? height * AsterRadiusRatio : height); | |
// Prevent from data to be inside the inner radius | |
return Math.max(heightIsLabelsOn, innerRadius); | |
}); | |
var arcDescriptorDataPoints: ArcDescriptor[] = pie(dataPoints); | |
var classSelector: ClassAndSelector = isHighlight ? AsterPlot.AsterHighlightedSlice : AsterPlot.AsterSlice; | |
var selection = this.mainGroupElement.selectAll(classSelector.selector) | |
.data(arcDescriptorDataPoints, (d, idx) => d.data ? d.data.identity.getKey() : idx); | |
selection.enter() | |
.append('path') | |
.attr('stroke', '#333') | |
.classed(classSelector.class, true); | |
selection | |
.attr('fill', d => d.data.color) | |
.style("fill-opacity", (d) => ColumnUtil.getFillOpacity(d.data.selected, d.data.highlight, hasSelection, hasHighlights)) | |
.transition().duration(duration) | |
.attrTween('d', function (data) { | |
if (!this.oldData) { | |
this.oldData = data; | |
return () => arc(data); | |
} | |
var interpolation = d3.interpolate(this.oldData, data); | |
this.oldData = interpolation(0); | |
return (x) => arc(interpolation(x)); | |
}); | |
selection | |
.exit() | |
.remove(); | |
TooltipManager.addTooltip(selection, (tooltipEvent: TooltipEvent) => tooltipEvent.data.data.tooltipInfo); | |
// Draw data labels only if they are on and there are no highlights or there are highlights and this is the highlighted data labels | |
if (labelSettings.show && (!hasHighlights || (hasHighlights && isHighlight))) { | |
var labelRadCalc = (d: AsterDataPoint) => { | |
var height: number = radius * (d && !isNaN(d.sliceHeight) ? d.sliceHeight : 1) / maxScore + innerRadius; | |
return Math.max(height, innerRadius); | |
}; | |
var labelArc = d3.svg.arc() | |
.innerRadius(d => labelRadCalc(d.data)) | |
.outerRadius(d => labelRadCalc(d.data)); | |
var lineRadCalc = (d: AsterDataPoint) => { | |
var height: number = (radius - innerRadius) * (d && !isNaN(d.sliceHeight) ? d.sliceHeight : 1) / maxScore; | |
height = innerRadius + height * AsterRadiusRatio; | |
return Math.max(height, innerRadius); | |
}; | |
var outlineArc = d3.svg.arc() | |
.innerRadius(d => lineRadCalc(d.data)) | |
.outerRadius(d => lineRadCalc(d.data)); | |
var layout = this.getLabelLayout(labelSettings, labelArc, this.currentViewport); | |
this.drawLabels(arcDescriptorDataPoints, this.mainLabelsElement, layout, this.currentViewport, outlineArc, labelArc); | |
} | |
else | |
dataLabelUtils.cleanDataLabels(this.mainLabelsElement, true); | |
// Draw center text and outline once for original data points | |
if (!isHighlight) { | |
this.drawCenterText(innerRadius); | |
this.drawOuterLine(innerRadius, radius, arcDescriptorDataPoints); | |
} | |
return selection; | |
} | |
private getLabelLayout(labelSettings: VisualDataLabelsSettings, arc: D3.Svg.Arc, viewport: IViewport): ILabelLayout { | |
var midAngle = function (d: ArcDescriptor) { return d.startAngle + (d.endAngle - d.startAngle) / 2; }; | |
var textProperties: TextProperties = { | |
fontFamily: dataLabelUtils.StandardFontFamily, | |
fontSize: PixelConverter.fromPoint(labelSettings.fontSize), | |
text: '', | |
}; | |
var isLabelsHasConflict = function (d: AsterArcDescriptor) { | |
var pos = arc.centroid(d); | |
textProperties.text = d.data.label; | |
var textWidth = TextMeasurementService.measureSvgTextWidth(textProperties); | |
var horizontalSpaceAvaliableForLabels = viewport.width / 2 - Math.abs(pos[0]); | |
var textHeight = TextMeasurementService.estimateSvgTextHeight(textProperties); | |
var verticalSpaceAvaliableForLabels = viewport.height / 2 - Math.abs(pos[1]); | |
d.isLabelHasConflict = textWidth > horizontalSpaceAvaliableForLabels || textHeight > verticalSpaceAvaliableForLabels; | |
return d.isLabelHasConflict; | |
}; | |
return { | |
labelText: (d: AsterArcDescriptor) => { | |
textProperties.text = d.data.label; | |
var pos = arc.centroid(d); | |
var xPos = isLabelsHasConflict(d) ? pos[0] * AsterConflictRatio : pos[0]; | |
var spaceAvaliableForLabels = viewport.width / 2 - Math.abs(xPos); | |
return TextMeasurementService.getTailoredTextOrDefault(textProperties, spaceAvaliableForLabels); | |
}, | |
labelLayout: { | |
x: (d: AsterArcDescriptor) => { | |
var pos = arc.centroid(d); | |
textProperties.text = d.data.label; | |
var xPos = d.isLabelHasConflict ? pos[0] * AsterConflictRatio : pos[0]; | |
return xPos; | |
}, | |
y: (d: AsterArcDescriptor) => { | |
var pos = arc.centroid(d); | |
var yPos = d.isLabelHasConflict ? pos[1] * AsterConflictRatio : pos[1]; | |
return yPos; | |
}, | |
}, | |
filter: (d: AsterArcDescriptor) => (d != null && !_.isEmpty(d.data.label)), | |
style: { | |
'fill': labelSettings.labelColor, | |
'font-size': textProperties.fontSize, | |
'text-anchor': (d: AsterArcDescriptor) => midAngle(d) < Math.PI ? 'start' : 'end', | |
}, | |
}; | |
} | |
private drawLabels(data: ArcDescriptor[], | |
context: D3.Selection, | |
layout: ILabelLayout, | |
viewport: IViewport, | |
outlineArc: D3.Svg.Arc, | |
labelArc: D3.Svg.Arc): void { | |
// Hide and reposition labels that overlap | |
var dataLabelManager = new DataLabelManager(); | |
var filteredData = dataLabelManager.hideCollidedLabels(viewport, data, layout, true /* addTransform */); | |
if (filteredData.length === 0) { | |
dataLabelUtils.cleanDataLabels(context, true); | |
return; | |
} | |
// Draw labels | |
if (context.select(AsterPlot.labelGraphicsContextClass.selector).empty()) | |
context.append('g').classed(AsterPlot.labelGraphicsContextClass.class, true); | |
var labels = context | |
.select(AsterPlot.labelGraphicsContextClass.selector) | |
.selectAll('.data-labels').data(filteredData, (d: ArcDescriptor) => d.data.identity.getKey()); | |
labels.enter().append('text').classed('data-labels', true); | |
if (!labels) | |
return; | |
labels | |
.attr({ x: (d: LabelEnabledDataPoint) => d.labelX, y: (d: LabelEnabledDataPoint) => d.labelY, dy: '.35em' }) | |
.text((d: LabelEnabledDataPoint) => d.labeltext) | |
.style(layout.style); | |
labels | |
.exit() | |
.remove(); | |
// Draw lines | |
if (context.select(AsterPlot.linesGraphicsContextClass.selector).empty()) | |
context.append('g').classed(AsterPlot.linesGraphicsContextClass.class, true); | |
// Remove lines for null and zero values | |
filteredData = _.filter(filteredData, (d: ArcDescriptor) => d.data.sliceHeight !== null && d.data.sliceHeight !== 0); | |
var lines = context.select(AsterPlot.linesGraphicsContextClass.selector).selectAll('polyline') | |
.data(filteredData, (d: ArcDescriptor) => d.data.identity.getKey()); | |
var labelLinePadding = 4; | |
var chartLinePadding = 1.02; | |
var midAngle = function (d: ArcDescriptor) { return d.startAngle + (d.endAngle - d.startAngle) / 2; }; | |
lines.enter() | |
.append('polyline') | |
.classed('line-label', true); | |
lines | |
.attr('points', function (d) { | |
var textPoint = [d.labelX, d.labelY]; | |
textPoint[0] = textPoint[0] + ((midAngle(d) < Math.PI ? -1 : 1) * labelLinePadding); | |
var chartPoint = outlineArc.centroid(d); | |
chartPoint[0] *= chartLinePadding; | |
chartPoint[1] *= chartLinePadding; | |
return [chartPoint, textPoint]; | |
}). | |
style({ | |
'opacity': 0.5, | |
'fill-opacity': 0, | |
'stroke': (d: ArcDescriptor) => this.data.labelSettings.labelColor, | |
}); | |
lines | |
.exit() | |
.remove(); | |
} | |
private renderLegend(asterPlotData: AsterData): void { | |
if (!asterPlotData || !asterPlotData.legendData) | |
return; | |
var legendData: LegendData = asterPlotData.legendData; | |
var objects: DataViewObjects = this.dataView && this.dataView.metadata ? this.dataView.metadata.objects : null; | |
var legendObjectProperties: DataViewObject = DataViewObjects.getObject(objects, AsterPlotLegendObjectName, {}); | |
if (legendObjectProperties) { | |
var legendSettings = asterPlotData.legendSettings; | |
// Force update for title text | |
legendObjectProperties['titleText'] = legendSettings.titleText; | |
LegendData.update(legendData, legendObjectProperties); | |
this.legend.changeOrientation(LegendPosition[legendSettings.position]); | |
} | |
this.legend.drawLegend(legendData, _.clone(this.currentViewport)); | |
Legend.positionChartArea(this.svg, this.legend); | |
} | |
private updateViewPortAccordingToLegend(): void { | |
var legendSettings = this.data.legendSettings; | |
if (!legendSettings || !legendSettings.show) | |
return; | |
var legendMargins: IViewport = this.legend.getMargins(); | |
var legendPosition: LegendPosition = LegendPosition[legendSettings.position]; | |
switch (legendPosition) { | |
case LegendPosition.Top: | |
case LegendPosition.TopCenter: | |
case LegendPosition.Bottom: | |
case LegendPosition.BottomCenter: { | |
this.currentViewport.height -= legendMargins.height; | |
break; | |
} | |
case LegendPosition.Left: | |
case LegendPosition.LeftCenter: | |
case LegendPosition.Right: | |
case LegendPosition.RightCenter: { | |
this.currentViewport.width -= legendMargins.width; | |
break; | |
} | |
default: | |
break; | |
} | |
} | |
private drawOuterLine(innerRadius: number, radius: number, data: ArcDescriptor[]): void { | |
var mainGroup = this.mainGroupElement; | |
var outlineArc = d3.svg.arc() | |
.innerRadius(innerRadius) | |
.outerRadius(radius); | |
if (this.data.showOuterLine) { | |
var OuterThickness: string = this.data.outerLineThickness + 'px'; | |
var outerLine = mainGroup.selectAll(AsterPlot.OuterLine.selector).data(data); | |
outerLine.enter().append('path'); | |
outerLine.attr("fill", "none") | |
.attr({ | |
'stroke': '#333', | |
'stroke-width': OuterThickness, | |
'd': outlineArc | |
}) | |
.style('opacity', 1) | |
.classed(AsterPlot.OuterLine.class, true); | |
outerLine.exit().remove(); | |
} | |
else | |
mainGroup.selectAll(AsterPlot.OuterLine.selector).remove(); | |
} | |
private getCenterText(dataView: DataView): string { | |
if (dataView && dataView.metadata && dataView.metadata.columns && dataView.categorical && dataView.categorical.values) | |
var columns = dataView.metadata.columns | |
for (var i=0; i< columns.length; i++){ | |
if (!columns[i].isMeasure) | |
return columns[i].displayName; | |
} | |
return ''; | |
} | |
private drawCenterText(innerRadius: number): void { | |
var text: string = this.getCenterText(this.dataView); | |
if (_.isEmpty(text)) { | |
this.mainGroupElement.select(AsterPlot.CenterLabelClass.selector).remove(); | |
return; | |
} | |
var centerTextProperties: TextProperties = { | |
fontFamily: dataLabelUtils.StandardFontFamily, | |
fontWeight: 'bold', | |
fontSize: PixelConverter.toString(innerRadius * AsterPlot.CenterTextFontHeightCoefficient), | |
text: text | |
}; | |
if (this.mainGroupElement.select(AsterPlot.CenterLabelClass.selector).empty()) | |
this.centerText = this.mainGroupElement.append('text').classed(AsterPlot.CenterLabelClass.class, true); | |
this.centerText | |
.style({ | |
'line-height': 1, | |
'font-weight': centerTextProperties.fontWeight, | |
'font-size': centerTextProperties.fontSize, | |
'fill': this.getLabelFill(this.dataView).solid.color | |
}) | |
.attr({ | |
'dy': '0.35em', | |
'text-anchor': 'middle' | |
}) | |
.text(TextMeasurementService.getTailoredTextOrDefault(centerTextProperties, innerRadius * AsterPlot.CenterTextFontWidthCoefficient)); | |
} | |
// This extracts fill color of the label from the DataView | |
private getLabelFill(dataView: DataView): Fill { | |
if (this.dataViewContainsObjects(dataView)) | |
return DataViewObjects.getValue(dataView.metadata.objects, AsterPlot.Properties.label.fill, AsterDefaultLabelFill); | |
return AsterDefaultLabelFill; | |
} | |
private dataViewContainsObjects(dataView: DataView) { | |
return dataView && dataView.metadata && dataView.metadata.objects; | |
} | |
private enumerateLegend(instances: VisualObjectInstance[]) { | |
var legendSettings: AsterPlotLegendSettings = this.data.legendSettings; | |
var instance: VisualObjectInstance = { | |
selector: null, | |
objectName: AsterPlotLegendObjectName, | |
displayName: 'Legend', | |
properties: { | |
show: legendSettings.show, | |
position: legendSettings.position, | |
showTitle: legendSettings.showTitle, | |
titleText: legendSettings.titleText, | |
labelColor: legendSettings.labelColor, | |
fontSize: legendSettings.fontSize, | |
} | |
}; | |
instances.push(instance); | |
} | |
private clearData(): void { | |
this.mainGroupElement.selectAll("path").remove(); | |
dataLabelUtils.cleanDataLabels(this.mainLabelsElement, true); | |
this.legend.drawLegend({ dataPoints: [] }, this.currentViewport); | |
} | |
public onClearSelection(): void { | |
if (this.interactivityService) | |
this.interactivityService.clearSelection(); | |
} | |
private enumerateLabels(instances: VisualObjectInstance[]): void { | |
var labelSettings = this.data.labelSettings; | |
var labels: VisualObjectInstance = { | |
objectName: 'labels', | |
displayName: 'Labels', | |
selector: null, | |
properties: { | |
show: labelSettings.show, | |
fontSize: labelSettings.fontSize, | |
labelPrecision: labelSettings.precision, | |
labelDisplayUnits: labelSettings.displayUnits, | |
color: labelSettings.labelColor, | |
} | |
}; | |
instances.push(labels); | |
} | |
// This function retruns the values to be displayed in the property pane for each object. | |
// Usually it is a bind pass of what the property pane gave you, but sometimes you may want to do | |
// validation and return other values/defaults | |
public enumerateObjectInstances(options: EnumerateVisualObjectInstancesOptions): VisualObjectInstance[] { | |
var instances: VisualObjectInstance[] = []; | |
if (!this.dataViewContainsCategory(this.dataView)) | |
return instances; | |
switch (options.objectName) { | |
case AsterPlotLegendObjectName: | |
if (this.data) | |
this.enumerateLegend(instances); | |
break; | |
case 'label': | |
var label: VisualObjectInstance = { | |
objectName: 'label', | |
displayName: 'Label', | |
selector: null, | |
properties: { | |
fill: this.getLabelFill(this.dataView) | |
} | |
}; | |
instances.push(label); | |
break; | |
case 'labels': | |
this.enumerateLabels(instances); | |
break; | |
case 'outerLine': | |
var outerLine: VisualObjectInstance = { | |
objectName: 'outerLine', | |
displayName: 'Outer Line', | |
selector: null, | |
properties: { | |
show: this.data.showOuterLine, | |
thickness: this.data.outerLineThickness, | |
} | |
}; | |
instances.push(outerLine); | |
break; | |
} | |
return instances; | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment