Merge pull request #2 from vtkalek/master

LineDotChart after convertion to new API 1.3.0
This commit is contained in:
Avi Sander 2017-01-12 11:22:02 +02:00 коммит произвёл GitHub
Родитель b615b98da6 b9b98c7208
Коммит aeff8b0aab
24 изменённых файлов: 2080 добавлений и 0 удалений

7
.gitignore поставляемый Normal file
Просмотреть файл

@ -0,0 +1,7 @@
node_modules
.DS_Store
.tmp
dist
typings
.api
*.log

Двоичные данные
assets/icon.png Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 457 B

12
assets/icon.svg Normal file
Просмотреть файл

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 15.0.2, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" id="Шар_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
width="20px" height="20px" viewBox="0 0 20 20" enable-background="new 0 0 20 20" xml:space="preserve">
<polygon fill="#808080" points="2,17.984 2,16.18 2,14.767 2,11.584 2,10.199 2,1 1,1 1,18.969 1.047,18.969 1.047,18.984
18.984,18.984 18.984,17.984 "/>
<polygon fill="#4D82B8" points="5.482,8.126 10.537,13.182 18.775,4.9 18.068,4.193 10.516,11.746 5.463,6.693 2,10.199 2,11.584
"/>
<circle r="2" cx="5.482" cy="8.126" fill="#005c55"/>
<circle r="2" cx="10.537" cy="13.182" fill="#005c55"/>
</svg>

После

Ширина:  |  Высота:  |  Размер: 865 B

Двоичные данные
assets/screenshot.png Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 33 KiB

Двоичные данные
assets/thumb.png Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 6.6 KiB

106
capabilities.json Normal file
Просмотреть файл

@ -0,0 +1,106 @@
{
"dataRoles": [
{
"name": "Date",
"kind": "Grouping",
"displayName": "Date"
},
{
"name": "Values",
"kind": "Measure",
"displayName": "Values"
}
],
"dataViewMappings": [
{
"conditions": [
{
"Date": {
"min": 0,
"max": 1
},
"Values": {
"min": 0,
"max": 1
},
"Labels": {
"min": 0,
"max": 1
}
}
],
"categorical": {
"categories": {
"select": [
{ "for": { "in": "Date" } }
],
"dataReductionAlgorithm": { "top": { "count": 10000 } }
},
"values": {
"select": [
{ "bind": { "to": "Values" } }
]
}
}
}
],
"supportsHighlight": true,
"objects": {
"lineoptions": {
"displayName": "Line",
"properties": {
"fill": {
"displayName": "Fill",
"type": { "fill": { "solid": { "color": true } } }
},
"lineThickness": {
"displayName": "Thickness",
"type": { "numeric": true }
}
}
},
"dotoptions": {
"displayName": "Dot",
"properties": {
"color": {
"displayName": "Fill",
"type": { "fill": { "solid": { "color": true } } }
},
"dotSizeMin": {
"displayName": "Min size",
"type": { "numeric": true }
},
"dotSizeMax": {
"displayName": "Max size",
"type": { "numeric": true }
}
}
},
"counteroptions": {
"displayName": "Counter",
"properties": {
"counterTitle": {
"displayName": "Title",
"type": { "text": true }
}
}
},
"misc": {
"displayName": "Animation",
"properties": {
"isAnimated": {
"displayName": "Animated",
"type": { "bool": true }
},
"isStopped": {
"displayName": "Stop on load",
"type": { "bool": true }
},
"duration": {
"displayName": "Time",
"type": { "numeric": true }
}
}
}
}
}

74
karma.conf.js Normal file
Просмотреть файл

@ -0,0 +1,74 @@
/*
* 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.
*/
'use strict';
const recursivePathToTests = 'test/**/*.ts';
module.exports = (config) => {
const browsers = [];
if (process.env.TRAVIS) {
browsers.push('ChromeTravisCI');
} else {
browsers.push('Chrome');
}
config.set({
browsers,
customLaunchers: {
ChromeTravisCI: {
base: 'Chrome',
flags: ['--no-sandbox']
}
},
colors: true,
frameworks: ['jasmine'],
reporters: ['progress'],
singleRun: true,
files: [
'.tmp/drop/visual.css',
'.tmp/drop/visual.js',
'node_modules/powerbi-visuals-utils-testutils/lib/index.js',
'node_modules/jasmine-jquery/lib/jasmine-jquery.js',
recursivePathToTests
],
preprocessors: {
[recursivePathToTests]: ['typescript']
},
typescriptPreprocessor: {
options: {
sourceMap: false,
target: 'ES5',
removeComments: false,
concatenateOutput: false
},
transformPath: (path) => {
return path.replace(/\.ts$/, '.js');
}
}
});
};

47
package.json Normal file
Просмотреть файл

@ -0,0 +1,47 @@
{
"name": "powerbi-visuals-linedotchart",
"description": "LineDot Chart",
"version": "0.3.3",
"author": {
"name": "Microsoft",
"email": "pbicvsupport@microsoft.com"
},
"scripts": {
"postinstall": "typings install && pbiviz update 1.3.0",
"typings": "typings",
"pbiviz": "pbiviz",
"start": "pbiviz start",
"package": "pbiviz package",
"lint": "node node_modules/tslint/bin/tslint \"+(src|test)/**/*.ts\"",
"pretest": "pbiviz package --resources --no-minify --no-pbiviz",
"test": "karma start"
},
"license": "MIT",
"repository": {
"type": "git",
"url": "git+https://github.com/Microsoft/PowerBI-visuals-linedotchart.git"
},
"devDependencies": {
"@types/jasmine": "^2.5.37",
"@types/jasmine-jquery": "^1.5.28",
"@types/lodash": "^4.14.43",
"d3": "3.5.5",
"globalize": "0.1.0-a2",
"jasmine": "^2.5.2",
"jasmine-jquery": "2.1.1",
"jquery": "3.1.1",
"karma": "1.3.0",
"karma-chrome-launcher": "2.0.0",
"karma-jasmine": "1.0.2",
"karma-typescript-preprocessor": "0.3.0",
"lodash": "4.16.2",
"moment": "2.15.1",
"powerbi-visuals-tools": "1.3.0",
"powerbi-visuals-utils-chartutils": "^0.2.1",
"powerbi-visuals-utils-dataviewutils": "^1.0.1",
"powerbi-visuals-utils-testutils": "^0.2.2",
"powerbi-visuals-utils-tooltiputils": "^0.3.0",
"tslint": "3.15.1",
"typings": "1.4.0"
}
}

37
pbiviz.json Normal file
Просмотреть файл

@ -0,0 +1,37 @@
{
"visual": {
"name": "LineDotChart",
"displayName": "LineDot Chart",
"guid": "LineDotChart1460463831201",
"visualClassName": "LineDotChart",
"version": "0.3.3",
"description": "The LineDot chart is an animated line chart with fun animated dots. Use the LineDot chart to engage your audience especially in a presentation context. The bubbles size can be dynamic based on data you provide. A counter is provided that you can use to show a running value as the chart animates. Format options are provided for Lines, Dots, and Animation.",
"supportUrl": "http://community.powerbi.com",
"gitHubUrl": "https://github.com/Microsoft/powerbi-visuals-linedotchart"
},
"apiVersion": "1.3.0",
"author": {
"name": "Microsoft",
"email": "pbicvsupport@microsoft.com"
},
"assets": {
"icon": "assets/icon.png"
},
"externalJS": [
"node_modules/jquery/dist/jquery.min.js",
"node_modules/lodash/lodash.min.js",
"node_modules/d3/d3.js",
"node_modules/powerbi-visuals-utils-typeutils/lib/index.js",
"node_modules/globalize/lib/globalize.js",
"node_modules/globalize/lib/cultures/globalize.culture.en-US.js",
"node_modules/powerbi-visuals-utils-svgutils/lib/index.js",
"node_modules/powerbi-visuals-utils-dataviewutils/lib/index.js",
"node_modules/powerbi-visuals-utils-formattingutils/lib/index.js",
"node_modules/powerbi-visuals-utils-interactivityutils/lib/index.js",
"node_modules/powerbi-visuals-utils-chartutils/lib/index.js",
"node_modules/powerbi-visuals-utils-dataviewutils/lib/index.js",
"node_modules/powerbi-visuals-utils-tooltiputils/lib/index.js"
],
"style": "style/lineDotChart.less",
"capabilities": "capabilities.json"
}

70
src/behavior.ts Normal file
Просмотреть файл

@ -0,0 +1,70 @@
/*
* 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.extensibility.visual {
import converterHelper = powerbi.extensibility.utils.dataview.converterHelper;
import IInteractiveBehavior = powerbi.extensibility.utils.interactivity.IInteractiveBehavior;
import IInteractivityService = powerbi.extensibility.utils.interactivity.IInteractivityService;
import ISelectionHandler = powerbi.extensibility.utils.interactivity.ISelectionHandler;
import SelectableDataPoint = powerbi.extensibility.utils.interactivity.SelectableDataPoint;
export class LineDotChartWebBehavior implements IInteractiveBehavior {
private selection: d3.Selection<any>;
private interactivityService: IInteractivityService;
private hasHighlights: boolean;
public bindEvents(options: LineDotChartBehaviorOptions, selectionHandler: ISelectionHandler): void {
let selection: d3.Selection<any> = this.selection = options.selection;
let clearCatcher: d3.Selection<any> = options.clearCatcher;
this.interactivityService = options.interactivityService;
this.hasHighlights = options.hasHighlights;
selection.on('click', function (d: SelectableDataPoint) {
selectionHandler.handleSelection(d, (d3.event as MouseEvent).ctrlKey);
(d3.event as MouseEvent).stopPropagation();
});
clearCatcher.on('click', function () {
selectionHandler.handleClearSelection();
});
}
public renderSelection(hasSelection: boolean): void {
let hasHighlights: boolean = this.hasHighlights;
this.selection.style("opacity", (d: LineDotPoint) => {
return lineDotChartUtils.getFillOpacity(d.selected, d.highlight, !d.highlight && hasSelection, !d.selected && hasHighlights);
});
}
}
export interface LineDotChartBehaviorOptions {
selection: d3.Selection<any>;
clearCatcher: d3.Selection<any>;
interactivityService: IInteractivityService;
hasHighlights: boolean;
}
}

110
src/columns.ts Normal file
Просмотреть файл

@ -0,0 +1,110 @@
/*
* 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.extensibility.visual {
import DataViewMetadataColumn = powerbi.DataViewMetadataColumn;
import DataViewValueColumns = powerbi.DataViewValueColumns;
import DataViewCategoricalColumn = powerbi.DataViewCategoricalColumn;
import DataViewValueColumn = powerbi.DataViewValueColumn;
import converterHelper = powerbi.extensibility.utils.dataview.converterHelper;
export class LineDotChartColumns<T> {
public static getColumnSources(dataView: DataView) {
return this.getColumnSourcesT<DataViewMetadataColumn>(dataView);
}
public static getTableValues(dataView: DataView) {
let table: DataViewTable = dataView && dataView.table;
let columns: LineDotChartColumns<any[]> = this.getColumnSourcesT<any[]>(dataView);
return columns && table && _.mapValues(
columns, (n: DataViewMetadataColumn, i) => n && table.rows.map(row => row[n.index]));
}
public static getTableRows(dataView: DataView) {
let table: DataViewTable = dataView && dataView.table;
let columns: LineDotChartColumns<any[]> = this.getColumnSourcesT<any[]>(dataView);
return columns && table && table.rows.map(row =>
_.mapValues(columns, (n: DataViewMetadataColumn, i) => n && row[n.index]));
}
public static getCategoricalValues(dataView: DataView) {
let categorical: DataViewCategorical = dataView && dataView.categorical;
let categories: DataViewCategoryColumn[] = categorical && categorical.categories || [];
let values: DataViewValueColumns = categorical && categorical.values || <DataViewValueColumns>[];
let series: any = categorical && values.source && this.getSeriesValues(dataView);
return categorical && _.mapValues(new this<any[]>(), (n, i) =>
(<DataViewCategoricalColumn[]>_.toArray(categories)).concat(_.toArray(values))
.filter(x => x.source.roles && x.source.roles[i]).map(x => x.values)[0]
|| values.source && values.source.roles && values.source.roles[i] && series);
}
public static getSeriesValues(dataView: DataView) {
return dataView && dataView.categorical && dataView.categorical.values
&& dataView.categorical.values.map(x => converterHelper.getSeriesName(x.source));
}
public static getCategoricalColumns(dataView: DataView) {
let categorical: DataViewCategorical = dataView && dataView.categorical;
let categories: DataViewCategoryColumn[] = categorical && categorical.categories || [];
let values: DataViewValueColumns = categorical && categorical.values || <DataViewValueColumns>[];
return categorical && _.mapValues(
new this<DataViewCategoryColumn & DataViewValueColumn[] & DataViewValueColumns>(),
(n, i) => {
let result: any = categories.filter(x => x.source.roles && x.source.roles[i])[0];
if (!result) {
result = values.source && values.source.roles && values.source.roles[i] && values;
}
if (!result) {
result = values.filter(x => x.source.roles && x.source.roles[i]);
if (_.isEmpty(result)) {
result = undefined;
}
}
return result;
});
}
public static getGroupedValueColumns(dataView: DataView) {
let categorical: DataViewCategorical = dataView && dataView.categorical;
let values: DataViewValueColumns = categorical && categorical.values;
let grouped: DataViewValueColumnGroup[] = values && values.grouped();
return grouped && grouped.map(g => _.mapValues(
new this<DataViewValueColumn>(),
(n, i) => g.values.filter(v => v.source.roles[i])[0]));
}
private static getColumnSourcesT<T>(dataView: DataView) {
let columns: DataViewMetadataColumn[] = dataView && dataView.metadata && dataView.metadata.columns;
return columns && _.mapValues(
new this<T>(), (n, i) => columns.filter(x => x.roles && x.roles[i])[0]);
}
public Date: T = null;
public Values: T = null;
}
}

75
src/dataInterfaces.ts Normal file
Просмотреть файл

@ -0,0 +1,75 @@
/*
* 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.extensibility.visual {
import SelectableDataPoint = powerbi.extensibility.utils.interactivity.SelectableDataPoint;
import IInteractivityService = powerbi.extensibility.utils.interactivity.IInteractivityService;
import IAxisProperties = powerbi.extensibility.utils.chart.axis.IAxisProperties;
import IValueFormatter = powerbi.extensibility.utils.formatting.IValueFormatter;
export interface LineDotPoint extends SelectableDataPoint {
time: number | Date;
value: number;
dot: number;
sum: number;
highlight?: boolean;
}
export interface Legend {
text: string;
transform?: string;
dx?: string;
dy?: string;
}
export interface LineDotChartViewModel {
dotPoints: LineDotPoint[];
settings: LineDotChartSettings;
dateMetadataColumn: DataViewMetadataColumn;
valuesMetadataColumn: DataViewMetadataColumn;
dateColumnFormatter: IValueFormatter;
isDateTime: boolean;
minDate: number;
maxDate: number;
minValue: number;
maxValue: number;
sumOfValues: number;
hasHighlights: boolean;
}
export interface MinMaxValue {
min: number;
max: number;
}
export interface LineDotChartDefaultSettingsRange {
dotSize: MinMaxValue;
lineThickness: MinMaxValue;
animationDuration: MinMaxValue;
}
}

57
src/settings.ts Normal file
Просмотреть файл

@ -0,0 +1,57 @@
/*
* 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.extensibility.visual {
import DataViewObjectsParser = powerbi.extensibility.utils.dataview.DataViewObjectsParser;
export class LineDotChartSettings extends DataViewObjectsParser {
public lineoptions: LineSettings = new LineSettings();
public dotoptions: DotSettings = new DotSettings();
public counteroptions: CounterSettings = new CounterSettings();
public misc: MiscSettings = new MiscSettings();
}
export class LineSettings {
public fill: string = "rgb(102, 212, 204)";
public lineThickness: number = 3;
}
export class DotSettings {
public color: string = "#005c55";
public dotSizeMin: number = 4;
public dotSizeMax: number = 38;
}
export class CounterSettings {
public counterTitle: string = "Total features";
}
export class MiscSettings {
public isAnimated: boolean = true;
public isStopped: boolean = true;
public duration: number = 20;
}
}

738
src/visual.ts Normal file
Просмотреть файл

@ -0,0 +1,738 @@
/*
* 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.extensibility.visual {
import ClassAndSelector = powerbi.extensibility.utils.svg.CssConstants.ClassAndSelector;
import createClassAndSelector = powerbi.extensibility.utils.svg.CssConstants.createClassAndSelector;
import DataViewObjectPropertyTypeDescriptor = powerbi.DataViewPropertyValue;
import SelectableDataPoint = powerbi.extensibility.utils.interactivity.SelectableDataPoint;
import IValueFormatter = powerbi.extensibility.utils.formatting.IValueFormatter;
import IInteractivityService = powerbi.extensibility.utils.interactivity.IInteractivityService;
import IMargin = powerbi.extensibility.utils.chart.axis.IMargin;
import IInteractiveBehavior = powerbi.extensibility.utils.interactivity.IInteractiveBehavior;
import ISelectionHandler = powerbi.extensibility.utils.interactivity.ISelectionHandler;
import appendClearCatcher = powerbi.extensibility.utils.interactivity.appendClearCatcher;
import createInteractivityService = powerbi.extensibility.utils.interactivity.createInteractivityService;
import valueFormatter = powerbi.extensibility.utils.formatting.valueFormatter;
import IAxisProperties = powerbi.extensibility.utils.chart.axis.IAxisProperties;
import IVisualHost = powerbi.extensibility.visual.IVisualHost;
import SVGUtil = powerbi.extensibility.utils.svg;
import AxisHelper = powerbi.extensibility.utils.chart.axis;
import TextMeasurementService = powerbi.extensibility.utils.formatting.textMeasurementService;
import IColorPalette = powerbi.extensibility.IColorPalette;
import tooltip = powerbi.extensibility.utils.tooltip;
import TooltipEventArgs = powerbi.extensibility.utils.tooltip.TooltipEventArgs;
import ITooltipServiceWrapper = powerbi.extensibility.utils.tooltip.ITooltipServiceWrapper;
import valueType = utils.type.ValueType;
import DataViewObjectsParser = utils.dataview.DataViewObjectsParser;
export interface LineDotChartDataRoles<T> {
Date?: T;
Values?: T;
}
export class LineDotChart implements IVisual {
private static Identity: ClassAndSelector = createClassAndSelector("lineDotChart");
private static Axes: ClassAndSelector = createClassAndSelector("axes");
private static Axis: ClassAndSelector = createClassAndSelector("axis");
private static Legends: ClassAndSelector = createClassAndSelector("legends");
private static Legend: ClassAndSelector = createClassAndSelector("legend");
private static Line: ClassAndSelector = createClassAndSelector("line");
private static LegendSize: number = 50;
private static AxisSize: number = 30;
private static defaultSettingsRange: LineDotChartDefaultSettingsRange = {
dotSize: {
min: 0,
max: 100
},
lineThickness: {
min: 0,
max: 50,
},
animationDuration: {
min: 0,
max: 1000,
}
};
private data: LineDotChartViewModel;
private root: d3.Selection<any>;
private main: d3.Selection<any>;
private axes: d3.Selection<any>;
private axisX: d3.Selection<any>;
private axisY: d3.Selection<any>;
private axisY2: d3.Selection<any>;
private legends: d3.Selection<any>;
private line: d3.Selection<any>;
private colors: IColorPalette;
private xAxisProperties: IAxisProperties;
private yAxisProperties: IAxisProperties;
private yAxis2Properties: IAxisProperties;
private layout: VisualLayout;
private interactivityService: IInteractivityService;
private behavior: IInteractiveBehavior;
private hostService: IVisualHost;
private get settings(): LineDotChartSettings {
return this.data && this.data.settings;
}
private static viewportMargins = {
top: 10,
right: 30,
bottom: 10,
left: 10
};
private static viewportDimentions = {
width: 150,
height: 150
};
private tooltipServiceWrapper: ITooltipServiceWrapper;
constructor(options: VisualConstructorOptions) {
this.tooltipServiceWrapper = tooltip.createTooltipServiceWrapper(
options.host.tooltipService,
options.element);
this.hostService = options.host;
this.layout = new VisualLayout(null, LineDotChart.viewportMargins);
this.layout.minViewport = <IViewport>LineDotChart.viewportDimentions;
this.interactivityService = createInteractivityService(options.host);
this.behavior = new LineDotChartWebBehavior();
this.root = d3.select(options.element)
.append('svg')
.classed(LineDotChart.Identity.class, true);
this.main = this.root.append('g');
this.axes = this.main.append('g').classed(LineDotChart.Axes.class, true);
this.axisX = this.axes.append('g').classed(LineDotChart.Axis.class, true);
this.axisY = this.axes.append('g').classed(LineDotChart.Axis.class, true);
this.axisY2 = this.axes.append('g').classed(LineDotChart.Axis.class, true);
this.legends = this.main.append('g').classed(LineDotChart.Legends.class, true);
this.line = this.main.append('g').classed(LineDotChart.Line.class, true);
this.colors = options.host.colorPalette;
}
public update(options: VisualUpdateOptions) {
if (!options || !options.dataViews || !options.dataViews[0]) {
return;
}
this.layout.viewport = options.viewport;
let data: LineDotChartViewModel = LineDotChart.converter(options.dataViews[0], this.hostService);
if (!data || _.isEmpty(data.dotPoints)) {
this.clear();
return;
}
this.data = data;
if (this.interactivityService) {
this.interactivityService.applySelectionStateToData(this.data.dotPoints);
}
this.resize();
this.calculateAxes();
this.draw();
}
public destroy() {
this.root = null;
}
public onClearSelection(): void {
if (this.interactivityService) {
this.interactivityService.clearSelection();
}
}
public clear() {
this.settings.misc.isAnimated = false;
this.axes.selectAll(LineDotChart.Axis.selector).selectAll("*").remove();
this.main.selectAll(LineDotChart.Legends.selector).selectAll("*").remove();
this.main.selectAll(LineDotChart.Line.selector).selectAll("*").remove();
this.main.selectAll(LineDotChart.Legend.selector).selectAll("*").remove();
this.line.selectAll(LineDotChart.textSelector).remove();
}
public setIsStopped(isStopped: Boolean): void {
let objects: VisualObjectInstancesToPersist = {
merge: [
<VisualObjectInstance>{
objectName: "misc",
selector: undefined,
properties: {
"isStopped": isStopped,
}
}
]
};
this.hostService.persistProperties(objects);
}
public enumerateObjectInstances(options: EnumerateVisualObjectInstancesOptions): VisualObjectInstanceEnumeration {
return LineDotChartSettings.enumerateObjectInstances(
this.settings || LineDotChartSettings.getDefault(),
options);
}
private static validateDataValue(value: number, defaultValues: MinMaxValue): number {
if (value < defaultValues.min) {
return defaultValues.min;
} else if (value > defaultValues.max) {
return defaultValues.max;
}
return value;
}
private static dateMaxCutter: number = .05;
private static makeSomeSpaceForCounter: number = .10;
private static converter(dataView: DataView, visualHost: IVisualHost): LineDotChartViewModel {
let categorical: LineDotChartColumns<DataViewCategoryColumn & DataViewValueColumn[]> = LineDotChartColumns.getCategoricalColumns(dataView);
if (!categorical
|| !categorical.Date
|| _.isEmpty(categorical.Date.values)
|| !categorical.Values
|| !categorical.Values[0]
|| _.isEmpty(categorical.Values[0].values)) {
return null;
}
let categoryType: valueType = AxisHelper.getCategoryValueType(categorical.Date.source, true);
if (AxisHelper.isOrdinal(categoryType)) {
return null;
}
let isDateTime: boolean = AxisHelper.isDateTime(categoryType);
let categoricalValues: LineDotChartColumns<any[]> = LineDotChartColumns.getCategoricalValues(dataView);
let settings: LineDotChartSettings = this.parseSettings(dataView);
let dateValues: number[] = [],
valueValues: number[] = [];
for (let i = 0, length = categoricalValues.Date.length; i < length; i++) {
if (_.isDate(categoricalValues.Date[i]) || _.isNumber(categoricalValues.Date[i])) {
if (isDateTime) {
dateValues.push((<Date>categoricalValues.Date[i]).getTime());
} else {
dateValues.push(categoricalValues.Date[i]);
}
valueValues.push(categoricalValues.Values[i] || 0);
}
}
let hasHighlights: boolean = !!(categorical.Values.length > 0 && categorical.Values[0].highlights);
let extentDate: [number, number] = d3.extent(dateValues);
let minDate: number = extentDate[0];
let maxDate: number = extentDate[1] + (extentDate[1] - extentDate[0]) * LineDotChart.dateMaxCutter;
let dateColumnFormatter = valueFormatter.create({
format: valueFormatter.getFormatStringByColumn(categorical.Date.source, true) || categorical.Date.source.format
});
let extentValues: [number, number] = d3.extent(valueValues);
let minValue: number = extentValues[0];
let maxValue: number = extentValues[1];
let dotPoints: LineDotPoint[] = [];
let sumOfValues: number = 0;
for (let i: number = 0, length: number = dateValues.length; i < length; i++) {
let value: number = valueValues[i];
let time: number = dateValues[i];
sumOfValues += value;
let selector: ISelectionId = visualHost.createSelectionIdBuilder().withCategory(categorical.Date, i).createSelectionId();
dotPoints.push({
dot: (maxValue - minValue) ? (value - minValue) / (maxValue - minValue) : 0,
value: value,
sum: sumOfValues,
time: time,
selected: false,
identity: selector,
highlight: hasHighlights && !!(categorical.Values[0].highlights[i])
});
}
// make some space for counter + 25%
sumOfValues = sumOfValues + (sumOfValues - minValue) * LineDotChart.makeSomeSpaceForCounter;
return {
dotPoints: dotPoints,
settings: settings,
dateMetadataColumn: categorical.Date.source,
valuesMetadataColumn: categorical.Values[0].source,
dateColumnFormatter: dateColumnFormatter,
isDateTime: isDateTime,
minDate: minDate,
maxDate: maxDate,
minValue: minValue,
maxValue: maxValue,
sumOfValues: sumOfValues,
hasHighlights: hasHighlights,
};
}
private static parseSettings(dataView: DataView): LineDotChartSettings {
let settings: LineDotChartSettings = LineDotChartSettings.parse<LineDotChartSettings>(dataView);
let defaultRange: LineDotChartDefaultSettingsRange = this.defaultSettingsRange;
settings.dotoptions.dotSizeMin = this.validateDataValue(settings.dotoptions.dotSizeMin, defaultRange.dotSize);
settings.dotoptions.dotSizeMax = this.validateDataValue(settings.dotoptions.dotSizeMax, {
min: settings.dotoptions.dotSizeMin,
max: defaultRange.dotSize.max
});
settings.lineoptions.lineThickness = this.validateDataValue(settings.lineoptions.lineThickness, defaultRange.lineThickness);
settings.misc.duration = this.validateDataValue(settings.misc.duration, defaultRange.animationDuration);
return settings;
}
private static outerPadding: number = 0;
private static forcedTickSize: number = 150;
private static xLabelMaxWidth: number = 160;
private static xLabelTickSize: number = 3.2;
private calculateAxes() {
let effectiveWidth: number = Math.max(0, this.layout.viewportIn.width - LineDotChart.LegendSize - LineDotChart.AxisSize);
let effectiveHeight: number = Math.max(0, this.layout.viewportIn.height - LineDotChart.LegendSize);
this.xAxisProperties = AxisHelper.createAxis({
pixelSpan: effectiveWidth,
dataDomain: [this.data.minDate, this.data.maxDate],
metaDataColumn: this.data.dateMetadataColumn,
formatString: null,
outerPadding: LineDotChart.outerPadding,
isCategoryAxis: true,
isScalar: true,
isVertical: false,
forcedTickCount: Math.max(this.layout.viewport.width / LineDotChart.forcedTickSize, 0),
useTickIntervalForDisplayUnits: true,
getValueFn: (index: number, type: valueType) => {
if (this.data.isDateTime) {
return this.data.dateColumnFormatter.format(new Date(index));
} else {
return index;
}
}
});
this.xAxisProperties.xLabelMaxWidth = Math.min(LineDotChart.xLabelMaxWidth, this.layout.viewportIn.width / LineDotChart.xLabelTickSize);
this.xAxisProperties.formatter = this.data.dateColumnFormatter;
this.yAxisProperties = AxisHelper.createAxis({
pixelSpan: effectiveHeight,
dataDomain: [this.data.minValue, this.data.sumOfValues],
metaDataColumn: this.data.valuesMetadataColumn,
formatString: null,
outerPadding: LineDotChart.outerPadding,
isCategoryAxis: false,
isScalar: true,
isVertical: true,
useTickIntervalForDisplayUnits: true
});
this.yAxis2Properties = AxisHelper.createAxis({
pixelSpan: effectiveHeight,
dataDomain: [this.data.minValue, this.data.sumOfValues],
metaDataColumn: this.data.valuesMetadataColumn,
formatString: null,
outerPadding: LineDotChart.outerPadding,
isCategoryAxis: false,
isScalar: true,
isVertical: true,
useTickIntervalForDisplayUnits: true
});
this.yAxis2Properties.axis.orient('right');
}
private static rotateAngle: number = 270;
private generateAxisLabels(): Legend[] {
return [
{
transform: SVGUtil.translate((this.layout.viewportIn.width) / 2, (this.layout.viewportIn.height)),
text: "", // xAxisTitle
dx: "1em",
dy: "-1em"
}, {
transform: SVGUtil.translateAndRotate(0, this.layout.viewportIn.height / 2, 0, 0, LineDotChart.rotateAngle),
text: "", // yAxisTitle
dx: "3em"
}
];
}
private resize(): void {
this.root.attr({
width: this.layout.viewport.width,
height: this.layout.viewport.height
});
this.main.attr('transform', SVGUtil.translate(this.layout.margin.left, this.layout.margin.top));
this.legends.attr('transform', SVGUtil.translate(this.layout.margin.left, this.layout.margin.top));
this.line.attr('transform', SVGUtil.translate(this.layout.margin.left + LineDotChart.LegendSize, 0));
this.axes.attr('transform', SVGUtil.translate(this.layout.margin.left + LineDotChart.LegendSize, 0));
this.axisX.attr('transform', SVGUtil.translate(0, this.layout.viewportIn.height - LineDotChart.LegendSize));
this.axisY2.attr('transform', SVGUtil.translate(this.layout.viewportIn.width - LineDotChart.LegendSize - LineDotChart.AxisSize, 0));
}
private static tickText: string = '.tick text';
private static dotPointsText: string = "g.path, g.dot-points";
private static dotPathText: string = "g.path";
private draw(): void {
this.stopAnimation();
this.renderLegends();
this.drawPlaybackButtons();
this.axisX.call(this.xAxisProperties.axis);
this.axisY.call(this.yAxisProperties.axis);
this.axisY2.call(this.yAxis2Properties.axis);
this.axisX.selectAll(LineDotChart.tickText).call(
AxisHelper.LabelLayoutStrategy.clip,
this.xAxisProperties.xLabelMaxWidth,
TextMeasurementService.svgEllipsis);
if (this.settings.misc.isAnimated && this.settings.misc.isStopped) {
this.main.selectAll(LineDotChart.Line.selector).selectAll(LineDotChart.dotPointsText).remove();
this.line.selectAll(LineDotChart.textSelector).remove();
// this.updateLineText("");
return;
}
let linePathSelection: d3.selection.Update<LineDotPoint[]> = this.line
.selectAll(LineDotChart.dotPathText)
.data([this.data.dotPoints]);
this.drawLine(linePathSelection);
this.drawClipPath(linePathSelection);
linePathSelection
.exit().remove();
this.drawDots();
}
private static lineDotChartPlayBtn: string = "lineDotChart__playBtn";
private static lineDotChartPlayBtnTranslate: string = "lineDotChartPlayBtnTranslate";
private static gLineDotChartPayBtn: string = "g.lineDotChart__playBtn";
private static playBtnGroupDiameter: number = 34;
private static playBtnGroupLineValues: string = "M0 2l10 6-10 6z";
private static playBtnGroupPlayTranslate: string = "playBtnGroupPlayTranslate";
private static playBtnGroupPathTranslate: string = "playBtnGroupPathTranslate";
private static playBtnGroupRectTranslate: string = "playBtnGroupRectTranslate";
private static playBtnGroupRectWidth: string = "2";
private static playBtnGroupRectHeight: string = "12";
private static StopButton: ClassAndSelector = createClassAndSelector("stop");
private drawPlaybackButtons() {
let playBtn: d3.selection.Update<string> = this.line.selectAll(LineDotChart.gLineDotChartPayBtn).data([""]);
let playBtnGroup: d3.Selection<string> = playBtn.enter()
.append("g")
.classed(LineDotChart.lineDotChartPlayBtn, true);
playBtnGroup
.classed(LineDotChart.lineDotChartPlayBtnTranslate, true)
.append("circle")
.attr("r", LineDotChart.playBtnGroupDiameter / 2)
.on('click', () => this.setIsStopped(!this.settings.misc.isStopped));
playBtnGroup.append("path")
.classed("play", true)
.classed(LineDotChart.playBtnGroupPlayTranslate, true)
.attr("d", LineDotChart.playBtnGroupLineValues)
.attr('pointer-events', "none");
playBtnGroup
.append("path")
.classed(LineDotChart.StopButton.class, true)
.classed(LineDotChart.playBtnGroupPathTranslate, true)
.attr("d", LineDotChart.playBtnGroupLineValues)
.attr("transform-origin", "center")
.attr('pointer-events', "none");
playBtnGroup
.append("rect")
.classed(LineDotChart.StopButton.class, true)
.classed(LineDotChart.playBtnGroupRectTranslate, true)
.attr("width", LineDotChart.playBtnGroupRectWidth)
.attr("height", LineDotChart.playBtnGroupRectHeight)
.attr('pointer-events', "none");
playBtn.selectAll("circle").attr("opacity", () => this.settings.misc.isAnimated ? 1 : 0);
playBtn.selectAll(".play").attr("opacity", () => this.settings.misc.isAnimated && this.settings.misc.isStopped ? 1 : 0);
playBtn.selectAll(LineDotChart.StopButton.selector).attr("opacity", () => this.settings.misc.isAnimated && !this.settings.misc.isStopped ? 1 : 0);
playBtn.exit().remove();
}
private static pathClassName: string = "path";
private static pathPlotClassName: string = "path.plot";
private static plotClassName: string = "plot";
private static lineClip: string = "lineClip";
private drawLine(linePathSelection: d3.selection.Update<LineDotPoint[]>) {
linePathSelection.enter().append("g").classed(LineDotChart.pathClassName, true);
let pathPlot: d3.selection.Update<LineDotPoint[]> = linePathSelection.selectAll(LineDotChart.pathPlotClassName).data(d => [d]);
pathPlot.enter()
.append('path')
.classed(LineDotChart.plotClassName, true);
// Draw the line
let drawLine: d3.svg.Line<any> = d3.svg.line()
.x((d: any) => this.xAxisProperties.scale(d.time))
.y((d: any) => this.yAxisProperties.scale(d.sum));
pathPlot
.attr('stroke', () => this.settings.lineoptions.fill)
.attr('stroke-width', this.settings.lineoptions.lineThickness)
.attr('d', drawLine)
.attr("clip-path", "url(" + location.href + '#' + LineDotChart.lineClip + ")");
}
private static zeroX: number = 0;
private static zeroY: number = 0;
private static millisecondsInOneSecond: number = 1000;
private drawClipPath(linePathSelection: d3.selection.Update<any>) {
let clipPath: d3.selection.Update<any> = linePathSelection.selectAll("clipPath").data(d => [d]);
clipPath.enter().append("clipPath")
.attr("id", LineDotChart.lineClip)
.append("rect")
.attr("x", LineDotChart.zeroX)
.attr("y", LineDotChart.zeroY);
let line_left: any = this.xAxisProperties.scale(_.first(this.data.dotPoints).time);
let line_right: any = this.xAxisProperties.scale(_.last(this.data.dotPoints).time);
if (this.settings.misc.isAnimated) {
clipPath
.selectAll("rect")
.attr('x', line_left)
.attr('width', 0)
.interrupt()
.transition()
.ease("linear")
.duration(this.animationDuration * LineDotChart.millisecondsInOneSecond)
.attr('width', line_right - line_left)
.attr("height", this.layout.viewportIn.height);
} else {
clipPath
.selectAll("rect")
.interrupt()
.attr('x', line_left)
.attr('width', line_right - line_left);
}
}
private static pointTime: number = 300;
private static dotPointsClass: string = "dot-points";
private static pointClassName: string = 'point';
private static pointScaleValue: number = 0.005;
private static pointTransformScaleValue: number = 3.4;
private drawDots() {
let point_time: number = this.settings.misc.isAnimated && !this.settings.misc.isStopped ? LineDotChart.pointTime : 0;
let hasHighlights: boolean = this.data.hasHighlights;
let hasSelection: boolean = this.interactivityService && this.interactivityService.hasSelection();
// Draw the individual data points that will be shown on hover with a tooltip
let lineTipSelection: d3.selection.Update<LineDotPoint[]> = this.line.selectAll('g.' + LineDotChart.dotPointsClass)
.data([this.data.dotPoints]);
lineTipSelection.enter()
.append("g")
.classed(LineDotChart.dotPointsClass, true);
let dotsSelection: d3.selection.Update<LineDotPoint> = lineTipSelection
.selectAll("circle." + LineDotChart.pointClassName)
.data(d => d);
dotsSelection.enter()
.append('circle')
.classed(LineDotChart.pointClassName, true)
.on('mouseover.point', this.showDataPoint)
.on('mouseout.point', this.hideDataPoint);
dotsSelection
.attr('fill', this.settings.dotoptions.color)
.style("opacity", (d: LineDotPoint) => {
return lineDotChartUtils.getFillOpacity(d.selected, d.highlight, !d.highlight && hasSelection, !d.selected && hasHighlights);
})
.attr('r', (d: LineDotPoint) =>
this.settings.dotoptions.dotSizeMin + d.dot * (this.settings.dotoptions.dotSizeMax - this.settings.dotoptions.dotSizeMin));
if (this.settings.misc.isAnimated) {
let maxTextLength: number = Math.min(350, this.xAxisProperties.scale.range()[1] - this.xAxisProperties.scale.range()[0] - 60);
let lineTextSelection: d3.Selection<any> = this.line.selectAll(LineDotChart.textSelector);
let lineText: d3.selection.Update<string> = lineTextSelection.data([""]);
lineText
.enter()
.append("text")
.attr('text-anchor', "end")
.classed("text", true);
lineText
.attr('x', this.layout.viewportIn.width - LineDotChart.widthMargin)
.attr('y', LineDotChart.yPosition)
.call(selection => TextMeasurementService.svgEllipsis(<any>selection.node(), maxTextLength));
lineText.exit().remove();
dotsSelection
.interrupt()
.attr('transform', (d: LineDotPoint) =>
SVGUtil.translateAndScale(this.xAxisProperties.scale(d.time), this.yAxisProperties.scale(d.sum), LineDotChart.pointScaleValue))
.transition()
.each("start", (d: LineDotPoint, i: number) => {
let text = this.settings.counteroptions.counterTitle + ' ' + (i + 1);
this.updateLineText(lineText, text);
})
.duration(point_time)
.delay((d: LineDotPoint, i: number) => this.pointDelay(this.data.dotPoints, i, this.animationDuration))
.ease("linear")
.attr('transform', (d: LineDotPoint) =>
SVGUtil.translateAndScale(this.xAxisProperties.scale(d.time), this.yAxisProperties.scale(d.sum), LineDotChart.pointTransformScaleValue))
.transition()
.duration(point_time)
.delay((d: LineDotPoint, i: number) => this.pointDelay(this.data.dotPoints, i, this.animationDuration) + point_time)
.ease("elastic")
.attr('transform', (d: LineDotPoint) =>
SVGUtil.translateAndScale(this.xAxisProperties.scale(d.time), this.yAxisProperties.scale(d.sum), 1));
} else {
dotsSelection
.interrupt()
.attr('transform', (d: LineDotPoint) =>
SVGUtil.translateAndScale(this.xAxisProperties.scale(d.time), this.yAxisProperties.scale(d.sum), 1));
this.line.selectAll(LineDotChart.textSelector).remove();
}
for (let i: number = 0; i < dotsSelection[0].length; i++) {
this.addTooltip(dotsSelection[0][i]);
}
dotsSelection.exit().remove();
lineTipSelection.exit().remove();
if (this.interactivityService) {
// Register interactivity;
let behaviorOptions: LineDotChartBehaviorOptions = {
selection: dotsSelection,
clearCatcher: this.root,
interactivityService: this.interactivityService,
hasHighlights: hasHighlights
};
this.interactivityService.bind(this.data.dotPoints, this.behavior, behaviorOptions);
}
}
private get animationDuration(): number {
if (this.settings && this.settings.misc) {
return this.settings.misc.duration;
}
return 0;
}
private stopAnimation(): void {
this.line.selectAll("*")
.transition()
.duration(0)
.delay(0);
d3.timer.flush();
}
private static textSelector: string = "text.text";
private static widthMargin: number = 85;
private static yPosition: number = 30;
private updateLineText(textSelector: d3.Selection<any>, text?: string): void {
textSelector.text(d => text);
}
private pointDelay(points: LineDotPoint[], num: number, animation_duration: number): number {
if (!points.length || !points[num] || num === 0 || !this.settings.misc.isAnimated || this.settings.misc.isStopped) {
return 0;
}
let time: number = <number>points[num].time;
let min: number = <number>points[0].time;
let max: number = <number>points[points.length - 1].time;
return animation_duration * 1000 * (time - min) / (max - min);
}
private static showClassName: string = 'show';
private showDataPoint(data: LineDotPoint, index: number): void {
d3.select(<any>this).classed(LineDotChart.showClassName, true);
}
private hideDataPoint(data: LineDotPoint, index: number): void {
d3.select(<any>this).classed(LineDotChart.showClassName, false);
}
private addTooltip(element: any): void {
let selection: d3.Selection<any> = d3.select(element);
let data: LineDotPoint = selection.datum();
this.tooltipServiceWrapper.addTooltip(selection, (event) => {
return [
{
displayName: "",
value: this.data.dateColumnFormatter.format(data.time)
},
{
displayName: "",
value: data.value.toString()
}
];
});
}
private renderLegends(): void {
let legends: Legend[] = this.generateAxisLabels();
let legendSelection: d3.selection.Update<any> = this.legends
.selectAll(LineDotChart.Legend.selector)
.data(legends);
legendSelection
.enter()
.append("svg:text");
legendSelection
.attr("x", 0)
.attr("y", 0)
.attr("dx", (item: Legend) => item.dx)
.attr("dy", (item: Legend) => item.dy)
.attr("transform", (item: Legend) => item.transform)
.text((item: Legend) => item.text)
.classed(LineDotChart.Legend.class, true);
legendSelection
.exit()
.remove();
}
}
export module lineDotChartUtils {
export let DimmedOpacity: number = 0.4;
export let DefaultOpacity: number = 1.0;
export function getFillOpacity(selected: boolean, highlight: boolean, hasSelection: boolean, hasPartialHighlights: boolean): number {
if ((hasPartialHighlights && !highlight) || (hasSelection && !selected)) {
return DimmedOpacity;
}
return DefaultOpacity;
}
}
}

137
src/visualLayout.ts Normal file
Просмотреть файл

@ -0,0 +1,137 @@
/*
* 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.extensibility.visual {
// powerbi
import IViewport = powerbi.IViewport;
// powerbi.visuals
import IMargin = powerbi.extensibility.utils.chart.axis.IMargin;
export class VisualLayout {
private marginValue: IMargin;
private viewportValue: IViewport;
private viewportInValue: IViewport;
private minViewportValue: IViewport;
private originalViewportValue: IViewport;
private previousOriginalViewportValue: IViewport;
public defaultMargin: IMargin;
public defaultViewport: IViewport;
constructor(defaultViewport?: IViewport, defaultMargin?: IMargin) {
this.defaultViewport = defaultViewport || { width: 0, height: 0 };
this.defaultMargin = defaultMargin || { top: 0, bottom: 0, right: 0, left: 0 };
}
public get viewport(): IViewport {
return this.viewportValue || (this.viewportValue = this.defaultViewport);
}
public get viewportCopy(): IViewport {
return _.clone(this.viewport);
}
// Returns viewport minus margin
public get viewportIn(): IViewport {
return this.viewportInValue || this.viewport;
}
public get minViewport(): IViewport {
return this.minViewportValue || { width: 0, height: 0 };
}
public get margin(): IMargin {
return this.marginValue || (this.marginValue = this.defaultMargin);
}
public set minViewport(value: IViewport) {
this.setUpdateObject(value, v => this.minViewportValue = v, VisualLayout.restrictToMinMax);
}
public set viewport(value: IViewport) {
this.previousOriginalViewportValue = _.clone(this.originalViewportValue);
this.originalViewportValue = _.clone(value);
this.setUpdateObject(value,
v => this.viewportValue = v,
o => VisualLayout.restrictToMinMax(o, this.minViewport));
}
public set margin(value: IMargin) {
this.setUpdateObject(value, v => this.marginValue = v, VisualLayout.restrictToMinMax);
}
// Returns true if viewport has updated after last change.
public get viewportChanged(): boolean {
return !!this.originalViewportValue && (!this.previousOriginalViewportValue
|| this.previousOriginalViewportValue.height !== this.originalViewportValue.height
|| this.previousOriginalViewportValue.width !== this.originalViewportValue.width);
}
public get viewportInIsZero(): boolean {
return this.viewportIn.width === 0 || this.viewportIn.height === 0;
}
public resetMargin(): void {
this.margin = this.defaultMargin;
}
private update(): void {
this.viewportInValue = VisualLayout.restrictToMinMax({
width: this.viewport.width - (this.margin.left + this.margin.right),
height: this.viewport.height - (this.margin.top + this.margin.bottom)
}, this.minViewportValue);
}
private setUpdateObject<T>(object: T, setObjectFn: (T) => void, beforeUpdateFn?: (T) => void): void {
object = _.clone(object);
setObjectFn(VisualLayout.createNotifyChangedObject(object, o => {
if (beforeUpdateFn) beforeUpdateFn(object);
this.update();
}));
if (beforeUpdateFn) { beforeUpdateFn(object); }
this.update();
}
private static createNotifyChangedObject<T>(object: T, objectChanged: (o?: T, key?: string) => void): T {
let result: T = <any>{};
_.keys(object).forEach(key => Object.defineProperty(result, key, {
get: () => object[key],
set: (value) => { object[key] = value; objectChanged(object, key); },
enumerable: true,
configurable: true
}));
return result;
}
private static restrictToMinMax<T>(value: T, minValue?: T): T {
_.keys(value).forEach(x => value[x] = Math.max(minValue && minValue[x] || 0, value[x]));
return value;
}
}
}

105
style/lineDotChart.less Normal file
Просмотреть файл

@ -0,0 +1,105 @@
/*
* 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.
*/
/**
* Imports external styles.
* We compile it as a less file in order to wrap the external CSS rules.
*/
@import (less) "node_modules/powerbi-visuals-utils-interactivityutils/lib/index.css";
@import (less) "node_modules/powerbi-visuals-utils-formattingutils/lib/index.css";
.lineDotChart {
font-family: helvetica, arial, sans-serif;
}
.lineDotChart .axis path,
.lineDotChart .legends path,
.lineDotChart .axis line,
.lineDotChart .legends line {
fill: none;
stroke: black;
shape-rendering: crispEdges;
}
.lineDotChart .axis text,
.lineDotChart .legends text {
fill: black;
font-size: 14px;
}
.lineDotChart .legends text {
font-size: 16px;
fill: #000;
text-anchor: middle;
}
.lineDotChart .line {
fill: none;
stroke-width: 2px;
}
.lineDotChart .point {
stroke: white;
stroke-opacity: .7;
stroke-width: .5;
pointer-events: all;
transition: opacity 0.2s ease-out;
}
.lineDotChart .text {
fill: black;
font-size: 32px;
}
.lineDotChart__playBtn {
opacity: .23;
cursor: pointer;
transition: opacity .3s;
}
.lineDotChart__playBtn:hover {
opacity: 1;
}
.lineDotChart__playBtn circle {
stroke-width: .5;
stroke: gray;
fill: white;
}
.lineDotChart__playBtn rect {
fill: black;
}
.lineDotChart__playBtn path {
fill: black;
}
.playBtnGroupPathTranslate {
transform:translate(-6px, -8px) rotate(180deg);
}
.lineDotChartPlayBtnTranslate{
transform:translate(40px, 20px);
}
.playBtnGroupPlayTranslate{
transform:translate(-4px, -8px);
}
.playBtnGroupRectTranslate{
transform:translate(-7px, -6px);
}

54
test/_references.ts Normal file
Просмотреть файл

@ -0,0 +1,54 @@
/*
* 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.
*/
// External
/// <reference path="../typings/index.d.ts" />
/// <reference path="../node_modules/@types/jasmine/index.d.ts" />
/// <reference path="../node_modules/@types/jasmine-jquery/index.d.ts" />
// Power BI API
/// <reference path="../.api/v1.3.0/PowerBI-visuals.d.ts" />
// Power BI Extensibility
/// <reference path="../node_modules/powerbi-visuals-utils-dataviewutils/lib/index.d.ts" />
/// <reference path="../node_modules/powerbi-visuals-utils-typeutils/lib/index.d.ts" />
/// <reference path="../node_modules/powerbi-visuals-utils-svgutils/lib/index.d.ts" />
/// <reference path="../node_modules/powerbi-visuals-utils-interactivityutils/lib/index.d.ts" />
/// <reference path="../node_modules/powerbi-visuals-utils-tooltiputils/lib/index.d.ts" />
/// <reference path="../node_modules/powerbi-visuals-utils-dataviewutils/lib/index.d.ts" />
/// <reference path="../node_modules/powerbi-visuals-utils-formattingutils/lib/index.d.ts" />
/// <reference path="../node_modules/powerbi-visuals-utils-chartutils/lib/index.d.ts"/>
/// <reference path="../node_modules/powerbi-visuals-utils-testutils/lib/index.d.ts"/>
// The visual
/// <reference path="../.tmp/drop/visual.d.ts" />
// Test
/// <reference path="helpers.ts" />
/// <reference path="visualData.ts" />
/// <reference path="visualBuilder.ts" />

47
test/helpers.ts Normal file
Просмотреть файл

@ -0,0 +1,47 @@
/*
* 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.
*/
/// <reference path="_references.ts"/>
namespace powerbitests.customVisuals {
import helpers = powerbi.extensibility.utils.test.helpers;
export function getHexColorFromNumber(value: number) {
let hex: string = value.toString(16).toUpperCase();
return "#" + (hex.length === 6 ? hex : _.range(0, 6 - hex.length, 0).join("") + hex);
}
export function getRandomInteger(min: number, max: number, exceptionList?: number[]): number {
return helpers.getRandomNumber(max, min, exceptionList, Math.floor);
}
export function getRandomHexColor(): string {
return getHexColorFromNumber(getRandomInteger(0, 16777215 + 1));
}
export function getRandomHexColors(count: number): string[] {
return _.range(count).map(x => getRandomHexColor());
}
}

79
test/visualBuilder.ts Normal file
Просмотреть файл

@ -0,0 +1,79 @@
/*
* 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.
*/
/// <reference path="_references.ts"/>
module powerbi.extensibility.visual.test {
import VisualBuilderBase = powerbi.extensibility.utils.test.VisualBuilderBase;
import getRandomNumber = powerbi.extensibility.utils.test.helpers.getRandomNumber;
// LineDotChart1460463831201
import VisualPlugin = powerbi.visuals.plugins.LineDotChart1460463831201;
import VisualClass = powerbi.extensibility.visual.LineDotChart1460463831201.LineDotChart;
import VisualSettings = powerbi.extensibility.visual.LineDotChart1460463831201.LineDotChartSettings;
export class LineDotChartBuilder extends VisualBuilderBase<VisualClass> {
constructor(width: number, height: number) {
super(width, height);
}
protected build(options: VisualConstructorOptions) {
return new VisualClass(options);
}
public get mainElement(): JQuery {
return this.element.children(".lineDotChart");
}
public get line() {
return this.mainElement
.children("g")
.children("g.line");
}
public get linePath() {
return this.line
.children("g.path")
.children("path.plot");
}
public get dots() {
return this.line
.children("g.dot-points")
.children("circle.point");
}
public get animationPlay(): JQuery {
return this.mainElement
.find("g.lineDotChart__playBtn");
}
public get counterTitle(): JQuery {
return this.line
.children("text.text");
}
}
}

88
test/visualData.ts Normal file
Просмотреть файл

@ -0,0 +1,88 @@
/*
* 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.
*/
/// <reference path="_references.ts" />
module powerbi.extensibility.visual.test {
// powerbi.extensibility.utils.type
import ValueType = powerbi.extensibility.utils.type.ValueType;
// powerbi.extensibility.utils.test
import getRandomNumber = powerbi.extensibility.utils.test.helpers.getRandomNumber;
import CustomizeColumnFn = powerbi.extensibility.utils.test.dataViewBuilder.CustomizeColumnFn;
import TestDataViewBuilder = powerbi.extensibility.utils.test.dataViewBuilder.TestDataViewBuilder;
import helpers = powerbi.extensibility.utils.test.helpers;
export function getRandomUniqueNumbers(count: number, min: number = 0, max: number = 1): number[] {
let result: number[] = [];
for (let i = 0; i < count; i++) {
result.push(getRandomNumber(min, max, result));
}
return result;
}
export function getRandomUniqueDates(count: number, start: Date, end: Date): Date[] {
return getRandomUniqueNumbers(count, start.getTime(), end.getTime()).map(x => new Date(x));
}
export function getRandomUniqueSortedDates(count: number, start: Date, end: Date): Date[] {
return getRandomUniqueDates(count, start, end).sort((a, b) => a.getTime() - b.getTime());
}
export class LineDotChartData extends TestDataViewBuilder {
public static ColumnDate: string = "Date";
public static ColumnValue: string = "Value";
public valuesDate: Date[] = getRandomUniqueSortedDates(
50,
new Date(2014, 9, 12, 3, 9, 50),
new Date(2016, 3, 1, 2, 43, 3));
public valuesValue = helpers.getRandomNumbers(this.valuesDate.length, 0, 5361);
public getDataView(columnNames?: string[]): powerbi.DataView {
return this.createCategoricalDataViewBuilder([
{
source: {
displayName: LineDotChartData.ColumnDate,
type: ValueType.fromDescriptor({ dateTime: true }),
roles: { Date: true }
},
values: this.valuesDate
}
], [
{
source: {
displayName: "Values",
type: ValueType.fromDescriptor({ integer: true }),
roles: { Values: true }
},
values: this.valuesValue
}
], columnNames).build();
}
}
}

142
test/visualTest.ts Normal file
Просмотреть файл

@ -0,0 +1,142 @@
/*
* 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.
*/
/// <reference path="_references.ts"/>
namespace powerbitests.customVisuals {
import VisualClass = powerbi.extensibility.visual.test.LineDotChartBuilder;
import LineDotChartData = powerbi.extensibility.visual.test.LineDotChartData;
import LineDotChartBuilder = powerbi.extensibility.visual.test.LineDotChartBuilder;
import helpers = powerbi.extensibility.utils.test.helpers;
import colorHelper = powerbi.extensibility.utils.test.helpers.color;
import RgbColor = powerbi.extensibility.utils.test.helpers.color.RgbColor;
import MockISelectionId = powerbi.extensibility.utils.test.mocks.MockISelectionId;
import createSelectionId = powerbi.extensibility.utils.test.mocks.createSelectionId;
import fromPointToPixel = powerbi.extensibility.utils.type.PixelConverter.fromPointToPixel;
import getRandomHexColor = powerbitests.customVisuals.getRandomHexColor;
describe("LineDotChartTests", () => {
let visualBuilder: powerbi.extensibility.visual.test.LineDotChartBuilder;
let defaultDataViewBuilder: LineDotChartData;
let dataView: powerbi.DataView;
beforeEach(() => {
visualBuilder = new LineDotChartBuilder(1000, 500);
defaultDataViewBuilder = new LineDotChartData();
dataView = defaultDataViewBuilder.getDataView();
});
describe("DOM tests", () => {
it("main element was created", () => {
expect(visualBuilder.mainElement.get(0)).toBeDefined();
});
it("update", (done) => {
visualBuilder.updateRenderTimeout(dataView, () => {
expect(visualBuilder.mainElement.find(".axis").length).not.toBe(0);
expect(visualBuilder.mainElement.find(".tick").length).not.toBe(0);
expect(visualBuilder.mainElement.find(".lineDotChart__playBtn").get(0)).toBeDefined();
expect(visualBuilder.mainElement.find(".legends").get(0)).toBeDefined();
done();
});
});
});
describe("Resize test", () => {
it("Counter", (done) => {
visualBuilder.viewport.width = 300;
dataView.metadata.objects = {
misc: {
isAnimated: true,
duration: 20
},
counteroptions: {
counterTitle: "Counter: "
}
};
visualBuilder.updateFlushAllD3Transitions(dataView);
helpers.clickElement(visualBuilder.animationPlay);
helpers.renderTimeout(() => {
expect(visualBuilder.counterTitle).toBeInDOM();
done();
});
});
});
describe("Format settings test", () => {
beforeEach(() => {
dataView.metadata.objects = {
misc: {
isAnimated: false
}
};
});
describe("Line", () => {
it("color", () => {
let color: string = getRandomHexColor();
(dataView.metadata.objects as any).lineoptions = { fill: colorHelper.getSolidColorStructuralObject(color) };
visualBuilder.updateFlushAllD3Transitions(dataView);
colorHelper.assertColorsMatch(visualBuilder.linePath.css('stroke'), color);
});
});
describe("Dot", () => {
it("color", () => {
let color: string = getRandomHexColor();
dataView.metadata.objects = {
dotoptions: {
color: colorHelper.getSolidColorStructuralObject(color)
}
};
visualBuilder.updateFlushAllD3Transitions(dataView);
visualBuilder.dots.toArray().map($).forEach(e =>
colorHelper.assertColorsMatch(e.attr('fill'), color));
});
});
describe("Validate params", () => {
it("Dots", () => {
dataView.metadata.objects = {
dotoptions: {
dotSizeMin: -6,
dotSizeMax: 678
}
};
visualBuilder.updateFlushAllD3Transitions(dataView);
visualBuilder.dots.toArray().map($).forEach(e => {
expect(e.attr("r")).toBeGreaterThan(-1);
expect(e.attr("r")).toBeLessThan(101);
});
});
});
});
});
}

28
tsconfig.json Normal file
Просмотреть файл

@ -0,0 +1,28 @@
{
"compilerOptions": {
"allowJs": false,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"target": "ES5",
"sourceMap": true,
"out": "./.tmp/build/visual.js",
"declaration": true
},
"files": [
"typings/index.d.ts",
".api/v1.3.0/PowerBI-visuals.d.ts",
"node_modules/powerbi-visuals-utils-formattingutils/lib/index.d.ts",
"node_modules/powerbi-visuals-utils-interactivityutils/lib/index.d.ts",
"node_modules/powerbi-visuals-utils-typeutils/lib/index.d.ts",
"node_modules/powerbi-visuals-utils-svgutils/lib/index.d.ts",
"node_modules/powerbi-visuals-utils-chartutils/lib/index.d.ts",
"node_modules/powerbi-visuals-utils-dataviewutils/lib/index.d.ts",
"node_modules/powerbi-visuals-utils-tooltiputils/lib/index.d.ts",
"src/dataInterfaces.ts",
"src/settings.ts",
"src/columns.ts",
"src/visualLayout.ts",
"src/behavior.ts",
"src/visual.ts"
]
}

58
tslint.json Normal file
Просмотреть файл

@ -0,0 +1,58 @@
{
"rules": {
"class-name": true,
"comment-format": [
true,
"check-space"
],
"indent": [
true,
"spaces"
],
"no-duplicate-variable": true,
"no-eval": true,
"no-internal-module": false,
"no-trailing-whitespace": true,
"no-unsafe-finally": true,
"no-var-keyword": true,
"one-line": [
true,
"check-open-brace",
"check-whitespace"
],
"quotemark": [
false,
"double"
],
"semicolon": [
true,
"always"
],
"triple-equals": [
true,
"allow-null-check"
],
"typedef-whitespace": [
true,
{
"call-signature": "nospace",
"index-signature": "nospace",
"parameter": "nospace",
"property-declaration": "nospace",
"variable-declaration": "nospace"
}
],
"variable-name": [
true,
"ban-keywords"
],
"whitespace": [
true,
"check-branch",
"check-decl",
"check-operator",
"check-separator",
"check-type"
]
}
}

9
typings.json Normal file
Просмотреть файл

@ -0,0 +1,9 @@
{
"globalDependencies": {
"d3": "registry:dt/d3#0.0.0+20160907005744",
"jasmine": "registry:dt/jasmine#2.5.0+20161119044246",
"jasmine-jquery": "registry:dt/jasmine-jquery#1.5.8+20161128184045",
"jquery": "registry:dt/jquery#1.10.0+20161119044246",
"lodash": "registry:dt/lodash#4.14.0+20161110215204"
}
}