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" + } +}