diff --git a/capabilities.json b/capabilities.json index 3a093ee..84dcdc7 100644 --- a/capabilities.json +++ b/capabilities.json @@ -64,6 +64,11 @@ "integer": true } ] + }, + { + "displayName": "Legend", + "name": "legend", + "kind": "Grouping" } ], "dataViewMappings": [ @@ -82,6 +87,9 @@ "slabY": { "max": 1 }, + "legend": { + "max": 1 + }, "category": { "max": 16 }, @@ -117,8 +125,17 @@ "for": { "in": "tooltip" } + }, + { + "for": { + "in": "legend" + } } - ] + ], "dataReductionAlgorithm": { + "top": { + "count": 2000 + } + } } } } @@ -202,6 +219,28 @@ } } }, + "legendSettings": { + "displayName": "Legend Settings", + "properties": { + "legendTitle": { + "displayName": "Legend Title", + "type": { + "text": true + } + }, + "legendColor": { + "displayName": "Legend Color", + "description": "The color of this legend type.", + "type": { + "fill": { + "solid": { + "color": true + } + } + } + } + } + }, "plotSettings": { "displayName": "Plot Settings", "properties": { @@ -229,6 +268,12 @@ } } } + }, + "useLegendColor": { + "displayName": "Use Legend Color", + "type": { + "bool": true + } } } }, @@ -310,7 +355,9 @@ "properties": { "show": { "displayName": "Enable Zooming", - "type": {"bool": true} + "type": { + "bool": true + } }, "maximum": { "displayName": "Maximum Zoom Factor", diff --git a/src/constants.ts b/src/constants.ts index f38d47a..6cb0454 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -3,6 +3,7 @@ export enum Settings { axisSettings = "axisSettings", colorSelector = "colorSelector", colorSettings = "colorSettings", + legendSettings = "legendSettings", overlayPlotSettings = "overlayPlotSettings", plotTitleSettings = "plotTitleSettings", tooltipTitleSettings = "tooltipTitleSettings", @@ -13,10 +14,15 @@ export enum YRangeSettingsNames{ min = "min", max = "max" } +export enum LegendSettingsNames{ + legendTitle = "legendTitle", + legendColor = "legendColor" +} export enum PlotSettingsNames { plotType = "plotType", - fill = "fill" + fill = "fill", + useLegendColor = "useLegendColor" } export enum TooltipTitleSettingsNames { title = "title" diff --git a/src/errors.ts b/src/errors.ts index cfdae3c..ac05540 100644 --- a/src/errors.ts +++ b/src/errors.ts @@ -83,6 +83,17 @@ export class PlotSizeError extends ParseAndTransformError { } } +export class PlotLegendError extends ParseAndTransformError { + /** + * + */ + constructor(plotName:string) { + const name = "Plot Legend Error"; + const message = `There is legend no data but legend colors are set to be used by ${plotName}. Please add legend data in the field pane.`; + super(message, name); + + } +} export class GetAxisInformationError extends ParseAndTransformError { /** diff --git a/src/marginSettings.ts b/src/marginSettings.ts index 16baa6f..6913ec4 100644 --- a/src/marginSettings.ts +++ b/src/marginSettings.ts @@ -2,8 +2,9 @@ import { Margins } from './plotInterface'; export class MarginSettings { static readonly svgTopPadding = 0; - static readonly svgBottomPadding = 10 + static readonly svgBottomPadding = 0; static readonly plotTitleHeight = 18; + static readonly legendHeight = 20; static readonly dotMargin = 4; static readonly margins: Margins = { top: 10, diff --git a/src/parseAndTransform.ts b/src/parseAndTransform.ts index 14105ef..913a5c9 100644 --- a/src/parseAndTransform.ts +++ b/src/parseAndTransform.ts @@ -4,12 +4,12 @@ import IVisualHost = powerbi.extensibility.visual.IVisualHost; import VisualUpdateOptions = powerbi.extensibility.visual.VisualUpdateOptions; import ISandboxExtendedColorPalette = powerbi.extensibility.ISandboxExtendedColorPalette; import { getValue, getColumnnColorByIndex, getAxisTextFillColor, getPlotFillColor, getColorSettings } from './objectEnumerationUtility'; -import { ViewModel, DataPoint, FormatSettings, PlotSettings, PlotModel, TooltipDataPoint, XAxisData, YAxisData, PlotType, SlabRectangle, SlabType, GeneralPlotSettings, Margins, AxisInformation, AxisInformationInterface, TooltipModel, ZoomingSettings } from './plotInterface'; +import { ViewModel, DataPoint, FormatSettings, PlotSettings, PlotModel, TooltipDataPoint, XAxisData, YAxisData, PlotType, SlabRectangle, SlabType, GeneralPlotSettings, Margins, AxisInformation, AxisInformationInterface, TooltipModel, ZoomingSettings, LegendData, Legend, LegendValue } from './plotInterface'; import { Color } from 'd3'; import { AxisSettingsNames, PlotSettingsNames, Settings, ColorSettingsNames, OverlayPlotSettingsNames, PlotTitleSettingsNames, TooltipTitleSettingsNames, YRangeSettingsNames, ZoomingSettingsNames } from './constants'; import { MarginSettings } from './marginSettings' import { ok, err, Result } from 'neverthrow' -import { AxisError, AxisNullValuesError, GetAxisInformationError, NoAxisError, NoValuesError, ParseAndTransformError, PlotSizeError, SVGSizeError } from './errors' +import { AxisError, AxisNullValuesError, GetAxisInformationError, NoAxisError, NoValuesError, ParseAndTransformError, PlotLegendError, PlotSizeError, SVGSizeError } from './errors' // TODO #n: Allow user to change bars colors @@ -65,7 +65,7 @@ export function visualTransform(options: VisualUpdateOptions, host: IVisualHost) let xData = new Array(xCount); let yData = new Array(yCount); let tooltipData = new Array(tooltipCount); - + let legendData: LegendData = null; let xDataPoints: number[] = []; @@ -73,7 +73,7 @@ export function visualTransform(options: VisualUpdateOptions, host: IVisualHost) let dataPoints: DataPoint[] = []; let slabWidth: number[] = []; let slabLength: number[] = []; - + let legend: Legend = null; //aquire all categorical values if (categorical.categories !== undefined) { @@ -109,6 +109,13 @@ export function visualTransform(options: VisualUpdateOptions, host: IVisualHost) }; tooltipData[tooltipId] = data; } + else if (roles.legend) { + legendData = { + name: category.source.displayName, + values: category.values, + columnId: category.source.index + }; + } } } //aquire all measure values @@ -146,14 +153,63 @@ export function visualTransform(options: VisualUpdateOptions, host: IVisualHost) }; tooltipData[tooltipId] = data; } + else if (roles.legend) { + legendData = { + name: value.source.displayName, + values: value.values, + columnId: value.source.index + }; + } } } + + const possibleNullValues: XAxisData[] = xData.filter(x => x.values.filter(y => y === null || y === undefined).length > 0) if (possibleNullValues.length > 0) { return err(new AxisNullValuesError(possibleNullValues[0].name)); } + const legendColors = { + OZE: "#e41a1c", + GZE: "#377eb8", + RAS: "#4daf4a" + } + + if (legendData != null) { + let legendSet = new Set(legendData.values); + + if (legendSet.has(null)) { + legendSet.delete(null); + } + let legendValues = Array.from(legendSet); + legend = { + legendDataPoints: [], + legendValues: [] + } + for (let i = 0; i < legendValues.length; i++) { + const val = legendValues[i] + legend.legendValues.push({ + color: legendColors[val], + selected: false, + value: val, + identity: host.createSelectionIdBuilder().createSelectionId() + }); + } + let legendXValues = xData[0].values; + for (let i = 0; i < Math.min(legendData.values.length, legendXValues.length); i++) { + legend.legendDataPoints.push({ + xValue: legendXValues[i], + yValue: legendData.values[i] + }); + + } + + + } + + debugger; + let plotTitles: string[] = []; for (let plotNr = 0; plotNr < yCount; plotNr++) { let yAxis: YAxisData = yData[plotNr] @@ -163,7 +219,7 @@ export function visualTransform(options: VisualUpdateOptions, host: IVisualHost) } let plotTitlesCount = plotTitles.filter(x => x.length > 0).length; let viewModel: ViewModel; - let viewModelResult = createViewModel(options, yCount, objects, colorPalette, plotTitlesCount) + let viewModelResult = createViewModel(options, yCount, objects, colorPalette, plotTitlesCount, legend) .map(vm => viewModel = vm) if (viewModelResult.isErr()) { return viewModelResult.mapErr(err => { return err; }); @@ -182,25 +238,41 @@ export function visualTransform(options: VisualUpdateOptions, host: IVisualHost) yDataPoints = yAxis.values; const maxLengthAttributes = Math.max(xDataPoints.length, yDataPoints.length); dataPoints = []; - + const yColumnId = yData[plotNr].columnId; + const yColumnObjects = metadataColumns[yColumnId].objects; + const plotSettings: PlotSettings = { + plotSettings: { + fill: getPlotFillColor(yColumnObjects, colorPalette, '#000000'), + plotType: PlotType[getValue(yColumnObjects, Settings.plotSettings, PlotSettingsNames.plotType, PlotType.LinePlot)], + useLegendColor: getValue(yColumnObjects, Settings.plotSettings, PlotSettingsNames.useLegendColor, false) + } + } //create datapoints for (let pointNr = 0; pointNr < maxLengthAttributes; pointNr++) { - const color: string = '#0f0f0f'; //getColumnnColorByIndex(xDataPoints, i, colorPalette); // TODO Add colors only if required - const selectionId: ISelectionId = host.createSelectionIdBuilder().withMeasure(xDataPoints[pointNr].toString()).createSelectionId(); + let color = plotSettings.plotSettings.fill; + const xVal = xDataPoints[pointNr]; + if (plotSettings.plotSettings.useLegendColor) { + if (legend != null) { + const legendVal = legend.legendDataPoints.find(x => x.xValue == xVal).yValue; + color = legendVal == null ? color : legend.legendValues.find(x => x.value == legendVal).color; + }else{ + return err(new PlotLegendError(yAxis.name)); + } + } + //const color = legend.legendValues.fin legend.legendDataPoints[pointNr].yValue let dataPoint: DataPoint = { - xValue: xDataPoints[pointNr], + xValue: xVal, yValue: yDataPoints[pointNr], identity: selectionId, selected: false, - color: color, + color: color }; dataPoints.push(dataPoint); } //get index of y-column in metadata - let yColumnId = yData[plotNr].columnId; - let yColumnObjects = metadataColumns[yColumnId].objects; + dataPoints = dataPoints.sort((a: DataPoint, b: DataPoint) => { return a.xValue - b.xValue; @@ -228,18 +300,14 @@ export function visualTransform(options: VisualUpdateOptions, host: IVisualHost) let plotTitle = plotTitles[plotNr] plotTop = plotTitle.length > 0 ? plotTop + MarginSettings.plotTitleHeight : plotTop; + let plotModel: PlotModel = { plotId: plotNr, formatSettings: formatSettings, xName: xAxis.name, yName: yAxis.name, plotTop: plotTop, - plotSettings: { - plotSettings: { - fill: getPlotFillColor(yColumnObjects, colorPalette, '#000000'), - plotType: PlotType[getValue(yColumnObjects, Settings.plotSettings, PlotSettingsNames.plotType, PlotType.LinePlot)] - }, - }, + plotSettings: plotSettings, plotTitleSettings: { title: plotTitle//getValue(yColumnObjects, Settings.plotTitleSettings, PlotTitleSettingsNames.title, yAxis.name) }, @@ -261,6 +329,7 @@ export function visualTransform(options: VisualUpdateOptions, host: IVisualHost) viewModel.plotModels[plotNr] = plotModel; plotTop += viewModel.generalPlotSettings.plotHeight + MarginSettings.margins.top + MarginSettings.margins.bottom; } + viewModel.generalPlotSettings.legendYPostion = plotTop; return ok(viewModel); @@ -324,14 +393,15 @@ function createSlabInformation(slabLength: number[], slabWidth: number[], viewMo } } -function createViewModel(options: VisualUpdateOptions, yCount: number, objects: powerbi.DataViewObjects, colorPalette: ISandboxExtendedColorPalette, plotTitlesCount: number): Result { +function createViewModel(options: VisualUpdateOptions, yCount: number, objects: powerbi.DataViewObjects, colorPalette: ISandboxExtendedColorPalette, plotTitlesCount: number, legend: Legend): Result { const margins = MarginSettings const svgHeight: number = options.viewport.height; const svgWidth: number = options.viewport.width; + const legendHeight = legend ? margins.legendHeight : 0; if (svgHeight === undefined || svgWidth === undefined || !svgHeight || !svgWidth) { return err(new SVGSizeError()); } - const plotHeightSpace: number = (svgHeight - margins.svgTopPadding - margins.svgBottomPadding - margins.plotTitleHeight * plotTitlesCount) / yCount; + const plotHeightSpace: number = (svgHeight - margins.svgTopPadding - margins.svgBottomPadding - legendHeight - margins.plotTitleHeight * plotTitlesCount) / yCount; if (plotHeightSpace < margins.miniumumPlotHeight) { return err(new PlotSizeError("vertical")); } @@ -339,15 +409,18 @@ function createViewModel(options: VisualUpdateOptions, yCount: number, objects: if (plotWidth < margins.miniumumPlotWidth) { return err(new PlotSizeError("horizontal")); } + let generalPlotSettings: GeneralPlotSettings = { plotTitleHeight: margins.plotTitleHeight, dotMargin: margins.dotMargin, plotHeight: plotHeightSpace - margins.margins.top - margins.margins.bottom, plotWidth: plotWidth, + legendHeight: legendHeight, xScalePadding: 0.1, solidOpacity: 1, transparentOpacity: 1, - margins: margins.margins + margins: margins.margins, + legendYPostion: 0 }; const zoomingSettings: ZoomingSettings = { @@ -369,7 +442,8 @@ function createViewModel(options: VisualUpdateOptions, yCount: number, objects: svgHeight: svgHeight, svgTopPadding: margins.svgTopPadding, svgWidth: svgWidth, - zoomingSettings: zoomingSettings + zoomingSettings: zoomingSettings, + legend: legend }; return ok(viewModel); } diff --git a/src/plotInterface.ts b/src/plotInterface.ts index ad4099b..f8a2f48 100644 --- a/src/plotInterface.ts +++ b/src/plotInterface.ts @@ -16,12 +16,15 @@ export interface ViewModel { generalPlotSettings: GeneralPlotSettings; tooltipModels: TooltipModel[]; zoomingSettings: ZoomingSettings; + legend: Legend; } export interface GeneralPlotSettings { plotHeight: number; plotWidth: number; plotTitleHeight: number; + legendHeight: number; + legendYPostion: number; dotMargin: number; xScalePadding: number; solidOpacity: number; @@ -115,6 +118,21 @@ export interface TooltipDataPoint { } +export interface LegendDataPoint { + xValue: PrimitiveValue; + yValue: PrimitiveValue; +} + +export interface LegendValue extends SelectableDataPoint { + color?: string; + value: PrimitiveValue; +} + +export interface Legend{ + legendDataPoints: LegendDataPoint[]; + legendValues: LegendValue[]; +} + export interface DataPoint extends SelectableDataPoint { xValue: PrimitiveValue; yValue: PrimitiveValue; @@ -147,6 +165,7 @@ export interface PlotSettings { plotSettings: { fill: string; plotType: PlotType; + useLegendColor: boolean; }; } @@ -156,12 +175,12 @@ export interface OverlayPlotSettings { }; } -export interface Legend { - text: string; - transform?: string; - dx?: string; - dy?: string; -} +// export interface Legend { +// text: string; +// transform?: string; +// dx?: string; +// dy?: string; +// } export interface XAxisData { values: number[]; @@ -174,6 +193,12 @@ export interface YAxisData { columnId: number; } +export interface LegendData { + values: string[]; + name?: string; + columnId: number; +} + export interface D3Plot { type: string; plot: any; diff --git a/src/visual.ts b/src/visual.ts index 84194d7..349eeee 100644 --- a/src/visual.ts +++ b/src/visual.ts @@ -45,9 +45,10 @@ import * as d3 from 'd3'; import { getPlotFillColor, getValue, getColorSettings } from './objectEnumerationUtility'; import { TooltipInterface, ViewModel, DataPoint, PlotModel, PlotType, SlabType, D3Plot, D3PlotXAxis, D3PlotYAxis, SlabRectangle, AxisInformation, TooltipModel, TooltipData, ZoomingSettings } from './plotInterface'; import { visualTransform } from './parseAndTransform'; -import { OverlayPlotSettingsNames, ColorSettingsNames, Constants, AxisSettingsNames, PlotSettingsNames, Settings, PlotTitleSettingsNames, TooltipTitleSettingsNames, YRangeSettingsNames, ZoomingSettingsNames } from './constants'; +import { OverlayPlotSettingsNames, ColorSettingsNames, Constants, AxisSettingsNames, PlotSettingsNames, Settings, PlotTitleSettingsNames, TooltipTitleSettingsNames, YRangeSettingsNames, ZoomingSettingsNames, LegendSettingsNames } from './constants'; import { err, ok, Result } from 'neverthrow'; import { AddClipPathError, AddPlotTitlesError, AddVerticalRulerError, AddZoomError, BuildBasicPlotError, BuildXAxisError, BuildYAxisError, CustomTooltipError, DrawLinePlotError, DrawScatterPlotError, PlotError, SlabInformationError } from './errors'; +import { dataViewWildcard } from "powerbi-visuals-utils-dataviewutils"; type Selection = d3.Selection; @@ -69,7 +70,38 @@ export class Visual implements IVisual { } + private drawLegend() { + const margins = this.viewModel.generalPlotSettings; + const yPosition = margins.legendYPostion; + const legendData = this.viewModel.legend.legendValues; + let widths = []; + let width = margins.margins.left; + this.svg.selectAll("legendText") + .data(legendData) + .enter() + .append("text") + .text(function (d) { return String(d.value) }) + .attr("x", function (d, i) { + let x = width + widths.push(width); + width = width + 25 + this.getComputedTextLength(); + return 10 + x; + }) + .attr("y", yPosition) // 100 is where the first dot appears. 25 is the distance between dots + // .style("fill", function (d) { return color(d) }) + + .attr("text-anchor", "left") + .style("alignment-baseline", "middle") + this.svg.selectAll("legendDots") + .data(legendData) + .enter() + .append("circle") + .attr("cx", function (d, i) { return widths[i] }) + .attr("cy", yPosition) // 100 is where the first dot appears. 25 is the distance between dots + .attr("r", 7) + .style("fill", function (d) { return d.color }) + } public update(options: VisualUpdateOptions) { @@ -82,8 +114,12 @@ export class Visual implements IVisual { this.svg.attr("width", this.viewModel.svgWidth) .attr("height", this.viewModel.svgHeight); // this.displayError(new Error("this is a test")); + if (this.viewModel.legend != null) { + this.drawLegend(); + } this.drawPlots(options); + }).mapErr(err => this.displayError(err)); } catch (error) { @@ -176,6 +212,13 @@ export class Visual implements IVisual { const generalPlotSettings = this.viewModel.generalPlotSettings; const plotWidth = generalPlotSettings.plotWidth; const plotHeight = generalPlotSettings.plotHeight; + this.svg.append('defs').append('clipPath') + .attr('id', 'clip') + .append('rect') + .attr('y', -generalPlotSettings.dotMargin) + .attr('x', -generalPlotSettings.dotMargin) + .attr('width', plotWidth - generalPlotSettings.margins.right + 2 * generalPlotSettings.dotMargin) + .attr('height', plotHeight + 2 * generalPlotSettings.dotMargin); return ok(null); } catch (error) { return err(new AddClipPathError(error.stack)) @@ -186,6 +229,14 @@ export class Visual implements IVisual { try { const generalPlotSettings = this.viewModel.generalPlotSettings; if (plotModel.plotTitleSettings.title.length > 0) { + plot + .append('text') + .attr('class', 'plotTitle') + .attr('text-anchor', 'left') + .attr('y', 0 - generalPlotSettings.plotTitleHeight - generalPlotSettings.margins.top) + .attr('x', 0) + .attr('dy', '1em') + .text(plotModel.plotTitleSettings.title); } return ok(null); } catch (error) { @@ -216,20 +267,27 @@ export class Visual implements IVisual { const xAxis = plot.append('g').classed('xAxis', true); const xScale = scaleLinear().domain([0, plotModel.xRange.max]).range([0, generalPlotSettings.plotWidth]); const xAxisValue = axisBottom(xScale); - + let xLabel = null; if (!plotModel.formatSettings.axisSettings.xAxis.ticks) { xAxisValue.tickValues([]); } if (plotModel.formatSettings.axisSettings.xAxis.lables) { + xLabel = plot + .append('text') + .attr('class', 'xLabel') + .attr('text-anchor', 'end') + .attr('x', generalPlotSettings.plotWidth / 2) + .attr('y', generalPlotSettings.plotHeight + 20) + .text(plotModel.xName); } xAxis .attr('transform', 'translate(0, ' + generalPlotSettings.plotHeight + ')') .call(xAxisValue); - return ok({ xAxis, xAxisValue, xScale, xLabel: null }); + return ok({ xAxis, xAxisValue, xScale, xLabel: xLabel }); } catch (error) { return err(new BuildXAxisError(error.stack)) } @@ -348,7 +406,7 @@ export class Visual implements IVisual { .data(dataPoints) .enter() .append('circle') - .attr('fill', plotModel.plotSettings.plotSettings.fill) + .attr('fill', (d: DataPoint) => d.color) .attr('stroke', 'none') .attr('cx', (d) => x.xScale(d.xValue)) .attr('cy', (d) => y.yScale(d.yValue)) @@ -409,7 +467,7 @@ export class Visual implements IVisual { .data(dataPoints) .enter() .append('circle') - .attr('fill', plotModel.plotSettings.plotSettings.fill) + .attr('fill', (d: DataPoint) => d.color)//plotModel.plotSettings.plotSettings.fill) .attr('stroke', 'none') .attr('cx', (d) => x.xScale(d.xValue)) .attr('cy', (d) => y.yScale(d.yValue)) @@ -646,6 +704,7 @@ export class Visual implements IVisual { // } public enumerateObjectInstances(options: EnumerateVisualObjectInstancesOptions): VisualObjectInstance[] | VisualObjectInstanceEnumerationObject { + debugger; const objectName = options.objectName; const colorPalette = this.host.colorPalette; const objects = this.dataview.metadata.objects; @@ -657,6 +716,8 @@ export class Visual implements IVisual { let metadataColumns: DataViewMetadataColumn[] = this.dataview.metadata.columns; switch (objectName) { case Settings.plotSettings: + setObjectEnumerationColumnSettings(yCount, metadataColumns, 3); + break; case Settings.axisSettings: case Settings.yRangeSettings: setObjectEnumerationColumnSettings(yCount, metadataColumns, 2); @@ -693,6 +754,33 @@ export class Visual implements IVisual { selector: null }); break; + case Settings.legendSettings: + objectEnumeration.push({ + objectName: objectName, + properties: { + legendTitle: getValue(objects, Settings.legendSettings, LegendSettingsNames.legendTitle, "Legend"), + }, + selector: null + }); + objectEnumeration.push({ + objectName: objectName, + displayName: "testing", + properties: { + legendColor: getValue(objects, Settings.legendSettings, LegendSettingsNames.legendColor, "Lnd"), + + }, + selector: dataViewWildcard.createDataViewWildcardSelector(dataViewWildcard.DataViewWildcardMatchingOption.InstancesAndTotals) + }); + objectEnumeration.push({ + objectName: objectName, + displayName: "testi2ng", + properties: { + legendColor: getValue(objects, Settings.legendSettings, LegendSettingsNames.legendColor, "Lnd"), + + }, + selector: dataViewWildcard.createDataViewWildcardSelector(dataViewWildcard.DataViewWildcardMatchingOption.InstancesAndTotals) + }); + break; case Settings.zoomingSettings: objectEnumeration.push({ @@ -725,11 +813,14 @@ export class Visual implements IVisual { case Settings.plotSettings: displayNames = { plotType: column.displayName + " Plot Type", - fill: column.displayName + " Plot Color" + fill: column.displayName + " Plot Color", + useLegendColor: column.displayName + " Use Legend Color" }; properties = { plotType: PlotType[getValue(columnObjects, Settings.plotSettings, PlotSettingsNames.plotType, PlotType.LinePlot)], - fill: getPlotFillColor(columnObjects, colorPalette, '#000000') + fill: getPlotFillColor(columnObjects, colorPalette, '#000000'), + useLegendColor: getValue(columnObjects, Settings.plotSettings, PlotSettingsNames.useLegendColor, false) + }; break;