1

I'm trying to work on this visual for Power BI but I am coming across a weird issue. This is supposed to be a bar chart (will convert to line) that supports a measure on both X and Y axis. When I try to plot with my data, I get an empty visual with no errors. My formatting options that I added (like tooltips or toggle options) don't even appear in the visual formatting options pane. I have messed around and I can not get this to work or really change at regardless of what I do, short of going in and throwing random characters into my code to break the syntax. I have even tried going to older visual files that I have tinkered with in the past and made sure that I'm not doing anything wrong, such as misusing modules, or placing them improperly. I even re-imported all of my modules, checked my variables, checked for typos, etc. I cannot seem to get this thing to work. I even tried removing the part in my capabilities that allows measures to be put into the axis to see if it would plot with normal value columns. To no avail, unfortunately. Maybe I have stagnated eyes and I have been missing something obvious, or it's something more complicated than that.

I would greatly appreciate appreciate any help. Even if it's something unrelated to the issue.


"use strict";
import "@babel/polyfill";
import "./../style/visual.less";
import powerbi from "powerbi-visuals-api";
import VisualConstructorOptions = powerbi.extensibility.visual.VisualConstructorOptions;
import VisualUpdateOptions = powerbi.extensibility.visual.VisualUpdateOptions;
import IVisual = powerbi.extensibility.visual.IVisual;
import EnumerateVisualObjectInstancesOptions = powerbi.EnumerateVisualObjectInstancesOptions;
import VisualObjectInstance = powerbi.VisualObjectInstance;
import DataView = powerbi.DataView;
import VisualObjectInstanceEnumerationObject = powerbi.VisualObjectInstanceEnumerationObject;
import * as d3 from "d3";
import { VisualSettings } from "./settings";
import ISelectionManager = powerbi.extensibility.ISelectionManager;
import { ChartDataPoint, ChartViewModel } from "./viewmodels/model";
import IVisualHost = powerbi.extensibility.visual.IVisualHost;
import * as DataViewObject from 'powerbi-visuals-utils-dataviewutils';

interface DataPoints {
    duration: number;
    value: number;
    details: number;
    wells: string;
    colour: string;
    identity: powerbi.visuals.ISelectionId;
    highlighted: boolean;
};
interface ViewModel {
    dataPoints: DataPoints[];
    maxValue: number;
    highlights: boolean;
};

export class Visual implements IVisual {

    private host: IVisualHost;
    private svg: d3.Selection<SVGElement>;
    private barGroup: d3.Selection<SVGElement>;
    private viewModel: ViewModel;
    private locale: string;
    private selectionManager: ISelectionManager;
    private xAxisGroup: d3.Selection<SVGElement>;
    private yAxisGroup: d3.Selection<SVGElement>;
    private settings = {
        axis: {
            x: {
                padding: {
                    default: 50,
                    value: 50
                },
                show: {
                    default: true,
                    value: true
                }
            },
            y: {
                padding: {
                    default: 50,
                    value: 50
                }
            },
            border: {
                top: {
                    default: 10,
                    value: 10
                }
            }
        }
    }

    constructor(options: VisualConstructorOptions) {
        this.host = options.host;
        this.svg = d3.select(options.element)
            .append(".svg")
            .classed("Visual", true);
        this.barGroup = this.svg.append("g")
            .classed("bar-group", true);         //this was chart
        this.xAxisGroup = this.svg.append("g")
            .classed("x-axis", true);
        this.selectionManager = this.host.createSelectionManager();
        this.yAxisGroup = this.svg.append("g")
            .classed("y-axis", true);



    }

    //This contains the 'canvas', its scaling, and how it can or cannot interact
    public update(options: VisualUpdateOptions) {
        //this.updateSettings(options);
        let viewModel = this.getViewModel(options);
        let width = options.viewport.width;
        let height = options.viewport.height;
        let xAxisPadding = this.settings.axis.x.show.value ? this.settings.axis.x.padding.value : 0;
        // let yAxisPadding = this.settings.axis.y.show.value ? this.settings.axis.y.padding.value : 0;
        this.svg.attr({
            width: width,
            height: height
        });

        let yScale = d3.scale.linear()
            .domain([0, this.viewModel.maxValue])
            .range([height - xAxisPadding, 0 + this.settings.axis.border.top.value]);


        let xScale = d3.scale.linear()
            .domain(viewModel.dataPoints.map(d => d.duration))
           // .rangeRoundBands([yAxisPadding, width], this.xPadding);
        let xAxis = d3.svg.axis()  //come back to this later if it causes issues. https://www.youtube.com/watch?v=zLNfXxDsa-s&list=PL6z9i4iVbl8C2mtjFlH3ECb3q00eFDLAG&index=14 3:40 in
            .scale(xScale)
            .orient("bottom")
            .tickSize(.5);
        let yAxis = d3.svg.axis()
            .scale(yScale)
            .orient("left")
            .tickSize(.5);

        this.xAxisGroup
            .call(xAxis)
            .attr({
                transform: "translate(0 " + (height - xAxisPadding) + ")"
            })
            .style({
                fill: "#777777"
            })
            .selectAll("text")
            .attr({
                "text-anchor": "end",
                "font-size": "x-small"

            });
        this.yAxisGroup
            .call(yAxis)
            .attr({
                transform: "translate(" + this.settings.axis.y.padding + ",0)"
            })
            .style({
                fill: "#777777"
            })
            .selectAll("text")
            .style({
                "text-anchor": "end",
                "font-size": "x-small"
            });


        let bars = this.barGroup
            .selectAll(".bar")            //keep an eye on this. was '.lines'
            .data(viewModel.dataPoints);
        bars.enter()
            .append("svg")
            .classed("bar", true);    //this was chart
        bars
            .attr({
                //width: xScale.range(),
                height: d => height = yScale(d.value) - xAxisPadding,
                x: d => xScale(d.duration),

            })
            .style({
                fill: d => d.colour,
                "fill-opacity": d => viewModel.highlights ? d.highlighted ? 1.0 : 0.5 : 1.0
            })
            .on("click", (d) => {
                this.selectionManager
                    .select(d.identity, true)
                    .then(ids => {
                        bars.style({
                            "fill-opacity": ids.length > 0 ?
                                d => ids.indexOf(d.identity) >= 0 ? 1.0 : 0.5
                                : 1.0


                        });

                    })

            });
        bars.exit()
            .remove();




    }
    /* private updateSettings(options: VisualUpdateOptions) {
         this.settings.axis.x.show.value = DataViewObjects.getValue
             (options.dataViews[0].metadata.objects, {
                 objectName: "xAxis",
                 propertyName: "show"
         })
     }*/

    private getViewModel(options: VisualUpdateOptions): ViewModel {
        let dv = options.dataViews;
        let viewModel: ViewModel = {
            dataPoints: [],
            maxValue: 0,
            highlights: false

        };

        /* if (!dv
             || !dv[0]
             || !dv[0].categorical
             || !dv[0].categorical.categories
             || !dv[0].categorical.categories[0].source
             || !dv[0].categorical.values)
             return viewModel;*/

        let view = dv[0].categorical;

        let categories = view.categories[0];
        let values = view.values[0];
        let highlights = values.highlights;
        for (let i = 0, len = Math.max(categories.values.length, values.values.length); i < len; i++) {
            viewModel.dataPoints.push({
                duration: <number>values.values[i],
                value: <number>values.values[i],
                details: <number>categories.values[i],
                wells: <string>categories.values[i],
                colour: this.host.colorPalette.getColor(<string>categories.values[i]).value,
                identity: this.host.createSelectionIdBuilder()
                    .withCategory(categories, i)
                    .createSelectionId(),
                highlighted: highlights ? highlights[i] ? true : false : false

            });
        }

        viewModel.maxValue = d3.max(viewModel.dataPoints, d => d.value);
        viewModel.highlights = viewModel.dataPoints.filter(d => d.highlighted).length > 0;
        return viewModel;

    }



    public enumerateObjectInstances(options: EnumerateVisualObjectInstancesOptions): VisualObjectInstance[] | VisualObjectInstanceEnumerationObject {
        let propertyGroupName = options.objectName;
        let properties: VisualObjectInstance[] = [];
        switch (propertyGroupName) {
            case "xAxisGroup":
                properties.push({
                    objectName: propertyGroupName,
                    properties: {
                        show: this.settings.axis.x.show.value
                    },
                    selector: null
                });
                break;

        };
        return properties
    }



}

For anyone curious, my end goal (eventually) is to have a line plot that can support a measure (calculated column) in either axis. To give an example, dates are very common for the X axis. I would also like to be able to use a measure that can take a date range such as 9/5/2019-9/10/2019 and convert that to a duration. Output expected is 1-5.

The Y axis measure may do something like.. display all values that meet certain parameters, for instance.

{

  "supportsHighlight": true,
  "dataRoles": [
    {
      "displayName": "Y Axis",
      "name": "values",
      "kind": "Grouping"
    },
    {
      "displayName": "Details",
      "name": "details",
      "kind": "Grouping"
    },
    {
      "displayName": "Duration",
      "name": "duration",
      "kind": "Measure"
    }

  ],
  "objects": {

    "xAxis": {
      "displayName": "X Axis",
      "properties": {
        "show": {
          "displayName": "Show X Axis",
          "type": {
            "bool": true
          }

        }
      }
    },
    "dataPoint": {
      "displayName": "Data colors",
      "properties": {
        "defaultColor": {
          "displayName": "Default color",
          "type": {
            "fill": {
              "solid": {
                "color": true
              }
            }
          }
        },
        "showAllDataPoints": {
          "displayName": "Show all",
          "type": {
            "bool": true
          }
        },
        "fill": {
          "displayName": "Fill",
          "type": {
            "fill": {
              "solid": {
                "color": true
              }
            }
          }
        },
        "fillRule": {
          "displayName": "Color saturation",
          "type": {
            "fill": {}
          }
        },
        "fontSize": {
          "displayName": "Text Size",
          "type": {
            "formatting": {
              "fontSize": true
            }
          }
        }
      }
    }
  },
  "dataViewMappings": [
    {
      "categorical": {
        "categories": {
          "for": {
            "in": "duration"
          },
          "dataReductionAlgorithm": {
            "top": {
              "count": 450
            }
          }
        },
        "values": {
          "group": {
            "by": "values",
            "select": [
              {
                "bind": {
                  "to": "details"
                }
              }
            ]
          },
          "dataReductionAlgorithm": {
            "top": {
              "count": 450
            }
          }
        }
      }
    }

  ]
}
Fehnraal
  • 40
  • 5
  • Not able to comment on the visual logic, but that is not how measures work. A measure **always** returns a scalar value. So if you create a visual with a measure on the y axis and a measure on the x axis and nothing else, the query generated would return a 1-row table of two values, [y-axis-measure] and [x-axis-measure]. To have a larger resultset, it would be necessary to put some column on the axis, against which the measure would be evaluated (once per value in the column). – greggyb Sep 05 '19 at 16:47
  • I should have reiterated above.. These 2 measures rely on a grouping or 'index' of date and time to function properly. I have gotten this to work in Python, but typescript is another beast all together. Each point in Y has timestamp data for its value. X will receive a data point for each Y data in every date/time in my data, and will calculate time between data points using the date-range thing mentioned earlier. Y will be a measure that takes numerical data (sales for example) and normalizes it between categories. – Fehnraal Sep 05 '19 at 16:59
  • Ah, gotcha. That makes sense. So basically you want a scatterplot that can dynamically re-label the date on the axis? E.g. You'd plot a date on the x axis, and a measure that does some logic to reformat that date, and display the result of the measure as the label, rather than the value of the date from the column? – greggyb Sep 05 '19 at 17:02
  • Yes, this will function very similarly to a scatterplot. Nearly identically, I guess. Just with lines. You're spot on. – Fehnraal Sep 05 '19 at 17:03
  • I wish I knew a way to explain this better without making it sound super complicated. But you got what I'm saying, so you're either pretty bright or my phrasing isn't as bad as I think.. lol so in the way scatter plots have a "details" field, this will function similarly. That way I can set my code to use these "details" as an index, causing it to plot a point for each value, at each time of each day, and have my X axis measure take those days and times, and turn them into a day- range. – Fehnraal Sep 05 '19 at 17:06
  • Do you have a mockup of what you're trying to achieve? I feel like it might be possible to just do it with a line chart and some DAX trickery. – greggyb Sep 05 '19 at 17:12
  • I do, but unfortunately its private data. I can simplify this significantly, though. The Y axis measure is entirely optional. This can be replaced by finding a way to invert the data on the Y axis in the visual code without using DAX to multiply the values by -1. example, a Y axis starting from 1, and going to 10 would need to be converted to 10, going to 1. without the -1, -2, -3, etc. Give me a moment and I will 'anonymize' a visual I created in Python to serve this purpose. It may also be worth nothing that I am unable to modify the data imported, apart from DAX measures. – Fehnraal Sep 05 '19 at 17:19
  • I have prepared something for you. It's messy, but it paints the picture for you. [link]https://imgur.com/a/PTqTgYM @greggyb – Fehnraal Sep 05 '19 at 17:29
  • Having a measure in the Y axis is merely a bonus for me. The only -real- requirement is that I draw these data points in reverse without transforming them to negative data. – Fehnraal Sep 05 '19 at 17:31
  • Well, not having a negative y axis is already going to make this a no-go with just DAX. Can you just use a Python or R visual in the report canvas instead? That would likely be easier. – greggyb Sep 05 '19 at 17:37
  • Yes, and no. I have experimented with Python a lot, and Power Bi does not currently allow interactions with Python visuals. For example, clicking a line in a normal PBI line chart would use that visual as a filter. Doing so in a Python visual does nothing. Its essentially a static image. R, however, may be interactive. I wish I could use R for this purpose, but my circumstances do not allow me to use R. This visual is very purpose-built and the person who has assigned this is very against R. Claims it to be clunky and slow. I don't have experience with R, so I cannot confirm nor deny. – Fehnraal Sep 05 '19 at 18:16
  • R visuals are equivalent to Python visuals regarding interactivity. – greggyb Sep 05 '19 at 18:20
  • That's unfortunate. It looks like my only real option here is typescript as I have been doing. I'm hoping Microsoft implements interactivity for their in-client script visuals. I'm much more familiar with Python (it's also a piece of cake.) Right now, Microsoft is focused on the data manipulation and migration aspects of Python. Which are admittedly, more pressing. – Fehnraal Sep 05 '19 at 18:26
  • Yeah, sorry to not have a way out for you. Custom visuals are perennially on my to-learn list, but I haven't gotten around to it yet – greggyb Sep 05 '19 at 18:30
  • No worries. I'm sure I'll shake some bugs out of this code eventually. I'll certainly be switching over to Python as soon as it becomes interactive though. Much less of a headache, and much more user friendly. Python also has a massive presence online when it comes to Q&A. Typescript is a bit harder to 'socially debug'. Creating visuals is definitely rewarding when the actually function. There's a 1,000,001 ways to make it your own without being forced to look at the default layouts and stylings you're accustomed to seeing. – Fehnraal Sep 05 '19 at 18:37
  • Could you share your source of the visual in github or capabilities.json file? Are errors during execution occur? – Ilfat Galiev Sep 08 '19 at 19:45
  • @IlfatGaliev I added capabilities.json to the post. The github link wouldn't help much, as I went a different direction with the visual than the author did. [link]https://github.com/JorgeCandeias/Power-BI-Custom-Visual-Fundamentals Here is the link to his repo. His has much more detail than mine does. I will not be using it as a bar chart, I will be converting it to line chart eventually. – Fehnraal Sep 09 '19 at 11:03
  • There are no errors. Occasionally I get an info icon that says I have too much data, though. – Fehnraal Sep 09 '19 at 16:17

1 Answers1

0

Based on the comments, part of the challenge is getting a y-axis that plots the smallest number at the top and the largest number at the bottom. This is do-able with a custom format-string for a measure. Here is a quick and dirty mockup using a standard line chart.

If you set the measure format to Custom and use a format string without a '-' for negative, e.g. "0;0;0", the visual will not display a '-' in the y-axis. This leads to the effect you're looking for.

Note below that the measure is -1 * SUM ( 'Table'[Column1] ). That column contains only positive values.

enter image description here

enter image description here

greggyb
  • 3,728
  • 1
  • 11
  • 32