Objetos personalizados en Power Bi

Taller: Introducción a los objetos visuales personalizados de PBI.

Peter Von Oostznanen

archivado en: JavaScript / 3 enero, 2021

Llevo siglos sin actualizar este blog por falta de tiempo, pero este año voy a tratar de escribir por lo menos una entrada al mes. Y comienzo con un tutorial sobre objetos personalizados en Power Bi, que solo resultará interesante si tienes que trabajar con esta herramienta.

Repositorio con el código y el material del taller

1. Introducción

Power Bi (PBI) es una herramienta de Business Intelligence desarrollada por Microsoft que permite enlazar distintas fuentes de datos -como una bbdd o unas tablas excel- para elaborar informes visuales mediante una interfaz gráfica.

Los informes se componen de objetos visuales, widgets que representan los datos. PBI viene con muchos objetos de fábrica y se pueden incorporar más desde el AppSource, pero también se pueden desarrollar desde cero si ninguno se adapta a nuestras necesidades.

Para desarrollar un objeto visual conviene conocer antes:

  1. TypeScript
  2. Técnicas básicas de manipulación del DOM (como appendChild)
  3. D3

Las herramientas recomendables son:

  1. Visual Studio Code
  2. Power Shell

Además, hay que tener una cuenta Power Bi Pro. Se puede probar una gratuita durante 60 días. Dados de alta en PBI pro, hay que configurar el entorno de desarrollo siguiendo las instrucciones indicadas en "Configuración del entorno para el desarrollo de un objeto visual de Power BI". Es sencillo, basta ir siguiendo esos pasos.

Preparando el tinglado

Este taller va a consistir en realizar un objeto visual responsive que muestre los datos en forma de tabla si el visor tiene más de 480px de ancho y como fichas (cards) si es menor.

La fuente de datos va a ser un documento excel con datos de los planetas del Sistema Solar, pero se puede utilizar cualquier otro recurso que tenga los datos ordenados por columnas y filas. He subido la tabla del ejemplo al repo junto al resto del código.

La forma más cómoda de preparar el tinglado es creando un área de trabajo específica. Luego le damos a nuevo > informe.

Seleccionamos "Pegar manualmente los datos", copiamos los registros de la tabla excel y los pegamos. Le damos nombre a la tabla y dejamos seleccionada la opción de usar la primera fila como cabecera. Claro está, hay muchas otras maneras de traerse los datos a un área de trabajo, pero esta es la más rápida para hacerlo desde app.powerbi.com y nos vale para esta práctica.

Nos mostrará un informe por defecto. Le damos a "Editar" y creamos una pestaña nueva donde trabajaremos nuestro objeto visual.

Ahora abrimos la consola Power Shell como administradores y nos vamos al directorio donde vayamos a desarrollar el objeto visual.

cd C:\Users\tu-nombre-de-usuario\Documents\como-sea-que-siga-la-ruta

Y ejecutamos el comando new de pbiviz seguido del nombre que le queramos poner al widget, que solo debe estar formado por letras y números, sin caracteres especiales. Por ejemplo:

pbiviz new tresponsive

3. Scaffolding

Cuando termina de hacer sus cosas, en un directorio con el nombre que hayamos especificado se encuentra el scaffolding, la estructura, del objeto visual.

Los archivos y directorios sobre los que vamos a trabajar son:

./src/visual.ts: Equivaldría al controlador de un sistema MVC. Es donde estará la lógica del objeto visual, la parte encargada de coger los datos y representarlos.

./src/settings.ts: Como veremos más adelante, esta clase sirve para trabajar con las preferencias que define el usuario en el editor.

./capabilities.json: Junto con el visual, es el archivo más importante del proyecto. Es un json donde se configuran todos los elementos del objeto visual, como la manera de traerse los datos o las preferencias de formato que tendrá el usuario.

./style\visual.less: Los estilos del objeto visual. Por defecto vienen con el preprocesador less y desconozco si se pueden pasar a sass. De todas maneras, en general, al ser un solo objeto y no toda una aplicación, importa realmente poco que preprocesador se use, ya que llevará solo unas pocas reglas.

./assets: Un directorio para ubicar los assets, como las imágenes.

./pbiviz.json: El archivo de configuración de pbiviz, donde se definen cosas como la ruta de los estilos o los assets.

El resto de la estructura es ya la misma de cualquier otro proyecto js basado en frames o librerías: node_modules, package, tslint, etcétera.

4. Hola mundo

Comenzamos a trabajar con visual.ts, que, como decía, es el controlador del objeto visual. El archivo incluye una clase llamada Visual que hereda de otra llamada IVisual.

La clase Visual cuenta, al menos, con dos métodos obligatorios y dos opcionales, pero en la práctica obligatorios.

  • constructor(): el constructor, que recibe un parámetro options con distintos datos que veremos más adelante.
  • update(): un método público que se invoca de forma automática cada vez que sucede algo, como un cambio en los datos, una interacción del usuario o el redimensionamiento del visor del informe.
  • parseSettings() y enumerateObjectInstances(), que se utilizan para trabajar con la configuración de las preferencias del usuario.

Además, se puede definir un método público opcional destroy(), el cual se invocará cuando se elimine el componente. La documentación oficial dice que no se suele utilizar, ya que Power Bi elimina todo el iframe donde va el objeto visual de golpe.

public destroy(): void {
    // do stuff
}

Vamos a quitar cosas que de momento no necesitamos. Abrimos el archivo './src/visual.ts' que trae por defecto en el install y dejamos solo el aviso legal y el código mínimo para que funcione.

En los imports:

/* Colección de polyfills y utilidades varias para trabajar con javaScript */
import 'core-js/stable';

/* los estilos */
import './../style/visual.less';

/* powerbi */
import powerbi from 'powerbi-visuals-api';

/* para tipar el constructor */
import VisualConstructorOptions = powerbi.extensibility.visual.VisualConstructorOptions;

/* para tipar el update */
import VisualUpdateOptions = powerbi.extensibility.visual.VisualUpdateOptions;

/* La clase madre */
import IVisual = powerbi.extensibility.visual.IVisual;

Borramos el resto.

Quitamos estas cuatro propiedades:

  private target: HTMLElement;
  private updateCount: number;
  private settings: VisualSettings;
  private textNode: Text;

Y de los métodos dejamos, vacíos, solo el constructor() y el update(). Es decir, nos debería quedar algo así:

// licencia...

'use strict';

import 'core-js/stable';
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;

export class Visual implements IVisual {

  constructor(options: VisualConstructorOptions) {
    console.log('Constructor', options);
  }

  public update(options: VisualUpdateOptions) {
    console.log('Update', options);
  }

}

Vamos ahora por consola en el Power Shell al directorio donde tenemos el código.

cd tresponsive

Y ejecutamos el comando start de pbiviz

pbiviz start

Cuando nos sale por consola el mensaje por consola diciendo que "Compiled successfully", volvemos al app.powerbi.com, a la pestaña donde estábamos editando el informe. Seleccionamos el "objeto visual del desarrollador" y lo arrastramos al área de trabajo.

Seleccionamos ahora el ov y marcamos la opción de "Activar la recarga automática" y ya se queda sincronizado el ov con el código que vayamos escribiendo.

Si abrimos la consola (F12), deberían aparecer el consoles.log que hemos dejado en el constructor. Si hacemos alguna acción sobre el ov, como redimensionarlo, saldrá también el del update.

Con esto tendríamos nuestro Hola mundo en pbiviziano, pero si queremos hacer algo un poco más sofisticado, podemos ir al constructor, un método que solo se llamará una vez, cuando se cree la instancia del ov y ahí poner algo que no necesita irse actualizando, como un subtitular.

Para eso usaremos el método js appendChild, ya que otros métodos que atacan directamente al DOM, como innerHTML están prohibidos por seguridad. Y para poder enganchar este nodo usaremos el parámetro element que viene entre las options, que nos dice cuál es el nodo padre del ov, el que lo contiene. Además, vamos a setearlo a una propiedad de la clase que podemos llamar target o whatever para manejarlo en otros métodos.

Algo así:

// ...

export class Visual implements IVisual {
  /* Aquí setearemos el contenedor principal del objeto visual*/
  private target: HTMLElement;

  constructor(options: VisualConstructorOptions) {
    console.log('Constructor', options);

    /* Seteamos el nodo contendor */
    this.target = options.element;

    /* Enganchamos un hola mundo */
    if (document) {
      /* Creamos el elemento, una cabecera */
      const newHeader: HTMLElement = document.createElement('h3');
      /* Insertamos un nodo de texto en la cabecera anterior */
      newHeader.appendChild(document.createTextNode('Práctica pbiviz: Hola Mundo'));
      /* Insertamos la cabecera en el target */
      this.target.appendChild(newHeader);
    }
  }


// ...

Y ahora nuestro ov debería mostrarse de forma parecida a esta.

5. Manejando datos

Ahora toca indicarle a PBI cómo queremos que nos pase los datos que va a consumir el ov y eso se hace en el capabilities.json, donde irán todas las configuraciones relacionadas con el editor de informes.

5.1. Data Roles

Abrimos el archivo y borramos todas las claves del json menos "dataRoles".

{
  "dataRoles": [
    {
      "displayName": "Category Data",
      "name": "category",
      "kind": "Grouping"
    },
    {
      "displayName": "Measure Data",
      "name": "measure",
      "kind": "Measure"
    }
  ]
}

Estos data roles son los "contenedores" de los campos que definimos en el editor de informes.

En un data rol podemos definir las siguientes claves:

  • displayName: el nombre que aparecerá en el editor visual.
  • name: el nombre normalizado que usaremos internamente en el código.
  • kind: el tipo que es. Hay tres tipos posibles:
    • Grouping: son los valores que se usan para agrupar los campos de medida, las cabeceras de una tabla para entendernos.
    • Measure: los valores, el equivalente a las filas en el ejemplo anterior.
    • GroupingOrMeasure: una combinación de los dos anteriores.
  • description: opcional, una descripción breve del campo.
  • requiredTypes: opcional, el tipo de datos que necesariamente debe recibir ese rol.
  • preferredTypes: opcional, el tipo de datos que convendría utilizar en ese rol.

Tanto los requiredTypes como los preferredTypes pueden ser:

  • bool: booleanos.
  • integer: números enteros.
  • numeric: valor numérico.
  • text: literales.
  • geography: datos geográficos, que aún no sé muy bien cómo van.

En nuestra tabla responsive vamos a querer una cabecera, un grouping, que sean literales, y un rol tipo measure que sean los n datos que agrupa el anterior.

Por lo que podríamos definir el dataRoles más o menos así:

{
  "dataRoles": [
    {
      "displayName": "Category",
      "name": "category",
      "kind": "Grouping",
      "description": "Headers table",
      "requiredTypes": [
        {
          "text": true
        }
      ]
    },
    {
      "displayName": "Measures",
      "name": "measure",
      "kind": "Measure",
      "description": "Rows table"
    }
  ]
}

5.2. Mapeos

Una vez definidos los roles, hay que indicar cómo se relacionan entre sí y para eso vamos a añadir otra clave al capabilities.json llamada dataViewMappings, que es un array donde indicamos los distintos tipos de asignaciones de datos, de formatos, que vamos a querer.

Hay 5 tipos:

  • categorical
  • single
  • table
  • matrix

De momento vamos a usar la categorical, en la que se define para una referencia (for), los headers de nuestra tabla, lo que hemos llamado en los roles category qué valores le corresponden (values), lo que hemos denominando measure.

"dataRoles": [...],
"dataViewMappings": [
  {
    "categorical": {
      "categories": {
        "for": {
          "in": "category"
        }
      },
      "values": {
        "select": [
          {
            "bind": {
              "to": "measure"
            }
          }
        ]
      }
    }
  }
]

Definido esto, si refrescamos el ov, en el editor nos deberían aparecer los data-roles.En category, nos llevamos el campo planetas y en measure, el resto.

6. Pintar los datos

Ahora que ya tenemos listo un canal de comunicación con los datos, toca comenzar a pintarlos : ).

6.1. Recuperar los datos

Podemos ver cómo están llegando los datos añadiendo un console.log en el método update, que recordemos se autoinvoca cada vez que sucede algo.

  public update(options: VisualUpdateOptions) {
    console.log('Update', options);
  }

O, en el editor, seleccionando el ov y marcando la opción del menú contextual "Mostrar vista de datos".

La parte que nos interesa ahora se encuentra en dataViews, donde vienen todos los datos mapeados, en la clave categorical, que recordemos es la que definimos en el capabilities.json.

Veamos primero el path para recuperar las cabeceras de la tabla -el dataRol que definimos con el nombre category del tipo grouping-, las cuales están definidas en options.dataViews[0].categorical.categories[0]. Ahí tendremos:

  • El nombre: options.dataViews[0].categorical.categories[0].source.displayName (string)
  • Los valores: dataViews[0].categories[0].source.displayName.values (array)

Además, podemos confirmar que se corresponden a la clave que hemos definido como "categories" consultando options.dataViews[0].categorical.categories[0].category, que es un booleano.

Así que podríamos hacer algo así:

1). Preparamos una propiedad privada donde iremos almacenando los datos mapeados como lo vamos a necesitar. La podemos llamar viewModel o whatever y de momento solo va a tener dos valores, el nombre de la cabecera y sus valores.

/* Aquí tendremos los datos mapeados como los necesitamos */
private viewModel = {
  headersName: '',
  headersValue: []
};

Y luego, aunque lo podríamos hacer directamente en el update, por aquello de ser más ordenaditos, vamos a crear un método auxiliar aparte que nos recupere las cabeceras.

public update(options: VisualUpdateOptions) {    
  this.vmGetHeaders(options);
  console.log('Update', options);
}

private vmGetHeaders(options: VisualUpdateOptions) {
  this.viewModel.headersName = options.dataViews[0].categorical.categories[0].source.displayName
  this.viewModel.headersValue = options.dataViews[0].categorical.categories[0].values;
}

Y con esto ya tendríamos en teoría recuperados los encabezados.

Sin embargo, es una solución fea, ya que depende de conocer de antemano la estructura del objeto. Lo suyo sería, en cambio, que fuera transparente, para lo que deberíamos recorrer el objeto hasta encontrar lo que buscamos. Y lo bueno es que pbi cuenta con unas clases auxiliares que nos ahorrarán este trabajo.

La que nos interesa ahora se llama dataRoleHelper, concretamente el método getCategoryIndexOfRole que devuelve el índice del dataRol que se especifica por nombre.

Añadimos la clase a los imports.

import { dataRoleHelper } from 'powerbi-visuals-utils-dataviewutils';

Y refactorizamos nuestro método para recuperar la cabecera.

private vmGetHeaders(options: VisualUpdateOptions) {
  const indexHeader = dataRoleHelper.getCategoryIndexOfRole(options.dataViews[0].categorical.categories, 'category');
  this.viewModel.headersName = options.dataViews[0].categorical.categories[indexHeader].source.displayName
  this.viewModel.headersValue = options.dataViews[0].categorical.categories[indexHeader].values;
}

Vamos ahora con las filas. Lo que nos interesa es convertir los datos que llegan en una estructura de este tipo, que luego es muy fácil de manejar para pintar la vista.

[
  {
  categoryName: string
  categoryValues: [...]
  }
]

Si desglosamos options, vemos que los datos que vamos a necesitar se encuentran en values.

Por lo que solo debemos recorrer ese array…

  private vmGetRows(options: VisualUpdateOptions) {
    const len = options.dataViews[0].categorical.values.length;
    let i = 0;

    for (; i < len; i++) {
      let objTemp = {
        categoryName: options.dataViews[0].categorical.values[i].source.displayName,
        categoryValues: options.dataViews[0].categorical.values[i].values
      }
      this.viewModel.rows.push(objTemp);
    }
  }

Y ahora ya sí tenemos todos los datos listos para pintar la tabla. Vamos con eso.

6.2. Pintando la tabla

La manera más cómoda de manipular el DOM con javScript es con el método innerHTML; sin embargo, cuando se trabaja con datos externos es muy arriesgado utilizarlo, ya que permite ataques de Cross-Site Scripting (XSS). Así que, en su lugar, trabajaremos con otros métodos más seguros, entre los que cabe destacar:

1) Para eliminar e insertar nodos:

  • Node.appendChild(): inserta un elemento al final de los contenidos del elemento padre en el que se está incluyendo.
  • Node.insertBefore(): justo al revés, lo inserta el primero.
  • Node.removeChild(): suprime un elemento.
  • Node.replaceChild(): lo reemplaza.

2) Para manipular los atributos y estilos de un elemento

  • Element.classList: una DOMTokenList con todas las clases de un elemento. Podemos aplicarle los métodos add, remove, toggle y contains.
  • Element.setAttribute(): cambia el valor de un atributo.
  • Element.getAttribute(): selecciona un atributo.
  • Element.hasAttribute(): comprueba si tiene un atributo.
  • Element.removeAttribute(): remueve un atributo.
  • Element.style: para modificar algunos estilos en línea.

Recordado esto, vamos a crear un método que pinte la tabla a partir de los datos seteados en nuestro viewModel. Algo así por ejemplo:

/* En la vida real, este método kilométrico habría que separarlo al menos en dos, 
uno para pintar la cabecera y otro las filas. Lo dejo junto sin embargo para que quede más claro */
private renderTable() {
  const len = this.viewModel.headersValue.length;
  const table = document.createElement('table');
  table.classList.add('tresponsive-table');
  table.id = 'js-main-table';

  const headerTable = document.createElement('thead');
  const rowHeader = document.createElement('tr');

  /* Añadimos una celda vacía para la columna con las categorías */
  let cellHeader = document.createElement('th');
  rowHeader.appendChild(cellHeader);

  for (let i = 0; i < len; i++) {
    cellHeader = document.createElement('th');
    cellHeader.appendChild(document.createTextNode(this.viewModel.headersValue[i]));
    rowHeader.appendChild(cellHeader);
  }

  headerTable.appendChild(rowHeader);
  table.appendChild(headerTable);

  /* En target recordemos que habíamos seteado en el constructor el contenedor principal del objeto visual*/
  this.target.appendChild(table);

  /* Vamos ahora con las filas de cada categoría */
  const bodyTable = document.createElement('tbody');
  const lem = this.viewModel.rows.length;

  for (let i = 0; i < lem; i++) {
    const rowCategory = document.createElement('tr');
    const lec = this.viewModel.rows[i].categoryValues.length;
    const cellCategorName = document.createElement('td');
    cellCategorName.appendChild(document.createTextNode(this.viewModel.rows[i].categoryName));
    rowCategory.appendChild(cellCategorName);

    for (let c = 0; c < lec; c++) {
      const cellCategory = document.createElement('td');
      cellCategory.appendChild(document.createTextNode(this.viewModel.rows[i].categoryValues));
      rowCategory.appendChild(cellCategory);
    }
    bodyTable.appendChild(rowCategory);
  }
  table.appendChild(bodyTable);
}

Con esto ya tendríamos una primera versión de la tabla.

Sin embargo, tenemos un problema y es que al utilizar el método appendChild, cada vez que se actualiza un dato se cuelga una tabla nueva, así que antes de pintar una tabla nueva tenemos que remover la antigua. Nos creamos un método para eso…

private reset() {
  /* Reseteamos el viewModel */
  this.viewModel.headersValue = [];
  this.viewModel.rows = [];

  /* Reseteamos el DOM */
  /* Lo eliminamos a la antigua, sin el remove, para que sea compatible
  con los pleistoExplorers */
  const table = document.getElementById('js-main-table');
  if (table) {
    table.parentNode.removeChild(table);
  }
}

…Y lo llamamos en el update antes de lanzar el método que pinta la tabla y los que recuperan los valores.

public update(options: VisualUpdateOptions) {
  this.reset();
  this.vmGetHeaders(options);
  this.vmGetRows(options);
  this.renderTable();
}

Por último, podemos aplicarle algunos estilos a la tabla en style\visual.less.

.tresponsive-table  {
  border-collapse: collapse;
  td {
      border: 1px solid rgb(12, 4, 83);
  }  
}

Práctica: cards

Una estrategia responsive para las tablas es convertir las filas en cards cuando el ancho del dispositivo es pequeño, que es una información que podemos recabar en el update consultando options.viewport.

Para practicar lo visto hasta ahora, se puede consultar este dato en el update y preparar un método que renderice los datos en cards si options.viewport.width es menor de 450. Además, se puede definir en el capabilities el modo table en lugar del categorical para ver cómo sería otra estructura de datos.

7. Preferencias de visualización

En general, los objetos visuales permiten que se definan distintas preferencias de visualización, de formato, al seleccionar el icono con forma de rodillo. Sin embargo, en nuestro caso aún no tenemos ninguna.

Esto es porque nos falta un parámetro clave en el capabilities: objects, donde se definen todas las opciones de formato. Lo añadimos…

...
"objects": {}
...

… Y ahora ya nos deberían aparecer las que vienen por defecto.

Ahora bien, los objetos visuales se montan en el DOM dentro un iframe.

Estas opciones por defecto solo afectan al contenedor de ese iframe. ¿Cómo podemos definir otras que se adapten a nuestras necesidades?

Para eso debemos preparar tres cosas:

  • Una definición en el capabilities.
  • La clase settings
  • Una par de métodos en visual.

Vamos con la primera utilizando como ejemplo la opción de que las filas alternen colores. Abrimos el capabilities y entre los objects añadimos una clave que se llame table. Aquí definiremos las propiedades, las opciones, de la tabla que podrá escoger el usuario. Cada opción irá en una clave con su propio nombre, en la que se definen varios parámetros como:

  • displayName: el nombre que se verá en el editor
  • description: la descripción
  • el tipo: input de texto, numérico, booleano…

En código queda más claro.

{
  "dataRoles": [...],
  "dataViewMappings": [...],
  "objects": {
    "table": {
      "displayName": "Tabla",
      "description": "Configuración de la tabla",
      "properties": {
        "alternate": {
          "displayName": "Alternar",
          "description": "Alternar colores en la tabla",
          "type": {
            "bool": true
          }
        }
      }
    }
  }
}

Luego vamos a src/visual.ts, donde se encuentra una clase que hereda la clase dataViewObjectsParser, que es una clase para trabajar con los formatos del editor.

Quitamos todo lo que viene de ejemplo para dejar la clase lista para nuestra tabla:

'use strict';

import { dataViewObjectsParser } from 'powerbi-visuals-utils-dataviewutils';
import DataViewObjectsParser = dataViewObjectsParser.DataViewObjectsParser;

export class VisualSettings extends DataViewObjectsParser {
}

Y añadimos una clase en la que setearemos los valores por defecto de nuestra configuración. Esta clase la setearemos en una propiedad pública que se debe llamar de igual manera que la clave que definimos en el object del capabilities. Algo así:

export class TableSettings {
  public alternate = false;
}

export class VisualSettings extends DataViewObjectsParser {
  public table = new TableSettings();
}

Con esto ya debería aparecernos la opción de la tabla en el editor, aunque de momento no hace nada.

Volvemos ahora al visual.ts, donde tenemos que hacer varias cosas.

Primero importamos la clase VisualSettings que acabamos de preparar, así como dos utilidades de pbi: powerbi.VisualObjectInstanceEnumeration y powerbi.EnumerateVisualObjectInstancesOptions que usaremos luego.

import { VisualSettings } from './settings';
import VisualObjectInstanceEnumeration = powerbi.VisualObjectInstanceEnumeration;
import EnumerateVisualObjectInstancesOptions = powerbi.EnumerateVisualObjectInstancesOptions;

A continuación añadimos una propiedad privada donde iremos seteando la configuración.

private visualSettings: VisualSettings;

Y añadimos este método público que usará pbi para recuperar las opciones.

public enumerateObjectInstances(options: EnumerateVisualObjectInstancesOptions): VisualObjectInstanceEnumeration {
  const settings: VisualSettings = this.visualSettings || <VisualSettings>VisualSettings.getDefault();
  return VisualSettings.enumerateObjectInstances(settings, options);
}

Ahora ya debería aparecer la opción de alternar en el editor.

Sin embargo no hace nada, ya que en el método enumerateObjectInstances le estamos indicando que seleccione las opciones por defecto, las que hemos seteado en la clase settings, si no hay ninguna definida en la propiedad privada visualSettings de Visual.

Como podemos imaginar, la solución pasa por recuperar las preferencias que va definiendo el usuario en el update, las cuales vienen -como toda la información- en el options.

Para eso creamos un método aparte, en el cual usaremos el método parse que hereda la clase VisualSettings de DataViewObjectsParser.

  public update(options: VisualUpdateOptions) {
    this.updateTableSettings(options);
    this.reset();
  ...

  private updateTableSettings(options: VisualUpdateOptions) {
    this.visualSettings = VisualSettings.parse<VisualSettings>(options.dataViews[0]);
  }
  ...

Ya solo nos falta añadir una clase css con el cebrado.

// style\visual.less
.tresponsive-table--alternate {
  tr:nth-of-type(odd) {
    background-color:#ccc;
  }
}

y añadirla en el render si alternate llega a true.

if (this.visualSettings && this.visualSettings.table && this.visualSettings.table.alternate) {
  table.classList.add('tresponsive-table--alternate')
}

Bueno, quedarían muchos temas por tratar -como los filtros o los eventos-, pero espero que este tutorial sirva de base para aprender a crear objetos visuales personalizados en Power Bi.

|| Tags: , ,

valoración de los lectores sobre Objetos personalizados en Power Bi

  • estrellica valoración positiva
  • estrellica valoración positiva
  • estrellica valoración positiva
  • estrellica valoración positiva
  • estrellica valoración positiva
  • 5 sobre 5 (1 votos)

¿Te ha parecido útil o interesante esta entrada?
dormido, valoración 1 nadapensativo, valoración 2 un poco sonrisa, valoración 3 a medias guiño, valoración 4 bastante aplauso, valoración 5 mucho

Tú opinión es muy importante, gracias por compartirla!

Los comentarios están cerrados.