Microsoft Fabric Community Conference 2025, March 31 - April 2, Las Vegas, Nevada. Use code FABINSIDER for a $400 discount.
Register nowGet inspired! Check out the entries from the Power BI DataViz World Championships preliminary rounds and give kudos to your favorites. View the vizzies.
Hello,
I have been working on a custom visual and would like to add a tooltip to it, although I can not get the option to appear in the "fields" area, to add data into it. Any help would be appreciated.
capabilities.json
{
"objects": {},
"dataRoles": [
{
"displayName": "CCFV",
"name": "category1",
"kind": "Grouping"
}
],
"tooltips": {
"supportedTypes": {
"default": true,
"canvas": true
},
"roles": [
"tooltips"
]},
"dataViewMappings": [
{
"conditions": [
{
"category1": {
"max": 1
}
}
],
"table": {
"rows": {
"for": {
"in": "category1"
}
}
}
}
]
}
Image of the editor.
visual.ts (some of it)
"use strict";
import "core-js/stable";
import "../style/visual.less";
import powerbi from "powerbi-visuals-api";
import IVisual = powerbi.extensibility.IVisual;
import VisualConstructorOptions = powerbi.extensibility.visual.VisualConstructorOptions;
import VisualUpdateOptions = powerbi.extensibility.visual.VisualUpdateOptions;
import EnumerateVisualObjectInstancesOptions = powerbi.EnumerateVisualObjectInstancesOptions;
import VisualObjectInstanceEnumeration = powerbi.VisualObjectInstanceEnumeration;
import IVisualHost = powerbi.extensibility.visual.IVisualHost;
import DataView = powerbi.DataView;
import VisualTooltipDataItem = powerbi.extensibility.VisualTooltipDataItem;
import * as d3 from "d3";
import {
createTooltipServiceWrapper,
ITooltipEventArgs,
ITooltipServiceWrapper,
} from "./tooltipServiceWrapper";
type Selection<T extends d3.BaseType> = d3.Selection<T, any, any, any>;
export class Visual implements IVisual {
private element: HTMLElement;
private isLandingPageOn: boolean;
private LandingPageRemoved: boolean;
private host: IVisualHost;
private svg: Selection<SVGElement>;
private container: Selection<SVGElement>;
private circle: Selection<SVGElement>;
private textValue: Selection<SVGElement>;
private textLabel: Selection<SVGElement>;
// Declare an array list for the line objects
private lineOffset: Selection<SVGElement>;
private arr = new Array();
private arrDots = new Array();
// Declare as array of integers to hold the segments of the circle
private arrSegments = new Array();
private arrOffsetSegments = new Array();
private singleSegment: number;
private categorys: number = 17;
private CenterarrayOffset_ = new Array();
private LandingPage: Selection<any>;
virificationArray: string[];
private tooltipServiceWrapper: ITooltipServiceWrapper;
constructor(options: VisualConstructorOptions) {
this.element = options.element;
this.svg = d3.select(options.element)
.append('svg')
.classed('circleCard', true);
this.host = options.host;
this.container = this.svg.append("g")
.classed('container', true);
this.tooltipServiceWrapper = createTooltipServiceWrapper(this.host.tooltipService, options.element);
this.tooltipServiceWrapper.addTooltip(this.container.selectAll('.circleCard'),
(tooltipEvent: ITooltipEventArgs<number>) => Visual.getTooltipData(tooltipEvent.data),
(tooltipEvent: ITooltipEventArgs<number>) => tooltipEvent.data[0]);
}
Solved! Go to Solution.
Hi @BrentonC,
In your capabilities, you specified a role named "tooltips", but haven't supplied a dataRole for it, so update the dataRoles section of your capabilities as follows:
{
...
"dataRoles": [
{
"displayName": "CCFV",
"name": "category1",
"kind": "Grouping"
},
{
"displayName": "Tooltips",
"name": "tooltips",
"kind": "Measure"
}
],
...
}
(the kind could be GroupingOrMeasure also, as you're using the table data view mapping; I've just gone with Measure for now)
This will then show up a place to add measure fields:
The sample bar chart has a very similar setup to this.
However if you want to make use of the measure(s) you pass in here, you'll need to update your dataViewMapping, as it will not appear in there and as such will not be accessible to the visual (note that only Dose is present):
I've modified this as follows:
{
...
"dataViewMappings": [
{
"conditions": [
{
"category1": {
"max": 1
}
}
],
"table": {
"rows": {
"select": [
{
"for": {
"in": "category1"
}
},
{
"for": {
"in": "tooltips"
}
}
]
}
}
}
]
...
}
Now, this will be accessible to the visual's data view, e.g.:
You'll then need to update your view model to include the tooltip data when you map the data view, and ensure that your getTooltipData function iterates through any additional measures you've added so that it knows what to display when the tooltip is rendered - unfortunately you haven't supplied these parts of your code for me to check, but it should be fairly straightforward for you to do if you're already mapping your category1 role into the visual's view model 🙂
Good luck!
Daniel
Proud to be a Super User!
On how to ask a technical question, if you really want an answer (courtesy of SQLBI)
Hi @BrentonC,
In your capabilities, you specified a role named "tooltips", but haven't supplied a dataRole for it, so update the dataRoles section of your capabilities as follows:
{
...
"dataRoles": [
{
"displayName": "CCFV",
"name": "category1",
"kind": "Grouping"
},
{
"displayName": "Tooltips",
"name": "tooltips",
"kind": "Measure"
}
],
...
}
(the kind could be GroupingOrMeasure also, as you're using the table data view mapping; I've just gone with Measure for now)
This will then show up a place to add measure fields:
The sample bar chart has a very similar setup to this.
However if you want to make use of the measure(s) you pass in here, you'll need to update your dataViewMapping, as it will not appear in there and as such will not be accessible to the visual (note that only Dose is present):
I've modified this as follows:
{
...
"dataViewMappings": [
{
"conditions": [
{
"category1": {
"max": 1
}
}
],
"table": {
"rows": {
"select": [
{
"for": {
"in": "category1"
}
},
{
"for": {
"in": "tooltips"
}
}
]
}
}
}
]
...
}
Now, this will be accessible to the visual's data view, e.g.:
You'll then need to update your view model to include the tooltip data when you map the data view, and ensure that your getTooltipData function iterates through any additional measures you've added so that it knows what to display when the tooltip is rendered - unfortunately you haven't supplied these parts of your code for me to check, but it should be fairly straightforward for you to do if you're already mapping your category1 role into the visual's view model 🙂
Good luck!
Daniel
Proud to be a Super User!
On how to ask a technical question, if you really want an answer (courtesy of SQLBI)
I am still having issues with getting my tooltips to display, I am sure it is how I have possibly implemented them in the visual.ts file?
"use strict";
import "core-js/stable";
import "../style/visual.less";
import powerbi from "powerbi-visuals-api";
import { dict } from './images';
import IVisual = powerbi.extensibility.IVisual;
import VisualConstructorOptions = powerbi.extensibility.visual.VisualConstructorOptions;
import VisualUpdateOptions = powerbi.extensibility.visual.VisualUpdateOptions;
import EnumerateVisualObjectInstancesOptions = powerbi.EnumerateVisualObjectInstancesOptions;
import VisualObjectInstanceEnumeration = powerbi.VisualObjectInstanceEnumeration;
import IVisualHost = powerbi.extensibility.visual.IVisualHost;
import DataView = powerbi.DataView;
import VisualTooltipDataItem = powerbi.extensibility.VisualTooltipDataItem;
import * as d3 from "d3";
import {
TooltipEventArgs,
TooltipEnabledDataPoint,
createTooltipServiceWrapper,
ITooltipServiceWrapper,
} from 'powerbi-visuals-utils-tooltiputils'
type Selection<T extends d3.BaseType> = d3.Selection<T, any, any, any>;
export class Visual implements IVisual {
private element: HTMLElement;
private isLandingPageOn: boolean;
private LandingPageRemoved: boolean;
private host: IVisualHost;
private svg: Selection<SVGElement>;
private container: Selection<SVGElement>;
private circle: Selection<SVGElement>;
// Declare an array list for the line objects
private lineOffset: Selection<SVGElement>;
private arr = new Array();
private arrDots = new Array();
// Declare as array of integers to hold the segments of the circle
private arrSegments = new Array();
private arrOffsetSegments = new Array();
private singleSegment: number;
private categorys: number = 17;
private CenterarrayOffset_ = new Array();
private CenterarrayOffset_bk = new Array();
private LandingPage: Selection<any>;
virificationArray: string[];
private tooltipServiceWrapper: ITooltipServiceWrapper;
constructor(options: VisualConstructorOptions) {
this.element = options.element;
this.svg = d3.select(options.element)
.append('svg')
.classed('circleCard', true);
this.host = options.host;
this.container = this.svg.append("g")
.classed('container', true);
}
private static getTooltipData(value: any): VisualTooltipDataItem[] {
return [{
displayName: value.category,
value: value.value.toString(),
color: value.color,
header: 'ToolTip Title'
}];
}
private HandleLandingPage(options: VisualUpdateOptions) {
console.log("landing")
if(!options.dataViews || !options.dataViews.length) {
if(!this.isLandingPageOn) {
this.isLandingPageOn = true;
const SampleLandingPage: Element = this.createSampleLandingPage();
this.element.appendChild(SampleLandingPage);
this.LandingPage = d3.select(SampleLandingPage);
}
} else {
if(this.isLandingPageOn && !this.LandingPageRemoved){
this.LandingPageRemoved = true;
this.LandingPage.remove();
}
}
}
private createSampleLandingPage(): Element {
let div = document.createElement("div");
let header = document.createElement("h1")
header.textContent = "Sample Bar Chart Landing Page";
header.setAttribute("class","LandingPage");
let p1 = document.createElement("a");
p1.setAttribute("class", "LandingPageHelpLink");
p1.textContent = "Learn more about Landing page";
div.appendChild(header);
div.appendChild(p1);
return div;
}
public update(options: VisualUpdateOptions) {
let dataView_: DataView = options.dataViews[0];
// this.host.fetchMoreData();
let width: number = options.viewport.width;
let height: number = options.viewport.height;
this.svg.attr("width", width);
this.svg.attr("height", height);
let radius: number = Math.min(width, height) / 2.4;
let radiusDot: number = Math.min(width, height) / 250;
let centerOffset: number;
let cX: number;
let cY: number;
let cX_: number;
let cY_: number;
let cXOffset: number;
let cYOffset: number;
let counter: number;
let counter1: number;
let entryCounter: number;
let ccfvfColour: string;
this.CenterarrayOffset_ = new Array();
this.CenterarrayOffset_bk = new Array();
this.virificationArray = new Array();
let colourCounter: number;
let colourString: string;
// To prevent from drawing everyting over and over again, remove all drawing and start again.
this.svg.selectAll('circle').remove();
this.svg.selectAll('image').remove();
this.HandleLandingPage(options);
entryCounter = 0
for (var j = 0; j < dataView_.table.rows.length; j++) {
if (
this.virificationArray.indexOf(dataView_.table.rows[j].toString().split(",")[2]) > -1){
}
else{
this.virificationArray.push(String(dataView_.table.rows[j].toString().split(",")[2]))
}
}
this.circle = this.container.append("circle")
.classed('circle', true);
// Create the lines for the categorys to split the kelly wheel into 17 sections
for (let i = 0; i < this.categorys; i++) {
this.arr.push(this.container.append("line").classed("line_" + i, true));
}
// Get the single segments of the kelly wheel in deg of 360
this.singleSegment = 360 / this.arr.length;
for (var i = 0; i < this.arr.length; i++) {
this.arrSegments.push(this.singleSegment * i)
}
for (var a = 0; a < this.virificationArray.length; a++) {
this.arrDots[a] = new Array();
entryCounter = 0
for (var j = 0; j < dataView_.table.rows.length; j++) {
if (
dataView_.table.rows[j].toString().split(",")[2] == this.virificationArray[a]) {
entryCounter = entryCounter + 1
}
}
for (var i = 0; i < entryCounter; i++) {
this.arrDots[a].push(this.container.append("circle").classed("circle__" + a + "_" + i, true));
}
}
/*
* Get the edge of the circle
*/
this.circle
.style("stroke", "black")
.style("stroke-width", 2)
.attr("r", radius)
.attr("cx", width / 2)
.attr("cy", height / 2)
.style("fill", "#ffe500");
counter = 0
counter1 = 0
for (var i = 0; i < 17; i++) {
cX = (width / 2) + radius * Math.cos(this.arrSegments[i] * Math.PI / 180);
cY = (height / 2) + radius * Math.sin(this.arrSegments[i] * Math.PI / 180);
cX_ = width / 2 - radius * Math.cos(this.arrSegments[i] );
cY_ = height / 2 - radius * Math.sin(this.arrSegments[i] );
centerOffset = 40;
// outer offset
cXOffset = (width / 2) + radius * Math.cos((this.arrSegments[i]) * Math.PI / 180);
cYOffset = (height / 2) + radius * Math.sin(this.arrSegments[i] * Math.PI / 180);
this.arr[i]
.attr("x1", '50%')
.attr("y1", '50%')
.attr("x2", cX)
.attr("y2", cY)
.attr("stroke-width", 3)
.attr("stroke", "black");
centerOffset = 0
// left right | up down | in out
this.CenterarrayOffset_ =
// First loop
[[23, 4, .06], //1
[20, 13, .06], //2
[15, 19, .06], //3
[4, 22, .06], //4
[-3, 23, .06],//5
[-11, 20, .06],//6
[-18, 16, .06],//7
[-22, 6, .06],//8
[-22, -1, .06],//9
[-20, -9, .06],//10
[-14, -16, .06],//11
[-9, -20, .06],//12
[-1, -20, .06],//13
[7, -20, .06],//14
[13, -17, .06],//15
[18, -11, .06],//16
[22, -3, .06]]//17
for (var z = 0; z < this.arrDots[counter].length; z++) {
if(z >=138){
// Max 138 per FV section
continue
}
colourCounter = 0
for (var j = 0; j < dataView_.table.rows.length; j++) {
// console.log(this.virificationArray[a])
if (
dataView_.table.rows[j].toString().split(",")[2] == this.virificationArray[i]) {
colourCounter = colourCounter + 1
}
if (colourCounter == z) {
colourString = dataView_.table.rows[j].toString().split(",")[1]
}
}
if (colourString == "Yes") {
ccfvfColour = "green"
}
else {
ccfvfColour = "red"
}
this.arrDots[i][z]
.style("fill", ccfvfColour)
.style("stroke", ccfvfColour)
.style("stroke-width", 2)
.attr("r", radiusDot)
.attr("cx", (this.CenterarrayOffset_[counter1][2] + centerOffset) * (cXOffset - (width / 2) + this.CenterarrayOffset_[counter1][0]) + (width / 2) + this.CenterarrayOffset_[counter1][0])
.attr("cy", (this.CenterarrayOffset_[counter1][2] + centerOffset) * (cYOffset - (height / 2) + this.CenterarrayOffset_[counter1][1]) + (height / 2) + this.CenterarrayOffset_[counter1][1])
;
centerOffset = (radius / 800000) + centerOffset + 0.025
if(z == 0){
this.CenterarrayOffset_bk = [
[15, 10, .48], //1
[0, 20, .48], //2
[-15, 22, .48], //3
[-30, 23, .48], //4
[-40, 12, .48],//5
[-44, 3, .48],//6
[-48, -12, .48],//7
[-47, -27, .48],//8
[-45, -37, .48],//9 6
[-35, -45, .48],//10
[-20, -53, .48],//11
[-10, -50, .48],//12
[6, -45, .48],//13
[15, -38, .48],//14
[24, -28, .48],//15
[36, -18, .48],//16
[40, -8, .48]]
this.container.append("svg:image")
.attr("xlink:href",dict[this.virificationArray[i]])
.attr("x", (.9 + centerOffset) * (cXOffset - (width / 2) + this.CenterarrayOffset_bk[counter1][0]) + (width / 2) + this.CenterarrayOffset_bk[counter1][0])
.attr("y", (.9 + centerOffset) * (cYOffset - (height / 2) + this.CenterarrayOffset_bk[counter1][1]) + (height / 2) + this.CenterarrayOffset_bk[counter1][1]).
attr("width", 50)
.attr("height", 50);;
}
if (z == 29) {
centerOffset = 0
this.CenterarrayOffset_ = [
[28, 12, .08], //1
[20, 22, .08], //2
[13, 29, .08], //3
[0, 31, .08], //4
[-10, 28, .08],//5
[-19, 22, .08],//6
[-27, 15, .08],//7
[-30, 2, .08],//8
[-28, -8, .08],//9
[-23, -18, .08],//10 2
[-15, -25, .08],//11
[-7, -28, .08],//12
[5, -27, .08],//13
[14, -24, .08],//14
[22, -19, .08],//15
[28, -9, .08],//16
[30, 2, .08]]
}
if (z == 58) {
//- left right +|- up down +| in out
centerOffset = 0
this.CenterarrayOffset_ = [
[28, 19, .18], //1
[19, 30, .18], //2
[5, 32, .18], //3
[-7, 31, .18], //4
[-17, 28, .18],//5
[-27, 21, .18],//6
[-33, 9, .18],//7 3
[-34, -3, .18],//8
[-28, -15, .18],//9
[-25, -27, .18],//10
[-14, -33, .18],//11
[1, -30, .18],//12
[11, -27, .18],//13
[22, -24, .18],//14
[28, -15, .18],//15
[32, -3, .18],//16
[30, 9, .18]]
}
if (z == 82) {
//- left right +|- up down +| in out
centerOffset = 0
this.CenterarrayOffset_ = [
[28, 26, .26], //1
[17, 37, .26], //2
[-2, 36, .26], //3
[-14, 32, .26], //4
[-25, 28, .26],//5
[-35, 20, .26],//6
[-39, 5, .26],//7
[-39, -9, .26],//8 4
[-28, -22, .26],//9
[-23, -35, .26],//10
[-10, -39, .26],//11
[7, -34, .26],//12
[18, -27, .26],//13
[28, -18, .26],//14
[34, -10, .26],//15
[36, 3, .26],//16
[30, 16, .26]]
}
if (z == 102) {
//- left right +|- up down +| in out
centerOffset = 0
this.CenterarrayOffset_ = [
[19, 33, .39], //1
[6, 40, .39], //2
[-9, 40, .39], //3
[-22, 31, .39], //4
[-33, 22, .39],//5
[-38, 9, .39],//6
[-40, -5, .39],//7
[-32, -20, .39],//8
[-25, -30, .39],//9
[-12, -39, .39],//10
[1, -39, .39],//11
[16, -33, .39],//12 5
[26, -25, .39],//13
[34, -16, .39],//14
[35, 1, .39],//15
[36, 12, .39],//16
[30, 24, .39]]
}
if (z == 120) {
//- left right +|- up down +| in out
centerOffset = 0
this.CenterarrayOffset_ = [
[17, 40, .54], //1
[0, 45, .54], //2
[-15, 43, .54], //3
[-30, 31, .54], //4
[-40, 21, .54],//5
[-44, 6, .54],//6
[-43, -12, .54],//7
[-33, -27, .54],//8
[-25, -37, .54],//9 6
[-10, -45, .54],//10
[9, -41, .54],//11
[24, -33, .54],//12
[33, -25, .54],//13
[40, -12, .54],//14
[41, 5, .54],//15
[36, 20, .54],//16
[30, 31, .54]]
}
if (z == 132) {
//- left right +|- up down +| in out
centerOffset = 0
this.CenterarrayOffset_ = [
[15, 47, .70], //1
[-4, 50, .70], //2
[-23, 43, .63], //3
[-37, 31, .63], //4
[-47, 21, .63],//5
[-50, 3, .63],//6
[-43, -21, .63],//7 7
[-37, -32, .63],//8
[-21, -44, .63],//9
[-4, -50, .63],//10
[15, -45, .63],//11
[31, -34, .63],//12
[40, -24, .63],//13
[46, -8, .63],//14
[47, 8, .63],//15
[35, 28, .63],//16
[30, 38, .63]]
}
if (z == 152) {
//- left right +|- up down +| in out
centerOffset = 0
this.CenterarrayOffset_ = [
[11, 52, .80], //1
[-11, 53, .80], //2
[-29, 45, .80], //3
[-43, 31, .80], //4
[-53, 16, .80],//5
[-53, -5, .80],//6
[-47, -25, .80],//7
[-35, -40, .80],//8
[-21, -50, .80],//9
[0, -55, .80],//10 8
[18, -50, .80],//11
[36, -37, .80],//12
[47, -24, .80],//13
[52, -6, .80],//14
[50, 14, .80],//15
[40, 31, .80],//16
[30, 44, .80]]
}
if (z == 140) {
// left right | up down | in out
centerOffset = 0
// Get the remainder of CCFV and add a number to the end of the column
}
}
this.svg.selectAll("line").raise()
counter = counter + 1
counter1 = counter1 + 1
}
this.tooltipServiceWrapper.addTooltip(this.svg.selectAll('circle'),
(tooltipEvent: TooltipEventArgs<number>) =>
Visual.getTooltipData(tooltipEvent.data),
(tooltipEvent: TooltipEventArgs<number>) => null);
this.arrDots = []
this.arr = []
}
}
Update:
I ended up getting it sorted, I added each object as they were made in a loop as such.
let dot = this.arrDots[i][z]
.style("fill", ccfvfColour)
.style("stroke", ccfvfColour)
.style("stroke-width", 2)
.attr("r", radiusDot)
.attr("cx", (this.CenterarrayOffset_[counter1][2] + centerOffset) * (cXOffset - (width / 2) + this.CenterarrayOffset_[counter1][0]) + (width / 2) + this.CenterarrayOffset_[counter1][0])
.attr("cy", (this.CenterarrayOffset_[counter1][2] + centerOffset) * (cYOffset - (height / 2) + this.CenterarrayOffset_[counter1][1]) + (height / 2) + this.CenterarrayOffset_[counter1][1])
this.tooltipServiceWrapper.addTooltip(dot,
(tooltipEvent: TooltipEventArgs<number>) => this.getTooltipData(tooltipEvent.data),
(tooltipEvent: TooltipEventArgs<number>) => null
// Get the remainder of CCFV and add a number to the end of the column
)
Thank You
Thank you very much, I am currently working on getting it sorted as I get stuck I'll post again for sure.
Brenton Collins
March 31 - April 2, 2025, in Las Vegas, Nevada. Use code MSCUST for a $150 discount!
Check out the February 2025 Power BI update to learn about new features.
User | Count |
---|---|
17 | |
6 | |
3 | |
2 | |
2 |