I've created a custom visual (combination scatter plot and dual-line graph) and am attempting to add tooltips. I used table databinding and followed an example I found here for assigning an Identity. For some reason, the tooltips are each only showing one line (employee ID) and the ID is the same for each tooltip.
import DataViewObjects = powerbi.extensibility.utils.dataview.DataViewObjects; module powerbi.extensibility.visual { "use strict"; interface DataPoint { grade: number; minimum: number; maximum: number; }; interface ViewModel { dataPoints: DataPoint[]; rangePenData: RangePoint[]; maxValue: number; minValue: number; maxGrade: number; minGrade: number; }; interface RangePoint { grade: number; rangePen: number; employeeID: string; tooltips: VisualTooltipDataItem[]; identity: ISelectionId[]; } export class Visual implements IVisual { private host: IVisualHost; private svg: d3.Selection<SVGElement>; private structureGroup: d3.Selection<SVGElement>; private yPadding: number = 0.1; private scatterGroup: d3.Selection<SVGElement>; private xAxisGroup: d3.Selection<SVGElement>; private yAxisGroup: d3.Selection<SVGElement>; private settings = { axis: { x: { padding: 25 }, y: { padding: 50 } }, border: { top: 10 } } constructor(options: VisualConstructorOptions) { this.host = options.host; this.svg = d3.select(options.element) .append("svg") .classed("vchart", true); this.structureGroup = this.svg.append("g") .classed("structure-group", true); this.scatterGroup = this.svg.append("g") .classed("scatter-group", true); this.xAxisGroup = this.svg.append("g") .classed("x-axis", true) this.yAxisGroup = this.svg.append("g") .classed("y-axis", true) } public update(options: VisualUpdateOptions) { let viewModel = this.getViewModel(options) let spread = Math.max(Math.abs(viewModel.minValue),Math.abs(viewModel.maxValue)) let width = options.viewport.width; let height = options.viewport.height; this.svg.attr({ width: width, height: height, }); let xScale = d3.scale.linear() .domain([spread*-1,spread]) .range([this.settings.axis.x.padding, width]); let xAxis = d3.svg.axis() .scale(xScale) .orient("bottom") .tickSize(1); this.xAxisGroup.call(xAxis) .attr({ transform: "translate(0, " + (height-this.settings.axis.x.padding) + ")" }); let yScale = d3.scale.linear() .domain([viewModel.maxGrade,viewModel.minGrade]) .range([0 + this.settings.border.top, height - this.settings.axis.x.padding]); let yAxis = d3.svg.axis() .scale(yScale) .orient("left") .tickSize(1); this.yAxisGroup .call(yAxis) .attr({ transform: "translate(" + (width + this.settings.axis.y.padding)/2 + ",0)" }) .selectAll("text") .style({ "text-anchor": "end", "font-size": "small" }); var valueline = d3.svg.line<DataPoint>() .x(function (d) { return xScale(d.minimum); }) .y(function (d) { return yScale(d.grade); }) var valueline2 = d3.svg.line<DataPoint>() .x(function (d) { return xScale(d.maximum); }) .y(function (d) { return yScale(d.grade); }) let minline = this.structureGroup .selectAll(".minline") .data([viewModel.dataPoints]); minline .enter() .append("path") .attr("class", "minline"); minline .attr("d", function (d) { return valueline(d); }) .style({ stroke: "green", fill: "none", "stroke-width": "3px", }); minline.exit() .remove(); let maxline = this.structureGroup .selectAll(".maxline") .data([viewModel.dataPoints]); maxline .enter() .append("path") .attr("class", "maxline"); maxline .attr("d", function (d) { return valueline2(d); }) .style({ stroke: "green", fill: "none", "stroke-width": "3px", }); maxline.exit() .remove(); let scatter = this.scatterGroup .selectAll(".rangePen") .data(viewModel.rangePenData) scatter .enter() .append("circle") .attr("class", "rangePen") scatter .attr("cx", d => xScale(d.rangePen)) .attr("cy", d => yScale(d.grade)) .attr("r", 5) .on("mouseover", (d) => { let mouse=d3.mouse(this.svg.node()); let x=mouse[0]; let y=mouse[1]; this.host.tooltipService.show({ dataItems: d.tooltips, identities: [d.identity], coordinates: [x, y], isTouchEvent: false }) }) scatter.exit() .remove(); } private getViewModel(options: VisualUpdateOptions): ViewModel { let dv = options.dataViews; let viewModel: ViewModel = { dataPoints: [], rangePenData: [], maxValue: 0, minValue: 0, maxGrade: 0, minGrade: 0, }; let view1 = dv[0].table; let view2 = dv[0]; for (let i = 0, len = view1.rows.length; i < len; i++) { viewModel.dataPoints.push({ grade: <number>view1.rows[i][1], minimum: <number>view1.rows[i][2]/-2, maximum: <number>view1.rows[i][2]/2, }); viewModel.rangePenData.push({ grade: <number>view1.rows[i][1], rangePen: <number>view1.rows[i][3], employeeID: <string>view1.rows[i][0], identity: this.getSelectionIds(view2, this.host), tooltips: [{ displayName: "Employee ID:", value: <string>view1.rows[i][0] }, { displayName: "Range Penetration:", value: <string>view1.rows[i][3] } ] }); } viewModel.maxValue = d3.max([d3.max(viewModel.dataPoints, d => d.maximum),d3.max(viewModel.rangePenData, d => d.rangePen)]); viewModel.minValue = d3.min([d3.min(viewModel.dataPoints, d => d.minimum),d3.min(viewModel.rangePenData, d => d.rangePen)]); viewModel.maxGrade = d3.max(viewModel.rangePenData, d => d.grade); viewModel.minGrade = d3.min(viewModel.rangePenData, d => d.grade); return viewModel; } public getSelectionIds(dataView: DataView, host: IVisualHost): ISelectionId[] { return dataView.table.identity.map((identity: DataViewScopeIdentity) => { const categoryColumn: DataViewCategoryColumn = { source: dataView.table.columns[0], values: null, identity: [identity] }; return host.createSelectionIdBuilder() .withCategory(categoryColumn, 0) .createSelectionId(); }) }; } }