diff --git a/CHANGELOG.md b/CHANGELOG.md index 6eeb367..625c5c0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,2 +1,7 @@ +## 1.1.0 +* ADD: Support of 10000 records +* ADD: Max number of bins was increased from 100 to 5000 +* ADD: New option for setting of start and end points for X axis + ## 1.0.2 * Fixed console errors when chart contains only 1 bar \ No newline at end of file diff --git a/capabilities.json b/capabilities.json index d7e787c..8461e06 100644 --- a/capabilities.json +++ b/capabilities.json @@ -33,7 +33,9 @@ "to": "Values" }, "dataReductionAlgorithm": { - "top": {} + "top": { + "count": 10000 + } } }, "values": { @@ -159,6 +161,24 @@ } ] } + }, + "start": { + "displayName": "Start", + "displayNameKey": "Visual_XAxis_Start", + "type": { + "numeric": true + }, + "placeHolderText": "Start", + "suppressFormatPainterCopy": true + }, + "end": { + "displayName": "End", + "displayNameKey": "Visual_XAxis_End", + "type": { + "numeric": true + }, + "placeHolderText": "End", + "suppressFormatPainterCopy": true } } }, diff --git a/package.json b/package.json index 71a1745..fb667df 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "powerbi-visuals-histogram", - "version": "1.0.2", + "version": "1.1.0", "description": "A histogram chart plots data ranges into intervals. Useful for estimating density.", "repository": { "type": "git", diff --git a/pbiviz.json b/pbiviz.json index 04fa5c0..33b9c63 100644 --- a/pbiviz.json +++ b/pbiviz.json @@ -4,7 +4,7 @@ "displayName": "Histogram", "guid": "Histogram1445664487616", "visualClassName": "Histogram", - "version": "1.0.2", + "version": "1.1.0", "description": "A histogram chart plots data ranges into intervals. Useful for estimating density.", "supportUrl": "http://community.powerbi.com", "gitHubUrl": "https://github.com/Microsoft/PowerBI-visuals-histogram" diff --git a/src/dataInterfaces.ts b/src/dataInterfaces.ts index 62d1906..69d7251 100644 --- a/src/dataInterfaces.ts +++ b/src/dataInterfaces.ts @@ -71,6 +71,9 @@ module powerbi.extensibility.visual { xLegendSize: number; yLegendSize: number; + xCorrectedMax: number; + xCorrectedMin: number; + xScale?: d3.scale.Linear; yScale?: d3.scale.Linear; diff --git a/src/settings.ts b/src/settings.ts index 3436661..c538a24 100644 --- a/src/settings.ts +++ b/src/settings.ts @@ -39,7 +39,7 @@ module powerbi.extensibility.visual { export class HistogramGeneralSettings { public static DefaultBins: number = null; public static MinNumberOfBins: number = 0; - public static MaxNumberOfBins: number = 100; + public static MaxNumberOfBins: number = 5000; /** * Please note that this property isn't enumerated in capabilities.json. @@ -70,7 +70,10 @@ module powerbi.extensibility.visual { public style: HistogramAxisStyle = HistogramAxisStyle.showTitleOnly; } - export class HistogramXAxisSettings extends HistogramAxisSettings { } + export class HistogramXAxisSettings extends HistogramAxisSettings { + public start: number = null; + public end: number = null; + } export class HistogramYAxisSettings extends HistogramAxisSettings { public start: number = 0; diff --git a/src/visual.ts b/src/visual.ts index 85ac177..ae0ffbc 100644 --- a/src/visual.ts +++ b/src/visual.ts @@ -114,6 +114,12 @@ module powerbi.extensibility.visual { right: string; } + enum UpdateColumnsWidthMode { + markOnly = "markOnly" as any, + calculatePointsDiff = "calculatePointsDiff" as any, + standardCalculation = "standardCalculation" as any + } + export class Histogram implements IVisual { private static ClassName: string = "histogram"; private static FrequencyText: string = "Frequency"; @@ -321,6 +327,7 @@ module powerbi.extensibility.visual { yLegendSize: number, borderValues: HistogramBorderValues, yAxisSettings: HistogramYAxisSettings, + xAxisSettings: HistogramXAxisSettings, sourceValues: number[] = categoryColumn.values as number[]; settings = Histogram.parseSettings(dataView); @@ -381,19 +388,28 @@ module powerbi.extensibility.visual { borderValues = Histogram.getBorderValues(bins); + // min-max for Y axis yAxisSettings = settings.yAxis; - let maxYvalue: number = (yAxisSettings.end !== null) && (yAxisSettings.end > yAxisSettings.start) ? yAxisSettings.end : borderValues.maxY; - let minYValue: number = yAxisSettings.start < maxYvalue ? yAxisSettings.start : 0; - settings.yAxis.start = Histogram.getCorrectXAxisValue(minYValue); settings.yAxis.end = Histogram.getCorrectXAxisValue(maxYvalue); + // min-max for X axis + xAxisSettings = settings.xAxis; + let maxXvalue: number = (xAxisSettings.end !== null) && (xAxisSettings.end > borderValues.minX) + ? xAxisSettings.end + : borderValues.maxX; + let minXValue: number = (xAxisSettings.start !== null) && xAxisSettings.start < maxXvalue + ? xAxisSettings.start + : borderValues.minX; + settings.xAxis.start = Histogram.getCorrectXAxisValue(minXValue); + settings.xAxis.end = Histogram.getCorrectXAxisValue(maxXvalue); + if (values.length >= Histogram.MinAmountOfValues) { valueFormatter = ValueFormatter.create({ format: ValueFormatter.getFormatStringByColumn(dataView.categorical.categories[0].source), @@ -431,7 +447,9 @@ module powerbi.extensibility.visual { yLabelFormatter, xLegendSize, yLegendSize, - formatter: valueFormatter + formatter: valueFormatter, + xCorrectedMin: null, + xCorrectedMax: null }; } @@ -706,6 +724,9 @@ module powerbi.extensibility.visual { } public update(options: VisualUpdateOptions): void { + let borderValues: HistogramBorderValues, + xAxisSettings: HistogramXAxisSettings; + if (!options || !options.dataViews || !options.dataViews[0]) { @@ -721,6 +742,9 @@ module powerbi.extensibility.visual { dataView, this.visualHost); + borderValues = this.dataView.borderValues; + xAxisSettings = this.dataView.settings.xAxis; + if (!this.isDataValid(this.dataView)) { this.clear(); @@ -733,8 +757,6 @@ module powerbi.extensibility.visual { this.columsAndAxesTransform(maxWidthOfVerticalAxisLabel); - this.updateWidthOfColumn(); - this.createScales(); this.applySelectionStateToData(); @@ -798,12 +820,13 @@ module powerbi.extensibility.visual { private createScales(): void { const yAxisSettings: HistogramYAxisSettings = this.dataView.settings.yAxis, + xAxisSettings: HistogramXAxisSettings = this.dataView.settings.xAxis, borderValues: HistogramBorderValues = this.dataView.borderValues; this.dataView.xScale = d3.scale.linear() .domain([ - borderValues.minX, - borderValues.maxX + this.dataView.xCorrectedMin !== null ? this.dataView.xCorrectedMin : xAxisSettings.start, + this.dataView.xCorrectedMax !== null ? this.dataView.xCorrectedMax : xAxisSettings.end ]) .range([ 0, @@ -837,13 +860,13 @@ module powerbi.extensibility.visual { }; } - private updateWidthOfColumn(): void { + private updateWidthOfColumn(updateMode: UpdateColumnsWidthMode): void { let countOfValues: number = this.dataView.dataPoints.length, - widthOfColumn: number; + widthOfColumn: number, + borderValues: HistogramBorderValues = this.dataView.borderValues, + firstDataPoint: number = this.dataView.xCorrectedMin ? this.dataView.xCorrectedMin : borderValues.minX; - widthOfColumn = countOfValues - ? this.viewportIn.width / countOfValues - Histogram.ColumnPadding - : Histogram.MinViewportInSize; + widthOfColumn = countOfValues ? this.dataView.xScale(firstDataPoint + this.dataView.dataPoints[0].dx) - Histogram.ColumnPadding : Histogram.MinViewportInSize; this.widthOfColumn = Math.max(widthOfColumn, Histogram.MinViewportInSize); } @@ -1002,6 +1025,9 @@ module powerbi.extensibility.visual { .append("svg:rect") .classed(Histogram.Column.class, true); + // We can operate by xScale inside this function only when scale was created + this.updateWidthOfColumn(UpdateColumnsWidthMode.standardCalculation); + updateColumnsSelection .attr({ "x": (dataPoint: HistogramDataPoint) => { @@ -1362,6 +1388,45 @@ module powerbi.extensibility.visual { this.root = null; } + /// Using in case when xAxis end (set in options) is lesser than calculated border max. + /// This function detect the closest point to xAxis end (set in options). + /// Each iteration tries to shift border limit left corresponding to interval + /// and be closer to xAxis end at the same time. + private findBorderMaxCloserToXAxisEnd( + currentBorderMax: number, + xAxisEnd: number, + interval: number + ): number { + while (currentBorderMax > xAxisEnd && xAxisEnd <= currentBorderMax - interval) { + currentBorderMax -= interval; + } + + return this.formatXlabelsForFiltering(currentBorderMax); + } + + /// Using in case when xAxis start (set in options) is greater than calculated border min. + /// This function detect the closest point to xAxis start (set in options). + /// Each iteration tries to shift border limit right corresponding to interval + /// and be closer to xAxis start at the same time. + private findBorderMinCloserToXAxisStart( + currentBorderMin: number, + xAxisStart: number, + interval: number + ): number { + while (currentBorderMin < xAxisStart && xAxisStart >= currentBorderMin + interval) { + currentBorderMin += interval; + } + + return this.formatXlabelsForFiltering(currentBorderMin); + } + + private formatXlabelsForFiltering( + nonFormattedPoint: number + ): number { + let formattedPoint: string = this.dataView.xLabelFormatter.format(nonFormattedPoint); + return parseFloat(formattedPoint); + } + private calculateXAxes( source: DataViewMetadataColumn, textProperties: TextProperties, @@ -1369,10 +1434,60 @@ module powerbi.extensibility.visual { scrollbarVisible: boolean): IAxisProperties { let axes: IAxisProperties, - width: number = this.viewportIn.width; + width: number = this.viewportIn.width, + xAxisSettings: HistogramXAxisSettings = this.dataView.settings.xAxis, + xPoints: number[], + interval: number, + borderValues: HistogramBorderValues = this.dataView.borderValues, + tmpStart: number, + tmpEnd: number, + tmpArr: number[], + closerLimit: number; + + xPoints = Histogram.rangesToArray(this.dataView.dataPoints); + + // It is necessary to find out interval to calculate all necessary points before and after offset (if start and end for X axis was changed by user) + if ((borderValues.maxX !== xAxisSettings.end || borderValues.minX !== xAxisSettings.start) && xPoints.length > 1) { + interval = this.dataView.dataPoints[0].dx; + + // If start point is greater than min border, it is necessary to remove non-using data points + if (xAxisSettings.start > borderValues.minX) { + closerLimit = this.findBorderMinCloserToXAxisStart(borderValues.minX, xAxisSettings.start, interval); + xPoints = xPoints.filter(dpv => this.formatXlabelsForFiltering(dpv) >= closerLimit); + this.dataView.xCorrectedMin = xPoints && xPoints.length > 0 ? xPoints[0] : null; + } + else { + // Add points before + tmpArr = []; + tmpStart = borderValues.minX; + while (xAxisSettings.start < tmpStart) { + tmpStart = tmpStart - interval; + tmpArr.push(tmpStart); + this.dataView.xCorrectedMin = tmpStart; + } + tmpArr.reverse(); + xPoints = tmpArr.concat(xPoints); + } + + // If end point is lesser than max border, it is necessary to remove non-using data points + if (xAxisSettings.end < borderValues.maxX) { + closerLimit = this.findBorderMaxCloserToXAxisEnd(borderValues.maxX, xAxisSettings.end, interval); + xPoints = xPoints.filter(dpv => this.formatXlabelsForFiltering(dpv) <= closerLimit); + this.dataView.xCorrectedMax = xPoints && xPoints.length > 0 ? xPoints[xPoints.length - 1] : null; + } + else { + // Add points after + tmpEnd = borderValues.maxX; + while (xAxisSettings.end > tmpEnd) { + tmpEnd = tmpEnd + interval; + xPoints.push(tmpEnd); + this.dataView.xCorrectedMax = tmpEnd; + } + } + } axes = this.calculateXAxesProperties( - Histogram.rangesToArray(this.dataView.dataPoints), + xPoints, axisScale.linear, source, Histogram.InnerPaddingRatio, diff --git a/stringResources/en-US/resources.resjson b/stringResources/en-US/resources.resjson index 1587b87..d74383a 100644 --- a/stringResources/en-US/resources.resjson +++ b/stringResources/en-US/resources.resjson @@ -16,6 +16,8 @@ "Visual_YAxis": "Y-Axis", "Visual_YAxis_Start": "Start", "Visual_YAxis_End": "End", + "Visual_XAxis_Start": "Start", + "Visual_XAxis_End": "End", "Visual_Position": "Position", "Visual_Position_Left": "Left", "Visual_Position_Right": "Right", diff --git a/test/visualTest.ts b/test/visualTest.ts index 14eda19..c79a289 100644 --- a/test/visualTest.ts +++ b/test/visualTest.ts @@ -183,6 +183,243 @@ module powerbi.extensibility.visual.test { expect(parseFloat(visualBuilder.yAxisTicks.first().text())).toBe(0); }); + + it("X-axis default ticks", () => { + dataViewBuilder.valuesCategory = [ + 9, 10, 11, 12, 13, 14 + ]; + + dataViewBuilder.valuesValue = [ + 772, 878, 398, 616, 170, 267, + ]; + + dataView = dataViewBuilder.getDataView(); + + visualBuilder.updateFlushAllD3Transitions(dataView); + + expect(visualBuilder.xAxisTicks.length).toBe(5); + expect(parseFloat(visualBuilder.xAxisTicks.first().text())).toBe(9); + }); + + it("X-axis start is lesser than min", () => { + dataViewBuilder.valuesCategory = [ + 9, 10, 11, 12, 13, 14 + ]; + + dataViewBuilder.valuesValue = [ + 772, 878, 398, 616, 170, 267, + ]; + + dataView = dataViewBuilder.getDataView(); + + dataView.metadata.objects = { + xAxis: { + start: 6 + } + }; + + visualBuilder.updateFlushAllD3Transitions(dataView); + + expect(visualBuilder.xAxisTicks.length).toBe(8); + expect(parseFloat(visualBuilder.xAxisTicks.first().text())).toBe(5.25); + }); + + it("X-axis end is greater than max and bins=7", () => { + dataViewBuilder.valuesCategory = [ + 9, 10, 11, 12, 13, 14 + ]; + + dataViewBuilder.valuesValue = [ + 772, 878, 398, 616, 170, 267, + ]; + + dataView = dataViewBuilder.getDataView(); + + dataView.metadata.objects = { + xAxis: { + end: 17.34 + }, + general: { + bins: 7 + } + }; + + visualBuilder.updateFlushAllD3Transitions(dataView); + + expect(visualBuilder.xAxisTicks.length).toBe(13); + expect(parseFloat(visualBuilder.xAxisTicks.last().text())).toBe(17.57); + }); + + it("X-axis start is greater than min and bins=7", () => { + dataViewBuilder.valuesCategory = [ + 9, 10, 11, 12, 13, 14 + ]; + + dataViewBuilder.valuesValue = [ + 772, 878, 398, 616, 170, 267, + ]; + + dataView = dataViewBuilder.getDataView(); + + dataView.metadata.objects = { + xAxis: { + start: 10 + }, + general: { + bins: 7 + } + }; + + visualBuilder.updateFlushAllD3Transitions(dataView); + + expect(visualBuilder.xAxisTicks.length).toBe(7); + expect(parseFloat(visualBuilder.xAxisTicks.first().text())).toBe(9.71); + }); + + it("X-axis end is lesser than max and bins=12", () => { + dataViewBuilder.valuesCategory = [ + 9, 10, 11, 12, 13, 14 + ]; + + dataViewBuilder.valuesValue = [ + 772, 878, 398, 616, 170, 267, + ]; + + dataView = dataViewBuilder.getDataView(); + + dataView.metadata.objects = { + xAxis: { + end: 12 + }, + general: { + bins: 12 + } + }; + + visualBuilder.updateFlushAllD3Transitions(dataView); + + expect(visualBuilder.xAxisTicks.length).toBe(9); + expect(parseFloat(visualBuilder.xAxisTicks.last().text())).toBe(12.33); + }); + + it("X-axis end is lesser than max and bins=6 and periodic number case", () => { + dataViewBuilder.valuesCategory = [ + 9, 10, 11, 12, 13, 14 + ]; + + dataViewBuilder.valuesValue = [ + 772, 878, 398, 616, 170, 267, + ]; + + dataView = dataViewBuilder.getDataView(); + + dataView.metadata.objects = { + xAxis: { + end: 13 + }, + general: { + bins: 6 + } + }; + + visualBuilder.updateFlushAllD3Transitions(dataView); + + expect(visualBuilder.xAxisTicks.length).toBe(6); + expect(parseFloat(visualBuilder.xAxisTicks.first().text())).toBe(9); + expect(parseFloat(visualBuilder.xAxisTicks.last().text())).toBe(13.17); + }); + + it("X-axis start is greater than max", () => { + dataViewBuilder.valuesCategory = [ + 9, 10, 11, 12, 13, 14 + ]; + + dataViewBuilder.valuesValue = [ + 772, 878, 398, 616, 170, 267, + ]; + + dataView = dataViewBuilder.getDataView(); + + dataView.metadata.objects = { + xAxis: { + start: 17 + } + }; + + visualBuilder.updateFlushAllD3Transitions(dataView); + + expect(parseFloat(visualBuilder.xAxisTicks.first().text())).toBe(9); + }); + + it("X-axis end is lesser than min", () => { + dataViewBuilder.valuesCategory = [ + 9, 10, 11, 12, 13, 14 + ]; + + dataViewBuilder.valuesValue = [ + 772, 878, 398, 616, 170, 267, + ]; + + dataView = dataViewBuilder.getDataView(); + + dataView.metadata.objects = { + xAxis: { + end: 8 + } + }; + + visualBuilder.updateFlushAllD3Transitions(dataView); + + expect(parseFloat(visualBuilder.xAxisTicks.last().text())).toBe(14); + }); + + it("X-axis start and end is lesser than min", () => { + dataViewBuilder.valuesCategory = [ + 9, 10, 11, 12, 13, 14 + ]; + + dataViewBuilder.valuesValue = [ + 772, 878, 398, 616, 170, 267, + ]; + + dataView = dataViewBuilder.getDataView(); + + dataView.metadata.objects = { + xAxis: { + start: 8, + end: 8 + } + }; + + visualBuilder.updateFlushAllD3Transitions(dataView); + + expect(parseFloat(visualBuilder.xAxisTicks.first().text())).toBe(7.75); + expect(parseFloat(visualBuilder.xAxisTicks.last().text())).toBe(14); + }); + + it("X-axis start and end is greater than max", () => { + dataViewBuilder.valuesCategory = [ + 9, 10, 11, 12, 13, 14 + ]; + + dataViewBuilder.valuesValue = [ + 772, 878, 398, 616, 170, 267, + ]; + + dataView = dataViewBuilder.getDataView(); + + dataView.metadata.objects = { + xAxis: { + start: 16, + end: 16 + } + }; + + visualBuilder.updateFlushAllD3Transitions(dataView); + + expect(parseFloat(visualBuilder.xAxisTicks.first().text())).toBe(9); + expect(parseFloat(visualBuilder.xAxisTicks.last().text())).toBe(16.5); + }); }); describe("Format settings test", () => {