Skip to main content
cancel
Showing results for 
Search instead for 
Did you mean: 

Earn the coveted Fabric Analytics Engineer certification. 100% off your exam for a limited time only!

Reply
Anonymous
Not applicable

Vega-lite visual in power bi - interactivity how to approach?

Hi,

 

I have created a scatter chart using vega-lite using https://github.com/microsoft/vegalite-for-powerbi. I want to add interaction with other visuals. I want to use the selectionId and selectionmanager described here https://docs.microsoft.com/en-us/power-bi/developer/visuals/selection-api). I use transform in vega-lite, and so far I understood, this makes the data unkown to PowerBI. How should I approach this? Especailly giving selectionID to the data points, because this is getting useless after compiling vega declerations. Any Hints suggestions appreciated.

 

br

1 ACCEPTED SOLUTION

Thanks, @Anonymous - I'm unfortunately not in a position to be able to replicate locally, but I have read through and I'll offer some thoughts and hopefully they may be able to help you.

From my understanding, the layer that you want to have support selection is the one containing the point mark? I notice that you have layers and marks that I presume are used to simulate mouse tracking, similar to this example, so just want to be sure.

I think that the primary issue will indeed be the fold transform. It's challenging to get an answer 100% right without the 'feel' of the data, and I presume that you can't share this. However a key thing to note here is that anything that changes the shape of the data is going to (a) cause Vega-Lite to discard any fields that don't match the transform (i.e. selection ID), and (b) cause a difference between what Power BI knows about the data view it gives you vs. the data that is now in the Vega-Lite spec.

Selection IDs are essentially pointers to the data view provided to the visual by Power BI, so row context is important to consider (and preserve).

This row context is the only shared knowledge between your visual and the main window, and Power BI works out everything on the pointer(s) you give it, so it's important to maintain a 1:1 mapping of data in any Vega-Lite layers or marks that you want to have interactivity around.

I have tried to explain this to the non-developer here as part of my visual's documentation. I don't know if this provides any further context for you around the concept; I have yet to write the section on selection/cross-filtering as I'm still developing the functionality, but it essentially works as best it can for the flexibility that Vega-Lite offers vs. the contracts we have to maintain with Power BI to ensure that we can signal back somethng sensible to the main window.

Much like a number of DAX solutions work better inside Power BI with the right model, it might be better to look at the data view mapping for your visual to see if you can get this to match the intended layer's granularity without having to use the fold transform. If this is not possible, then it might be very challenging to achieve, unless you can generate selection IDs dynamically when points are clicked (which is certainly possible, but again you need to bear in mind that Power BI assumes any selection ID will match the grain of the data view).

One other thing: the click handling as it stands is probably fine, at least for now 🙂 However, rather than using d3 to select the elements and bind interactivity that way, Vega-Lite's event listeners (if bound to a view) can expose an underlying datum's info if it is clicked. If you bind a 'click' event and suitable handling function when constructing your view, the item object passed into the bound function will have a datum property that contains everything that Vega-Lite has for the clicked target; you would ideally want this to contain your Selection ID. This also leaves the management of events to Vega-Lite so any clean-up and management is easier and reduces the amount of code you need to write (and may remove the need for D3 in your visual).

I hope that some of this helps you out, but feel free to ask anything more specific and I'll try to answer as and when I can.

Daniel





Did I answer your question? Mark my post as a solution!

Proud to be a Super User!


My course: Introduction to Developing Power BI Visuals


On how to ask a technical question, if you really want an answer (courtesy of SQLBI)




View solution in original post

4 REPLIES 4
dm-p
Super User
Super User

If you're using transforms in Vega-Lite, this will indeed often strip away a lot of the additional data that your dataset might have, so if you're binding a Selection ID to your initial dataset then this can get removed by the interpreter.

Can you provide more details as to what transform you are doing within Vega-Lite - possibly your spec (full code would be better) and a sample of your data?

Have you also considered doing the transformation on your dataset in TypeScript prior to passing to the Vega View API? Is there something specific to Vega that you need?

If you can pre-process this dataset before passing it to the view API, it may be a way of avoiding Vega stripping it out. If you can keep your dataset as 'pure' as possible from Power BI to Vega, you will have a much better time trying to get interactivity to work and keeping state in sync. For instance, due to how the selection manager works and how Power BI delegates the detail around this rather than your visual, it's very tricky to use functionality such as Vega-Lite's params to manage this.

In Deneb, we'll be implementing this in the Vega view with an EventListener and the visual logic, rather than a SignalListener and Vega-Lite's internal store. This will manage the selection state at the dataset level rather than managing state inside Vega-Lite and is more in-line with how you'd manage it in a custom visual that you'd build from scratch (tracking selection using a boolean value against a datum). This way, you can get the Selection ID from the clicked datum and use your visual logic to manage state for the selection manager, and pass back to the View API. You can then use an encoding on this "selected" field to handle the visual effects.

Not sure if this gives you any further ideas, but I've spent a lot of time in this area and might save you going down a few dead ends that I had to explore 🙂

Good luck!

Daniel





Did I answer your question? Mark my post as a solution!

Proud to be a Super User!


My course: Introduction to Developing Power BI Visuals


On how to ask a technical question, if you really want an answer (courtesy of SQLBI)




Anonymous
Not applicable

Hey Daniel,

 

thank you for you quick and detailed answer.

 

This is my Vega declaration.

 

 

 

 

    const spec: vl.TopLevelSpec = {
      $schema: "https://vega.github.io/schema/vega-lite/v5.json",
      description: "",

      width: "container",
      height: "container",

      config: {
        autosize: { type: "fit", contains: "padding" },
        axis: { domain: true, ticks: false },
        style: {
          "guide-label": { fontSize: 12, fill: "#000000" },
          "guide-title": { fontSize: 14, fill: "#000000" },
        },
        background: null,
        padding: { left: 10, top: 5, right: 10, bottom: 5 },
      },

      data: {
        name: "dataset",
        values: dataPoints as Dict<any>,
      },

      // encoding: {},
      encoding: {
        x: {
          field: "x2",
          type: "temporal",
          axis: { grid: false, title: "", orient: "bottom", labels: false },
        },
      },

      layer: [
        {
          mark: { type: "text", angle: -90, color: "#000000", align: "left" },
          encoding: {
            x: {
              field: "x2",
              type: "temporal",
              timeUnit: { unit: "utcyearmonthdatehoursminutes" },
              axis: {
                grid: false,
                title: "",
                orient: "top",
                labelAngle: -45,
                labels: false,
                labelAlign: "left",
                // labelExpr:"[timeFormat(datum.value, '%b %d %Y'), timeFormat(datum.value, '%H:%M')]",
              },
            },
            text: { field: "bandnr" },
            y: { value: -10 },
            color: {
              condition: [
                { test: "datum['sortmst'] == 'SM5'", value: "green" },
                { test: "datum['sortmst'] == 'SM8'", value: "steelblue" },
              ],
              value: "black",
            },
          },
        },
        // Show steelgrade at the bottom x-axis
        {
          mark: { type: "text", color: "#000000", align: "right", angle: -90 },
          encoding: {
            x: {
              field: "x2",
              type: "temporal",
              timeUnit: { unit: "utcyearmonthdatehoursminutes" },
              axis: {
                grid: false,
                title: "",
                orient: "bottom",
                labelAngle: -45,
                labels: false,
              },
            },
            text: { field: "steelgrade" },
            y: { value: "height" },
          },
        },
        // Vertical rule combined with tooltip information
        {
          params: [
            {
              name: "hover",
              select: {
                type: "point",
                encodings: ["x"],
                nearest: true,
                on: "mouseover",
              },
            },
          ],
          mark: { type: "rule", color: "gray" },
          encoding: {
            opacity: {
              condition: {
                param: "hover",
                value: 1,
                empty: false,
              },
              value: 0,
            },
            tooltip: [
              { field: "xAlone", type: "ordinal", title: xTitle },
              { field: "sortmst", type: "ordinal", title: xTitle },
              { field: "xAlone", type: "ordinal", title: xTitle },
              {
                field: "bottomXSteelgrade",
                type: "ordinal",
                title: bottomXTitleSteelgrade,
              },
              {
                field: "x2",
                timeUnit: "utcyearmonthdatehoursminutes",
                type: "temporal",
                title: "Zeit",
              },
              {
                field: "dataField1",
                type: "quantitative",
                title: dataField1Title,
                format: ".2f",
              },
              { field: "vonLaenge", type: "ordinal", title: vonLaengeTitle },
              { field: "bisLaenge", type: "ordinal", title: bisLaengeTitle },
              { field: "tooltip3", type: "ordinal", title: tooltip3Title },
              { field: "tooltip4", type: "ordinal", title: tooltip4Title },
            ],
          },
        },
        {
          transform: [{ fold: [dataField1Title], as: ["measure", "temp"] }],
          params: [
            {
              name: "pts",
              select: { type: "point", on: "click" },
            },
          ],
          mark: { type: "point", size: 10, fill: "#1b9e77", stroke: null },
          encoding: {
            x: { field: "x2", type: "temporal", axis: null },
            y: {
              field: "dataField1",
              type: "quantitative",
              axis: { labels: true, title: null },
              scale: {
                zero: false,
                domain: [this.localMinValue, this.localMaxVAlue],
              },
            },
            color: {
              field: "measure",
              type: "nominal",
              legend: { title: null, orient: "right", symbolStrokeWidth: 0 },
            },
            opacity: {
              condition: {
                param: "pts",
                value: 1,
              },
              value: 0.05,
            },
          },
        },
      ],
      resolve: { scale: { x: "independent", color: "independent" } },
    };


    const vgSpec = vl.compile(spec).spec;

    console.log("vgSpec: ", vgSpec);

    var tooltip = new Handler();
    const runtime = vega.parse(vgSpec);

    console.log("runtime: ", runtime);

    this.view = new vega.View(runtime)
      .logLevel(vega.Warn)
      .initialize(this.target)
      .renderer(this.settings.rendering.svg ? "svg" : "canvas")
      .tooltip(tooltip.call)
      .run();

    this.view.run();

 

 

 

and this are my data points whre I have the selectionID

 

 

export interface DataPoint {
  x: string;
  xAlone: string;
  x2: string;
  // Steelgrade
  bottomXSteelgrade: string;

  sortmst: string;
  bandnr: string;
  steelgrade: string;

  vonLaenge: string;
  bisLaenge: string;
  tooltip3: string;
  tooltip4: string;

  dataField1: number;
  dataField2: number;
  dataField3: number;
  dataField4: number;

  selection: ISelectionId;
}

 

 

 

and I create them here:

 

 

 

      for (
        let i = 0, len = Math.max(x.values.length, dataField1.values.length);
        i < len;
        i++
      ) {
        const categoryValue: powerbi.PrimitiveValue = dataField1.values[i]; //categories[0].values[i];

        const categorySelectionId: ISelectionId = host
          .createSelectionIdBuilder()
          .withCategory(category, parseInt("" + dataField1.values[i])) // we have only one category (only one `Manufacturer` column)
          .createSelectionId();

        console.log("SelectionID: ");
        console.log(categorySelectionId);

        dataPoints.push({
          x: x.values[i] + "/" + sortmst.values[i],
          xAlone: x.values[i] + "",
          x2: bottomX.values[i] + "",
          bottomXSteelgrade: bottomXSteelgrade.values[i] + "",

          sortmst: sortmst ? sortmst.values[i] + "" : "",
          bandnr: x.values[i] + "",
          steelgrade: bottomXSteelgrade.values[i] + "",
          vonLaenge: vonLaenge.values[i] + "",
          bisLaenge: bisLaenge.values[i] + "",
          tooltip3: tooltip3.values[i] + "",
          tooltip4: tooltip4.values[i] + "",

          dataField1: dataField1 ? +dataField1.values[i] : 0,
          dataField2: dataField2 ? +dataField2.values[i] : 0,
          dataField3: dataField3 ? +dataField3.values[i] : 0,
          dataField4: dataField4 ? +dataField4.values[i] : 0,

          selection: categorySelectionId,
        });
      }

 

 

 

and this is my update function

 

 

 

    let dataElements: any;
      this.settings = Visual.parseSettings(
        options && options.dataViews && options.dataViews[0]
      );

      if (
        this.view &&
        (options.type & VisualUpdateType.Resize ||
          options.type & VisualUpdateType.ResizeEnd)
      ) {
        this.view
          .width(options.viewport.width)
          .height(options.viewport.height)
          .run();
        return;
      }

      dataElements = this.visualTransform(
        options,
        this.selectionIdBuilder,
        this.target,
        this.host
      );

      if (!dataElements) {
        this.target.innerHTML = "Need category and measure";
        this.view = null;
        return;
      }

      let width: number = options.viewport.width;
      let height: number = options.viewport.height;

      let wrapThis = this;

      this.r(function (target, interactivity, behavior, svg) {
        setTimeout(function () {
          console.log("Dom Ready");

          this.pointSelection = d3Select("body")
            .selectAll("path")
            .data(dataElements);

          d3Select("body")
            .selectAll("path")
            .on("click", (d) => {

               console.log("Click!", d);
               wrapThis.selectionManager.select(d, false);
            });

       }, 5000);

     });

      this.visualizeElements(options, dataElements);

 

 

 

I am not sure if it was good idea to put the click function into update :).

 

br

 

 

Thanks, @Anonymous - I'm unfortunately not in a position to be able to replicate locally, but I have read through and I'll offer some thoughts and hopefully they may be able to help you.

From my understanding, the layer that you want to have support selection is the one containing the point mark? I notice that you have layers and marks that I presume are used to simulate mouse tracking, similar to this example, so just want to be sure.

I think that the primary issue will indeed be the fold transform. It's challenging to get an answer 100% right without the 'feel' of the data, and I presume that you can't share this. However a key thing to note here is that anything that changes the shape of the data is going to (a) cause Vega-Lite to discard any fields that don't match the transform (i.e. selection ID), and (b) cause a difference between what Power BI knows about the data view it gives you vs. the data that is now in the Vega-Lite spec.

Selection IDs are essentially pointers to the data view provided to the visual by Power BI, so row context is important to consider (and preserve).

This row context is the only shared knowledge between your visual and the main window, and Power BI works out everything on the pointer(s) you give it, so it's important to maintain a 1:1 mapping of data in any Vega-Lite layers or marks that you want to have interactivity around.

I have tried to explain this to the non-developer here as part of my visual's documentation. I don't know if this provides any further context for you around the concept; I have yet to write the section on selection/cross-filtering as I'm still developing the functionality, but it essentially works as best it can for the flexibility that Vega-Lite offers vs. the contracts we have to maintain with Power BI to ensure that we can signal back somethng sensible to the main window.

Much like a number of DAX solutions work better inside Power BI with the right model, it might be better to look at the data view mapping for your visual to see if you can get this to match the intended layer's granularity without having to use the fold transform. If this is not possible, then it might be very challenging to achieve, unless you can generate selection IDs dynamically when points are clicked (which is certainly possible, but again you need to bear in mind that Power BI assumes any selection ID will match the grain of the data view).

One other thing: the click handling as it stands is probably fine, at least for now 🙂 However, rather than using d3 to select the elements and bind interactivity that way, Vega-Lite's event listeners (if bound to a view) can expose an underlying datum's info if it is clicked. If you bind a 'click' event and suitable handling function when constructing your view, the item object passed into the bound function will have a datum property that contains everything that Vega-Lite has for the clicked target; you would ideally want this to contain your Selection ID. This also leaves the management of events to Vega-Lite so any clean-up and management is easier and reduces the amount of code you need to write (and may remove the need for D3 in your visual).

I hope that some of this helps you out, but feel free to ask anything more specific and I'll try to answer as and when I can.

Daniel





Did I answer your question? Mark my post as a solution!

Proud to be a Super User!


My course: Introduction to Developing Power BI Visuals


On how to ask a technical question, if you really want an answer (courtesy of SQLBI)




Anonymous
Not applicable

Hi Daniel,

 

thank you for your detailed reply. Always getting great answers from you :). I will try to process the data without transforming it. Unfortunately in the mean time the project requirements have changed and interactivity has most least priority 😕 now.

 

br

Helpful resources

Announcements
April AMA free

Microsoft Fabric AMA Livestream

Join us Tuesday, April 09, 9:00 – 10:00 AM PST for a live, expert-led Q&A session on all things Microsoft Fabric!

March Fabric Community Update

Fabric Community Update - March 2024

Find out what's new and trending in the Fabric Community.