diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..5f3a711
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,7 @@
+node_modules
+.DS_Store
+.tmp
+dist
+typings
+.api
+*.log
diff --git a/assets/icon.png b/assets/icon.png
new file mode 100644
index 0000000..4eb96f2
Binary files /dev/null and b/assets/icon.png differ
diff --git a/assets/icon.svg b/assets/icon.svg
new file mode 100644
index 0000000..db9b150
--- /dev/null
+++ b/assets/icon.svg
@@ -0,0 +1,12 @@
+
+
+
+
diff --git a/assets/screenshot.png b/assets/screenshot.png
new file mode 100644
index 0000000..158c2ff
Binary files /dev/null and b/assets/screenshot.png differ
diff --git a/assets/thumb.png b/assets/thumb.png
new file mode 100644
index 0000000..202b324
Binary files /dev/null and b/assets/thumb.png differ
diff --git a/capabilities.json b/capabilities.json
new file mode 100644
index 0000000..d3e2808
--- /dev/null
+++ b/capabilities.json
@@ -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 }
+ }
+ }
+ }
+ }
+}
diff --git a/karma.conf.js b/karma.conf.js
new file mode 100644
index 0000000..c183db8
--- /dev/null
+++ b/karma.conf.js
@@ -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');
+ }
+ }
+ });
+};
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..0a17b3c
--- /dev/null
+++ b/package.json
@@ -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"
+ }
+}
diff --git a/pbiviz.json b/pbiviz.json
new file mode 100644
index 0000000..2bfb12f
--- /dev/null
+++ b/pbiviz.json
@@ -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"
+}
\ No newline at end of file
diff --git a/src/behavior.ts b/src/behavior.ts
new file mode 100644
index 0000000..206a8c7
--- /dev/null
+++ b/src/behavior.ts
@@ -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;
+ private interactivityService: IInteractivityService;
+ private hasHighlights: boolean;
+
+ public bindEvents(options: LineDotChartBehaviorOptions, selectionHandler: ISelectionHandler): void {
+ let selection: d3.Selection = this.selection = options.selection;
+ let clearCatcher: d3.Selection = 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;
+ clearCatcher: d3.Selection;
+ interactivityService: IInteractivityService;
+ hasHighlights: boolean;
+ }
+}
diff --git a/src/columns.ts b/src/columns.ts
new file mode 100644
index 0000000..a391660
--- /dev/null
+++ b/src/columns.ts
@@ -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 {
+
+ public static getColumnSources(dataView: DataView) {
+ return this.getColumnSourcesT(dataView);
+ }
+
+ public static getTableValues(dataView: DataView) {
+ let table: DataViewTable = dataView && dataView.table;
+ let columns: LineDotChartColumns = this.getColumnSourcesT(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 = this.getColumnSourcesT(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 || [];
+ let series: any = categorical && values.source && this.getSeriesValues(dataView);
+ return categorical && _.mapValues(new this(), (n, i) =>
+ (_.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 || [];
+ return categorical && _.mapValues(
+ new this(),
+ (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(),
+ (n, i) => g.values.filter(v => v.source.roles[i])[0]));
+ }
+
+ private static getColumnSourcesT(dataView: DataView) {
+ let columns: DataViewMetadataColumn[] = dataView && dataView.metadata && dataView.metadata.columns;
+ return columns && _.mapValues(
+ new this(), (n, i) => columns.filter(x => x.roles && x.roles[i])[0]);
+ }
+
+ public Date: T = null;
+ public Values: T = null;
+ }
+}
diff --git a/src/dataInterfaces.ts b/src/dataInterfaces.ts
new file mode 100644
index 0000000..cab7a8e
--- /dev/null
+++ b/src/dataInterfaces.ts
@@ -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;
+ }
+}
+
+
diff --git a/src/settings.ts b/src/settings.ts
new file mode 100644
index 0000000..425d66c
--- /dev/null
+++ b/src/settings.ts
@@ -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;
+ }
+}
diff --git a/src/visual.ts b/src/visual.ts
new file mode 100644
index 0000000..b0c4396
--- /dev/null
+++ b/src/visual.ts
@@ -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 {
+ 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;
+ private main: d3.Selection;
+ private axes: d3.Selection;
+ private axisX: d3.Selection;
+ private axisY: d3.Selection;
+ private axisY2: d3.Selection;
+ private legends: d3.Selection;
+ private line: d3.Selection;
+ 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 = 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: [
+ {
+ 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 = 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 = 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((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(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 = 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 = this.line.selectAll(LineDotChart.gLineDotChartPayBtn).data([""]);
+ let playBtnGroup: d3.Selection = 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) {
+ linePathSelection.enter().append("g").classed(LineDotChart.pathClassName, true);
+
+ let pathPlot: d3.selection.Update = linePathSelection.selectAll(LineDotChart.pathPlotClassName).data(d => [d]);
+ pathPlot.enter()
+ .append('path')
+ .classed(LineDotChart.plotClassName, true);
+
+ // Draw the line
+ let drawLine: d3.svg.Line = 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) {
+ let clipPath: d3.selection.Update = 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 = this.line.selectAll('g.' + LineDotChart.dotPointsClass)
+ .data([this.data.dotPoints]);
+
+ lineTipSelection.enter()
+ .append("g")
+ .classed(LineDotChart.dotPointsClass, true);
+
+ let dotsSelection: d3.selection.Update = 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 = this.line.selectAll(LineDotChart.textSelector);
+ let lineText: d3.selection.Update = 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(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, 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 = points[num].time;
+ let min: number = points[0].time;
+ let max: 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(this).classed(LineDotChart.showClassName, true);
+ }
+
+ private hideDataPoint(data: LineDotPoint, index: number): void {
+ d3.select(this).classed(LineDotChart.showClassName, false);
+ }
+
+ private addTooltip(element: any): void {
+ let selection: d3.Selection = 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 = 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;
+ }
+ }
+}
diff --git a/src/visualLayout.ts b/src/visualLayout.ts
new file mode 100644
index 0000000..732f21b
--- /dev/null
+++ b/src/visualLayout.ts
@@ -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(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(object: T, objectChanged: (o?: T, key?: string) => void): T {
+ let result: T = {};
+ _.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(value: T, minValue?: T): T {
+ _.keys(value).forEach(x => value[x] = Math.max(minValue && minValue[x] || 0, value[x]));
+ return value;
+ }
+ }
+}
+
diff --git a/style/lineDotChart.less b/style/lineDotChart.less
new file mode 100644
index 0000000..1d982b7
--- /dev/null
+++ b/style/lineDotChart.less
@@ -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);
+}
\ No newline at end of file
diff --git a/test/_references.ts b/test/_references.ts
new file mode 100644
index 0000000..18d632a
--- /dev/null
+++ b/test/_references.ts
@@ -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
+///
+///
+///
+
+// Power BI API
+///
+
+// Power BI Extensibility
+///
+///
+///
+///
+///
+///
+///
+///
+///
+
+// The visual
+///
+
+// Test
+///
+///
+///
+
diff --git a/test/helpers.ts b/test/helpers.ts
new file mode 100644
index 0000000..86138d9
--- /dev/null
+++ b/test/helpers.ts
@@ -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.
+ */
+
+///
+
+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());
+ }
+
+}
\ No newline at end of file
diff --git a/test/visualBuilder.ts b/test/visualBuilder.ts
new file mode 100644
index 0000000..1794735
--- /dev/null
+++ b/test/visualBuilder.ts
@@ -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.
+ */
+
+///
+
+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 {
+ 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");
+ }
+
+ }
+}
diff --git a/test/visualData.ts b/test/visualData.ts
new file mode 100644
index 0000000..c59942e
--- /dev/null
+++ b/test/visualData.ts
@@ -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.
+ */
+
+///
+
+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();
+ }
+ }
+}
diff --git a/test/visualTest.ts b/test/visualTest.ts
new file mode 100644
index 0000000..bd13cf4
--- /dev/null
+++ b/test/visualTest.ts
@@ -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.
+ */
+
+///
+
+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);
+ });
+ });
+ });
+
+ });
+ });
+}
diff --git a/tsconfig.json b/tsconfig.json
new file mode 100644
index 0000000..8e2f6cf
--- /dev/null
+++ b/tsconfig.json
@@ -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"
+ ]
+}
\ No newline at end of file
diff --git a/tslint.json b/tslint.json
new file mode 100644
index 0000000..aaaf8a6
--- /dev/null
+++ b/tslint.json
@@ -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"
+ ]
+ }
+}
\ No newline at end of file
diff --git a/typings.json b/typings.json
new file mode 100644
index 0000000..8d0d468
--- /dev/null
+++ b/typings.json
@@ -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"
+ }
+}