From 34cff5ed251f6774b9786762ddea75f2d702aa73 Mon Sep 17 00:00:00 2001 From: Andrew Johnson Date: Sun, 16 Jul 2023 22:54:25 +0300 Subject: [PATCH 1/6] Split svgListen to class --- src/Classes/svgDotsClass.ts | 10 ++-- src/Classes/svgObjectClass.ts | 2 - src/Classes/svgSelectionClass.ts | 8 ---- src/Classes/svgTooltipLineClass.ts | 30 ++++++++++++ src/visual.ts | 77 ++++++++++++++---------------- 5 files changed, 70 insertions(+), 57 deletions(-) create mode 100644 src/Classes/svgTooltipLineClass.ts diff --git a/src/Classes/svgDotsClass.ts b/src/Classes/svgDotsClass.ts index c490de2..9134d93 100644 --- a/src/Classes/svgDotsClass.ts +++ b/src/Classes/svgDotsClass.ts @@ -11,9 +11,11 @@ class svgDotsClass { draw(viewModel: viewModelClass): void { this.dotsGroup.selectAll(".dotsgroup").remove() - if (!(viewModel.plotPoints)) { + if (!(viewModel.plotProperties.displayPlot)) { return; } + const lower: number = viewModel.plotProperties.yAxis.lower; + const upper: number = viewModel.plotProperties.yAxis.upper; this.dotsGroup .append('g') @@ -27,11 +29,7 @@ class svgDotsClass { .attr("cx", (d: plotData) => viewModel.plotProperties.xScale(d.x)) .attr("r", (d: plotData) => d.aesthetics.size) .style("fill", (d: plotData) => { - if (viewModel.plotProperties.displayPlot) { - return between(d.value, viewModel.plotProperties.yAxis.lower, viewModel.plotProperties.yAxis.upper) ? d.aesthetics.colour : "#FFFFFF"; - } else { - return "#FFFFFF"; - } + return between(d.value, lower, upper) ? d.aesthetics.colour : "#FFFFFF"; }) } diff --git a/src/Classes/svgObjectClass.ts b/src/Classes/svgObjectClass.ts index fd9f58e..1ba60bd 100644 --- a/src/Classes/svgObjectClass.ts +++ b/src/Classes/svgObjectClass.ts @@ -3,7 +3,6 @@ type SelectionBase = d3.Selection; class svgObjectClass { - listeningRect: SelectionBase; tooltipLineGroup: SelectionBase; xAxisGroup: d3.Selection; xAxisLabels: d3.Selection; @@ -12,7 +11,6 @@ class svgObjectClass { constructor(svg: d3.Selection) { this.tooltipLineGroup = svg.append("g"); - this.listeningRect = svg.append("g"); this.xAxisGroup = svg.append("g"); this.yAxisGroup = svg.append("g"); this.xAxisLabels = svg.append("text"); diff --git a/src/Classes/svgSelectionClass.ts b/src/Classes/svgSelectionClass.ts index 7dbc64c..e5b231e 100644 --- a/src/Classes/svgSelectionClass.ts +++ b/src/Classes/svgSelectionClass.ts @@ -5,23 +5,15 @@ import viewModel from "./viewModelClass" type SelectionAny = d3.Selection; class svgSelectionClass { - listeningRectSelection: SelectionAny; tooltipLineSelection: SelectionAny; update(args: { svgObjects: svgObjectClass, viewModel: viewModel }) { if (args.viewModel.plotPoints) { - this.listeningRectSelection = args.svgObjects - .listeningRect - .selectAll(".obs-sel") - .data(args.viewModel.plotPoints); this.tooltipLineSelection = args.svgObjects .tooltipLineGroup .selectAll(".ttip-line") .data(args.viewModel.plotPoints); } else { - this.listeningRectSelection = args.svgObjects - .listeningRect - .selectAll(".obs-sel"); this.tooltipLineSelection = args.svgObjects .tooltipLineGroup .selectAll(".ttip-line"); diff --git a/src/Classes/svgTooltipLineClass.ts b/src/Classes/svgTooltipLineClass.ts new file mode 100644 index 0000000..ab33269 --- /dev/null +++ b/src/Classes/svgTooltipLineClass.ts @@ -0,0 +1,30 @@ +import * as d3 from "d3"; +import viewModelClass from "./viewModelClass"; +type SelectionBase = d3.Selection; + +class svgTooltipLineClass { + tooltipLineGroup: SelectionBase; + + draw(viewModel: viewModelClass): void { + this.tooltipLineGroup.selectAll(".obs-sel").remove() + if (!(viewModel.plotProperties.displayPlot)) { + return; + } + + this.tooltipLineGroup + .append('g') + .classed("obs-sel", true) + .selectAll(".obs-sel") + .data(viewModel.plotPoints) + .enter() + .append("rect") + .style("fill","transparent") + .attr("width", viewModel.plotProperties.width) + .attr("height", viewModel.plotProperties.height) + } + + constructor(svg: d3.Selection) { + this.tooltipLineGroup = svg.append("g"); + } +} +export default svgTooltipLineClass diff --git a/src/visual.ts b/src/visual.ts index 23afe92..3ba7e50 100644 --- a/src/visual.ts +++ b/src/visual.ts @@ -25,6 +25,7 @@ import svgSelectionClass from "./Classes/svgSelectionClass" import { axisProperties } from "./Classes/plotPropertiesClass" import svgLinesClass from "./Classes/svgLinesClass"; import svgDotsClass from "./Classes/svgDotsClass"; +import svgTooltipLineClass from "./Classes/svgTooltipLineClass"; type SelectionAny = d3.Selection; @@ -36,6 +37,7 @@ export class Visual implements IVisual { private svgIcons: svgIconClass; private svgLines: svgLinesClass; private svgDots: svgDotsClass + private svgTooltipLine: svgTooltipLineClass; private svgSelections: svgSelectionClass; private viewModel: viewModelClass; private selectionManager: ISelectionManager; @@ -55,6 +57,7 @@ export class Visual implements IVisual { this.svgIcons = new svgIconClass(this.svg); this.svgLines = new svgLinesClass(this.svg); this.svgDots = new svgDotsClass(this.svg); + this.svgTooltipLine = new svgTooltipLineClass(this.svg); this.svgSelections = new svgSelectionClass(); this.viewModel = new viewModelClass(); this.viewModel.firstRun = true; @@ -88,6 +91,7 @@ export class Visual implements IVisual { .attr("height", this.viewModel.plotProperties.height); console.log("TooltipTracking start") + this.svgTooltipLine.draw(this.viewModel) this.initTooltipTracking(); console.log("Draw axes start") @@ -139,49 +143,40 @@ export class Visual implements IVisual { .attr("height", this.viewModel.plotProperties.height) .style("fill-opacity", 0); - const tooltipMerged = this.svgSelections - .listeningRectSelection - .enter() - .append("rect") - .merge(this.svgSelections.listeningRectSelection) - tooltipMerged.classed("obs-sel", true); - - tooltipMerged.style("fill","transparent") - .attr("width", this.viewModel.plotProperties.width) - .attr("height", this.viewModel.plotProperties.height); - - tooltipMerged.on("mousemove", (event) => { - if (this.viewModel.plotProperties.displayPlot) { - const xValue: number = this.viewModel.plotProperties.xScale.invert(event.pageX); - const xRange: number[] = this.viewModel - .plotPoints - .map(d => d.x) - .map(d => Math.abs(d - xValue)); - const nearestDenominator: number = d3.leastIndex(xRange,(a,b) => a-b) as number; - const scaled_x: number = this.viewModel.plotProperties.xScale(this.viewModel.plotPoints[nearestDenominator].x) - const scaled_y: number = this.viewModel.plotProperties.yScale(this.viewModel.plotPoints[nearestDenominator].value) - - this.host.tooltipService.show({ - dataItems: this.viewModel.plotPoints[nearestDenominator].tooltip, - identities: [this.viewModel.plotPoints[nearestDenominator].identity], - coordinates: [scaled_x, scaled_y], - isTouchEvent: false - }); - xAxisLine.style("fill-opacity", 1).attr("transform", "translate(" + scaled_x + ",0)"); - } - }); - - tooltipMerged.on("mouseleave", () => { - if (this.viewModel.plotProperties.displayPlot) { - this.host.tooltipService.hide({ - immediately: true, - isTouchEvent: false + this.svgTooltipLine + .tooltipLineGroup + .selectAll(".obs-sel") + .selectChildren() + .on("mousemove", (event) => { + if (this.viewModel.plotProperties.displayPlot) { + const xValue: number = this.viewModel.plotProperties.xScale.invert(event.pageX); + const xRange: number[] = this.viewModel + .plotPoints + .map(d => d.x) + .map(d => Math.abs(d - xValue)); + const nearestDenominator: number = d3.leastIndex(xRange,(a,b) => a-b) as number; + const scaled_x: number = this.viewModel.plotProperties.xScale(this.viewModel.plotPoints[nearestDenominator].x) + const scaled_y: number = this.viewModel.plotProperties.yScale(this.viewModel.plotPoints[nearestDenominator].value) + + this.host.tooltipService.show({ + dataItems: this.viewModel.plotPoints[nearestDenominator].tooltip, + identities: [this.viewModel.plotPoints[nearestDenominator].identity], + coordinates: [scaled_x, scaled_y], + isTouchEvent: false + }); + xAxisLine.style("fill-opacity", 1).attr("transform", "translate(" + scaled_x + ",0)"); + } + }) + .on("mouseleave", () => { + if (this.viewModel.plotProperties.displayPlot) { + this.host.tooltipService.hide({ + immediately: true, + isTouchEvent: false + }); + xAxisLine.style("fill-opacity", 0); + } }); - xAxisLine.style("fill-opacity", 0); - } - }); xAxisLine.exit().remove() - tooltipMerged.exit().remove() } drawXAxis(): void { From 14014434857ceca1d93978745bd4d378f4429df7 Mon Sep 17 00:00:00 2001 From: Andrew Johnson Date: Mon, 17 Jul 2023 02:12:44 +0300 Subject: [PATCH 2/6] Centralised plotting class --- src/Classes/plottingClass.ts | 53 ++++++ src/Classes/svgIconClass.ts | 2 +- src/Classes/svgObjectClass.ts | 21 --- src/Classes/svgSelectionClass.ts | 24 --- src/Classes/svgTooltipLineClass.ts | 13 ++ src/Classes/svgXAxisClass.ts | 90 +++++++++ src/Classes/svgYAxisClass.ts | 79 ++++++++ src/visual.ts | 282 +++++------------------------ 8 files changed, 285 insertions(+), 279 deletions(-) create mode 100644 src/Classes/plottingClass.ts delete mode 100644 src/Classes/svgObjectClass.ts delete mode 100644 src/Classes/svgSelectionClass.ts create mode 100644 src/Classes/svgXAxisClass.ts create mode 100644 src/Classes/svgYAxisClass.ts diff --git a/src/Classes/plottingClass.ts b/src/Classes/plottingClass.ts new file mode 100644 index 0000000..f56a385 --- /dev/null +++ b/src/Classes/plottingClass.ts @@ -0,0 +1,53 @@ +import * as d3 from "d3"; +import powerbi from "powerbi-visuals-api"; +import extensibility = powerbi.extensibility; +import ex_visual = extensibility.visual; +import VisualConstructorOptions = ex_visual.VisualConstructorOptions; +import svgDotsClass from "./svgDotsClass"; +import svgIconClass from "./svgIconClass"; +import svgLinesClass from "./svgLinesClass"; +import svgTooltipLineClass from "./svgTooltipLineClass"; +import svgXAxisClass from "./svgXAxisClass"; +import svgYAxisClass from "./svgYAxisClass"; +import viewModelClass from "./viewModelClass"; + +interface svgTypes { + svg: d3.Selection, + svgTooltipLine: svgTooltipLineClass, + svgXAxis: svgXAxisClass, + svgYAxis: svgYAxisClass, + svgDots: svgDotsClass, + svgLines: svgLinesClass, + svgIcons: svgIconClass, + draw: (viewModel: viewModelClass) => void +} + +type svgKeys = Exclude; + +class plottingClass implements svgTypes { + svg: d3.Selection; + svgTooltipLine: svgTooltipLineClass; + svgXAxis: svgXAxisClass; + svgYAxis: svgYAxisClass; + svgDots: svgDotsClass; + svgLines: svgLinesClass; + svgIcons: svgIconClass; + + draw(viewModel: viewModelClass): void { + Object.getOwnPropertyNames(this).filter(d => !(["draw", "svg"].includes(d))).forEach(key => { + this[key as svgKeys].draw(viewModel); + }) + } + + constructor(options: VisualConstructorOptions) { + this.svg = d3.select(options.element).append("svg"); + this.svgTooltipLine = new svgTooltipLineClass(this.svg); + this.svgXAxis = new svgXAxisClass(this.svg); + this.svgYAxis = new svgYAxisClass(this.svg); + this.svgDots = new svgDotsClass(this.svg); + this.svgLines = new svgLinesClass(this.svg); + this.svgIcons = new svgIconClass(this.svg); + } +} + +export default plottingClass; diff --git a/src/Classes/svgIconClass.ts b/src/Classes/svgIconClass.ts index c5b1baa..209a066 100644 --- a/src/Classes/svgIconClass.ts +++ b/src/Classes/svgIconClass.ts @@ -138,7 +138,7 @@ class svgIconClass { return iconsPresent; } - drawIcons(viewModel: viewModelClass): void { + draw(viewModel: viewModelClass): void { d3.selectAll(".icongroup").remove() const draw_variation: boolean = viewModel.inputSettings.nhs_icons.show_variation_icons; if (!draw_variation) { diff --git a/src/Classes/svgObjectClass.ts b/src/Classes/svgObjectClass.ts deleted file mode 100644 index 1ba60bd..0000000 --- a/src/Classes/svgObjectClass.ts +++ /dev/null @@ -1,21 +0,0 @@ -import * as d3 from "d3"; -type SelectionBase = d3.Selection; - - -class svgObjectClass { - tooltipLineGroup: SelectionBase; - xAxisGroup: d3.Selection; - xAxisLabels: d3.Selection; - yAxisGroup: d3.Selection; - yAxisLabels: d3.Selection; - - constructor(svg: d3.Selection) { - this.tooltipLineGroup = svg.append("g"); - this.xAxisGroup = svg.append("g"); - this.yAxisGroup = svg.append("g"); - this.xAxisLabels = svg.append("text"); - this.yAxisLabels = svg.append("text"); - } -} - -export default svgObjectClass diff --git a/src/Classes/svgSelectionClass.ts b/src/Classes/svgSelectionClass.ts deleted file mode 100644 index e5b231e..0000000 --- a/src/Classes/svgSelectionClass.ts +++ /dev/null @@ -1,24 +0,0 @@ -import * as d3 from "d3"; -import svgObjectClass from "./svgObjectClass" -import viewModel from "./viewModelClass" - -type SelectionAny = d3.Selection; - -class svgSelectionClass { - tooltipLineSelection: SelectionAny; - - update(args: { svgObjects: svgObjectClass, viewModel: viewModel }) { - if (args.viewModel.plotPoints) { - this.tooltipLineSelection = args.svgObjects - .tooltipLineGroup - .selectAll(".ttip-line") - .data(args.viewModel.plotPoints); - } else { - this.tooltipLineSelection = args.svgObjects - .tooltipLineGroup - .selectAll(".ttip-line"); - } - } -} - -export default svgSelectionClass diff --git a/src/Classes/svgTooltipLineClass.ts b/src/Classes/svgTooltipLineClass.ts index ab33269..e817078 100644 --- a/src/Classes/svgTooltipLineClass.ts +++ b/src/Classes/svgTooltipLineClass.ts @@ -7,6 +7,7 @@ class svgTooltipLineClass { draw(viewModel: viewModelClass): void { this.tooltipLineGroup.selectAll(".obs-sel").remove() + this.tooltipLineGroup.selectAll(".ttip-line").remove() if (!(viewModel.plotProperties.displayPlot)) { return; } @@ -21,6 +22,18 @@ class svgTooltipLineClass { .style("fill","transparent") .attr("width", viewModel.plotProperties.width) .attr("height", viewModel.plotProperties.height) + + this.tooltipLineGroup + .append('g') + .classed("ttip-line", true) + .selectAll(".ttip-line") + .data(viewModel.plotPoints) + .enter() + .append("rect") + .attr("stroke-width", "1px") + .attr("width", ".5px") + .attr("height", viewModel.plotProperties.height) + .style("fill-opacity", 0) } constructor(svg: d3.Selection) { diff --git a/src/Classes/svgXAxisClass.ts b/src/Classes/svgXAxisClass.ts new file mode 100644 index 0000000..d0bb695 --- /dev/null +++ b/src/Classes/svgXAxisClass.ts @@ -0,0 +1,90 @@ +import * as d3 from "d3"; +import viewModelClass from "./viewModelClass"; +import { axisProperties } from "./plotPropertiesClass"; +type SelectionBase = d3.Selection; + +class svgXAxisClass { + xAxisGroup: SelectionBase; + refreshingAxis: boolean + + draw(viewModel: viewModelClass): void { + this.xAxisGroup.selectAll(".xaxisgroup").remove() + this.xAxisGroup.selectAll(".xaxislabel").remove() + if (!(viewModel.plotProperties.displayPlot)) { + return; + } + + const xAxisProperties: axisProperties = viewModel.plotProperties.xAxis; + let xAxis: d3.Axis; + + if (xAxisProperties.ticks) { + xAxis = d3.axisBottom(viewModel.plotProperties.xScale); + if (xAxisProperties.tick_count) { + xAxis.ticks(xAxisProperties.tick_count) + } + if (viewModel.tickLabels) { + xAxis.tickFormat(d => { + return viewModel.tickLabels.map(d => d.x).includes(d) + ? viewModel.tickLabels[d].label + : ""; + }) + } + } else { + xAxis = d3.axisBottom(viewModel.plotProperties.xScale).tickValues([]); + } + + const axisHeight: number = viewModel.plotProperties.height - viewModel.plotProperties.yAxis.end_padding; + + this.xAxisGroup + .append('g') + .classed("xaxisgroup", true) + .call(xAxis) + .attr("color", viewModel.plotProperties.displayPlot ? xAxisProperties.colour : "#FFFFFF") + // Plots the axis at the correct height + .attr("transform", "translate(0, " + axisHeight + ")") + .selectAll(".tick text") + // Right-align + .style("text-anchor", xAxisProperties.tick_rotation < 0.0 ? "end" : "start") + // Rotate tick labels + .attr("dx", xAxisProperties.tick_rotation < 0.0 ? "-.8em" : ".8em") + .attr("dy", xAxisProperties.tick_rotation < 0.0 ? "-.15em" : ".15em") + .attr("transform","rotate(" + xAxisProperties.tick_rotation + ")") + // Scale font + .style("font-size", xAxisProperties.tick_size) + .style("font-family", xAxisProperties.tick_font) + .style("fill", viewModel.plotProperties.displayPlot ? xAxisProperties.tick_colour : "#FFFFFF"); + + const currNode: SVGGElement = this.xAxisGroup.selectAll(".xaxisgroup").selectChildren().node() as SVGGElement; + const xAxisCoordinates: DOMRect = currNode.getBoundingClientRect() as DOMRect; + + // Update padding and re-draw axis if large tick values rendered outside of plot + const tickBelowPlotAmount: number = xAxisCoordinates.bottom - viewModel.plotProperties.height; + const tickLeftofPlotAmount: number = xAxisCoordinates.left; + if ((tickBelowPlotAmount > 0 || tickLeftofPlotAmount < 0)) { + if (!this.refreshingAxis) { + this.refreshingAxis = true + viewModel.plotProperties.yAxis.end_padding += tickBelowPlotAmount; + viewModel.plotProperties.initialiseScale(); + this.draw(viewModel); + } + } + this.refreshingAxis = false + + const bottomMidpoint: number = viewModel.plotProperties.height - (viewModel.plotProperties.height - xAxisCoordinates.bottom) / 2.5; + this.xAxisGroup + .append("text") + .classed("xaxislabel", true) + .attr("x",viewModel.plotProperties.width/2) + .attr("y", bottomMidpoint) + .style("text-anchor", "middle") + .text(xAxisProperties.label) + .style("font-size", xAxisProperties.label_size) + .style("font-family", xAxisProperties.label_font) + .style("fill", xAxisProperties.label_colour); + } + + constructor(svg: d3.Selection) { + this.xAxisGroup = svg.append("g"); + } +} +export default svgXAxisClass diff --git a/src/Classes/svgYAxisClass.ts b/src/Classes/svgYAxisClass.ts new file mode 100644 index 0000000..b2a0a6e --- /dev/null +++ b/src/Classes/svgYAxisClass.ts @@ -0,0 +1,79 @@ +import * as d3 from "d3"; +import viewModelClass from "./viewModelClass"; +import { axisProperties } from "./plotPropertiesClass"; +type SelectionBase = d3.Selection; + +class svgYAxisClass { + yAxisGroup: SelectionBase; + + draw(viewModel: viewModelClass): void { + this.yAxisGroup.selectAll(".yaxisgroup").remove() + this.yAxisGroup.selectAll(".yaxislabel").remove() + if (!(viewModel.plotProperties.displayPlot)) { + return; + } + + const yAxisProperties: axisProperties = viewModel.plotProperties.yAxis; + let yAxis: d3.Axis; + const yaxis_sig_figs: number = viewModel.inputSettings.y_axis.ylimit_sig_figs; + const sig_figs: number = yaxis_sig_figs === null ? viewModel.inputSettings.spc.sig_figs : yaxis_sig_figs; + const multiplier: number = viewModel.inputSettings.spc.multiplier; + + if (viewModel.plotProperties.displayPlot) { + if (yAxisProperties.ticks) { + yAxis = d3.axisLeft(viewModel.plotProperties.yScale); + if (yAxisProperties.tick_count) { + yAxis.ticks(yAxisProperties.tick_count) + } + yAxis.tickFormat( + d => { + return viewModel.inputData.percentLabels + ? (d * (multiplier === 100 ? 1 : (multiplier === 1 ? 100 : multiplier))).toFixed(sig_figs) + "%" + : (d).toFixed(sig_figs); + } + ); + } else { + yAxis = d3.axisLeft(viewModel.plotProperties.yScale).tickValues([]); + } + } else { + yAxis = d3.axisLeft(viewModel.plotProperties.yScale) + } + + this.yAxisGroup + .append('g') + .classed("yaxisgroup", true) + .call(yAxis) + .attr("color", yAxisProperties.colour) + .attr("transform", "translate(" + viewModel.plotProperties.xAxis.start_padding + ",0)") + .selectAll(".tick text") + // Right-align + .style("text-anchor", "right") + // Rotate tick labels + .attr("transform","rotate(" + yAxisProperties.tick_rotation + ")") + // Scale font + .style("font-size", yAxisProperties.tick_size) + .style("font-family", yAxisProperties.tick_font) + .style("fill", yAxisProperties.tick_colour); + + const currNode: SVGGElement = this.yAxisGroup.selectAll(".yaxisgroup").selectChildren().node() as SVGGElement; + const yAxisCoordinates: DOMRect = currNode.getBoundingClientRect() as DOMRect; + const leftMidpoint: number = yAxisCoordinates.x * 0.7; + + this.yAxisGroup + .append("text") + .classed("yaxislabel", true) + .attr("x",leftMidpoint) + .attr("y",viewModel.plotProperties.height/2) + .attr("transform","rotate(-90," + leftMidpoint +"," + viewModel.plotProperties.height/2 +")") + .text(yAxisProperties.label) + .style("text-anchor", "middle") + .style("font-size", yAxisProperties.label_size) + .style("font-family", yAxisProperties.label_font) + .style("fill", yAxisProperties.label_colour); + } + + constructor(svg: d3.Selection) { + this.yAxisGroup = svg.append("g"); + } +} +export default svgYAxisClass diff --git a/src/visual.ts b/src/visual.ts index 3ba7e50..131a040 100644 --- a/src/visual.ts +++ b/src/visual.ts @@ -19,46 +19,23 @@ import IVisualEventService = extensibility.IVisualEventService; import viewModelClass from "./Classes/viewModelClass" import { plotData } from "./Classes/viewModelClass"; import * as d3 from "d3"; -import svgObjectClass from "./Classes/svgObjectClass" -import svgIconClass from "./Classes/svgIconClass" -import svgSelectionClass from "./Classes/svgSelectionClass" -import { axisProperties } from "./Classes/plotPropertiesClass" -import svgLinesClass from "./Classes/svgLinesClass"; -import svgDotsClass from "./Classes/svgDotsClass"; -import svgTooltipLineClass from "./Classes/svgTooltipLineClass"; - -type SelectionAny = d3.Selection; +import plottingClass from "./Classes/plottingClass"; export class Visual implements IVisual { private host: IVisualHost; + private plotting: plottingClass; private updateOptions: VisualUpdateOptions; - private svg: d3.Selection; - private svgObjects: svgObjectClass; - private svgIcons: svgIconClass; - private svgLines: svgLinesClass; - private svgDots: svgDotsClass - private svgTooltipLine: svgTooltipLineClass; - private svgSelections: svgSelectionClass; private viewModel: viewModelClass; private selectionManager: ISelectionManager; // Service for notifying external clients (export to powerpoint/pdf) of rendering status private events: IVisualEventService; - private refreshingAxis: boolean; constructor(options: VisualConstructorOptions) { console.log("Constructor start") console.log(options) this.events = options.host.eventService; this.host = options.host; - this.svg = d3.select(options.element) - .append("svg"); - - this.svgObjects = new svgObjectClass(this.svg); - this.svgIcons = new svgIconClass(this.svg); - this.svgLines = new svgLinesClass(this.svg); - this.svgDots = new svgDotsClass(this.svg); - this.svgTooltipLine = new svgTooltipLineClass(this.svg); - this.svgSelections = new svgSelectionClass(); + this.plotting = new plottingClass(options); this.viewModel = new viewModelClass(); this.viewModel.firstRun = true; @@ -82,37 +59,17 @@ export class Visual implements IVisual { host: this.host }); console.log(this.viewModel) - console.log("svgSelections start") - this.svgSelections.update({ svgObjects: this.svgObjects, - viewModel: this.viewModel}); - console.log("svg scale start") - this.svg.attr("width", this.viewModel.plotProperties.width) - .attr("height", this.viewModel.plotProperties.height); - - console.log("TooltipTracking start") - this.svgTooltipLine.draw(this.viewModel) - this.initTooltipTracking(); - - console.log("Draw axes start") - this.drawXAxis(); - this.drawYAxis(); - - console.log("Draw Lines start") - this.svgLines.draw(this.viewModel) - - console.log("Draw dots start") - this.svgDots.draw(this.viewModel) - this.addDotsInteractivity(); - - console.log("Draw icons start") - this.svgIcons.drawIcons(this.viewModel) + this.plotting.svg + .attr("width", this.viewModel.plotProperties.width) + .attr("height", this.viewModel.plotProperties.height); + this.plotting.draw(this.viewModel) + this.addInteractivity(); this.updateHighlighting(); - this.addContextMenu(); - this.svg.on('click', () => { + this.plotting.svg.on('click', () => { this.selectionManager.clear(); this.updateHighlighting(); }); @@ -131,189 +88,14 @@ export class Visual implements IVisual { return this.viewModel.inputSettings.createSettingsEntry(options.objectName); } - initTooltipTracking(): void { - const xAxisLine = this.svgSelections - .tooltipLineSelection - .enter() - .append("rect") - .merge(this.svgSelections.tooltipLineSelection); - xAxisLine.classed("ttip-line", true); - xAxisLine.attr("stroke-width", "1px") - .attr("width", ".5px") - .attr("height", this.viewModel.plotProperties.height) - .style("fill-opacity", 0); - - this.svgTooltipLine - .tooltipLineGroup - .selectAll(".obs-sel") - .selectChildren() - .on("mousemove", (event) => { - if (this.viewModel.plotProperties.displayPlot) { - const xValue: number = this.viewModel.plotProperties.xScale.invert(event.pageX); - const xRange: number[] = this.viewModel - .plotPoints - .map(d => d.x) - .map(d => Math.abs(d - xValue)); - const nearestDenominator: number = d3.leastIndex(xRange,(a,b) => a-b) as number; - const scaled_x: number = this.viewModel.plotProperties.xScale(this.viewModel.plotPoints[nearestDenominator].x) - const scaled_y: number = this.viewModel.plotProperties.yScale(this.viewModel.plotPoints[nearestDenominator].value) - - this.host.tooltipService.show({ - dataItems: this.viewModel.plotPoints[nearestDenominator].tooltip, - identities: [this.viewModel.plotPoints[nearestDenominator].identity], - coordinates: [scaled_x, scaled_y], - isTouchEvent: false - }); - xAxisLine.style("fill-opacity", 1).attr("transform", "translate(" + scaled_x + ",0)"); - } - }) - .on("mouseleave", () => { - if (this.viewModel.plotProperties.displayPlot) { - this.host.tooltipService.hide({ - immediately: true, - isTouchEvent: false - }); - xAxisLine.style("fill-opacity", 0); - } - }); - xAxisLine.exit().remove() - } - - drawXAxis(): void { - const xAxisProperties: axisProperties = this.viewModel.plotProperties.xAxis; - let xAxis: d3.Axis; - - if (xAxisProperties.ticks) { - xAxis = d3.axisBottom(this.viewModel.plotProperties.xScale); - if (xAxisProperties.tick_count) { - xAxis.ticks(xAxisProperties.tick_count) - } - if (this.viewModel.tickLabels) { - xAxis.tickFormat(d => { - return this.viewModel.tickLabels.map(d => d.x).includes(d) - ? this.viewModel.tickLabels[d].label - : ""; - }) - } - } else { - xAxis = d3.axisBottom(this.viewModel.plotProperties.xScale).tickValues([]); - } - - const axisHeight: number = this.viewModel.plotProperties.height - this.viewModel.plotProperties.yAxis.end_padding; - - this.svgObjects - .xAxisGroup - .call(xAxis) - .attr("color", this.viewModel.plotProperties.displayPlot ? xAxisProperties.colour : "#FFFFFF") - // Plots the axis at the correct height - .attr("transform", "translate(0, " + axisHeight + ")") - .selectAll(".tick text") - // Right-align - .style("text-anchor", xAxisProperties.tick_rotation < 0.0 ? "end" : "start") - // Rotate tick labels - .attr("dx", xAxisProperties.tick_rotation < 0.0 ? "-.8em" : ".8em") - .attr("dy", xAxisProperties.tick_rotation < 0.0 ? "-.15em" : ".15em") - .attr("transform","rotate(" + xAxisProperties.tick_rotation + ")") - // Scale font - .style("font-size", xAxisProperties.tick_size) - .style("font-family", xAxisProperties.tick_font) - .style("fill", this.viewModel.plotProperties.displayPlot ? xAxisProperties.tick_colour : "#FFFFFF"); - - const currNode: SVGGElement = this.svgObjects.xAxisGroup.node() as SVGGElement; - const xAxisCoordinates: DOMRect = currNode.getBoundingClientRect() as DOMRect; - - // Update padding and re-draw axis if large tick values rendered outside of plot - const tickBelowPlotAmount: number = xAxisCoordinates.bottom - this.viewModel.plotProperties.height; - const tickLeftofPlotAmount: number = xAxisCoordinates.left; - if ((tickBelowPlotAmount > 0 || tickLeftofPlotAmount < 0)) { - if (!this.refreshingAxis) { - this.refreshingAxis = true - this.viewModel.plotProperties.yAxis.end_padding += tickBelowPlotAmount; - this.viewModel.plotProperties.initialiseScale(); - this.drawXAxis(); - } - } - this.refreshingAxis = false - - const bottomMidpoint: number = this.viewModel.plotProperties.height - (this.viewModel.plotProperties.height - xAxisCoordinates.bottom) / 2.5; - - this.svgObjects - .xAxisLabels - .attr("x",this.viewModel.plotProperties.width/2) - .attr("y", bottomMidpoint) - .style("text-anchor", "middle") - .text(xAxisProperties.label) - .style("font-size", xAxisProperties.label_size) - .style("font-family", xAxisProperties.label_font) - .style("fill", this.viewModel.plotProperties.displayPlot ? xAxisProperties.label_colour : "#FFFFFF"); - } - - drawYAxis(): void { - const yAxisProperties: axisProperties = this.viewModel.plotProperties.yAxis; - let yAxis: d3.Axis; - const yaxis_sig_figs: number = this.viewModel.inputSettings.y_axis.ylimit_sig_figs; - const sig_figs: number = yaxis_sig_figs === null ? this.viewModel.inputSettings.spc.sig_figs : yaxis_sig_figs; - const multiplier: number = this.viewModel.inputSettings.spc.multiplier; - - if (this.viewModel.plotProperties.displayPlot) { - if (yAxisProperties.ticks) { - yAxis = d3.axisLeft(this.viewModel.plotProperties.yScale); - if (yAxisProperties.tick_count) { - yAxis.ticks(yAxisProperties.tick_count) - } - yAxis.tickFormat( - d => { - return this.viewModel.inputData.percentLabels - ? (d * (multiplier === 100 ? 1 : (multiplier === 1 ? 100 : multiplier))).toFixed(sig_figs) + "%" - : (d).toFixed(sig_figs); - } - ); - } else { - yAxis = d3.axisLeft(this.viewModel.plotProperties.yScale).tickValues([]); - } - } else { - yAxis = d3.axisLeft(this.viewModel.plotProperties.yScale) - } - - // Draw axes on plot - this.svgObjects - .yAxisGroup - .call(yAxis) - .attr("color", this.viewModel.plotProperties.displayPlot ? yAxisProperties.colour : "#FFFFFF") - .attr("transform", "translate(" + this.viewModel.plotProperties.xAxis.start_padding + ",0)") - .selectAll(".tick text") - // Right-align - .style("text-anchor", "right") - // Rotate tick labels - .attr("transform","rotate(" + yAxisProperties.tick_rotation + ")") - // Scale font - .style("font-size", yAxisProperties.tick_size) - .style("font-family", yAxisProperties.tick_font) - .style("fill", this.viewModel.plotProperties.displayPlot ? yAxisProperties.tick_colour : "#FFFFFF"); - - const currNode: SVGGElement = this.svgObjects.yAxisGroup.node() as SVGGElement; - const yAxisCoordinates: DOMRect = currNode.getBoundingClientRect() as DOMRect; - const leftMidpoint: number = yAxisCoordinates.x * 0.7; - - this.svgObjects - .yAxisLabels - .attr("x",leftMidpoint) - .attr("y",this.viewModel.plotProperties.height/2) - .attr("transform","rotate(-90," + leftMidpoint +"," + this.viewModel.plotProperties.height/2 +")") - .text(yAxisProperties.label) - .style("text-anchor", "middle") - .style("font-size", yAxisProperties.label_size) - .style("font-family", yAxisProperties.label_font) - .style("fill", this.viewModel.plotProperties.displayPlot ? yAxisProperties.label_colour : "#FFFFFF"); - } - - addDotsInteractivity(): void { + addInteractivity(): void { if (!this.viewModel.plotProperties.displayPlot) { return; } // Change opacity (highlighting) with selections in other plots // Specify actions to take when clicking on dots - this.svgDots + this.plotting + .svgDots .dotsGroup .selectAll(".dotsgroup") .selectChildren() @@ -369,10 +151,44 @@ export class Visual implements IVisual { isTouchEvent: false }) }); + + const xAxisLine = this.plotting + .svgTooltipLine + .tooltipLineGroup + .selectAll(".ttip-line") + .selectChildren(); + + this.plotting + .svgTooltipLine + .tooltipLineGroup + .selectAll(".obs-sel") + .selectChildren() + .on("mousemove", (event) => { + const xValue: number = this.viewModel.plotProperties.xScale.invert(event.pageX); + const xRange: number[] = this.viewModel + .plotPoints + .map(d => d.x) + .map(d => Math.abs(d - xValue)); + const nearestDenominator: number = d3.leastIndex(xRange,(a,b) => a-b); + const scaled_x: number = this.viewModel.plotProperties.xScale(this.viewModel.plotPoints[nearestDenominator].x) + const scaled_y: number = this.viewModel.plotProperties.yScale(this.viewModel.plotPoints[nearestDenominator].value) + + this.host.tooltipService.show({ + dataItems: this.viewModel.plotPoints[nearestDenominator].tooltip, + identities: [this.viewModel.plotPoints[nearestDenominator].identity], + coordinates: [scaled_x, scaled_y], + isTouchEvent: false + }); + xAxisLine.style("fill-opacity", 1).attr("transform", `translate(${scaled_x},0)`); + }) + .on("mouseleave", () => { + this.host.tooltipService.hide({ immediately: true, isTouchEvent: false }); + xAxisLine.style("fill-opacity", 0); + }); } addContextMenu(): void { - this.svg.on('contextmenu', (event) => { + this.plotting.svg.on('contextmenu', (event) => { const eventTarget: EventTarget = event.target; const dataPoint: plotData = (d3.select(eventTarget).datum()); this.selectionManager.showContextMenu(dataPoint ? dataPoint.identity : {}, { @@ -393,7 +209,7 @@ export class Visual implements IVisual { const opacityFull: number = this.viewModel.inputSettings.scatter.opacity; const opacityReduced: number = this.viewModel.inputSettings.scatter.opacity_unselected; - this.svgLines.highlight(anyHighlights, allSelectionIDs, opacityFull, opacityReduced) - this.svgDots.highlight(anyHighlights, allSelectionIDs, opacityFull, opacityReduced) + this.plotting.svgLines.highlight(anyHighlights, allSelectionIDs, opacityFull, opacityReduced) + this.plotting.svgDots.highlight(anyHighlights, allSelectionIDs, opacityFull, opacityReduced) } } From 158872c3117d3b8e360cd01df73e04c1df84ce2e Mon Sep 17 00:00:00 2001 From: Andrew Johnson Date: Mon, 17 Jul 2023 02:15:16 +0300 Subject: [PATCH 3/6] Simplify --- src/Classes/plottingClass.ts | 21 ++++----------------- 1 file changed, 4 insertions(+), 17 deletions(-) diff --git a/src/Classes/plottingClass.ts b/src/Classes/plottingClass.ts index f56a385..cab152c 100644 --- a/src/Classes/plottingClass.ts +++ b/src/Classes/plottingClass.ts @@ -11,20 +11,7 @@ import svgXAxisClass from "./svgXAxisClass"; import svgYAxisClass from "./svgYAxisClass"; import viewModelClass from "./viewModelClass"; -interface svgTypes { - svg: d3.Selection, - svgTooltipLine: svgTooltipLineClass, - svgXAxis: svgXAxisClass, - svgYAxis: svgYAxisClass, - svgDots: svgDotsClass, - svgLines: svgLinesClass, - svgIcons: svgIconClass, - draw: (viewModel: viewModelClass) => void -} - -type svgKeys = Exclude; - -class plottingClass implements svgTypes { +class plottingClass { svg: d3.Selection; svgTooltipLine: svgTooltipLineClass; svgXAxis: svgXAxisClass; @@ -34,9 +21,9 @@ class plottingClass implements svgTypes { svgIcons: svgIconClass; draw(viewModel: viewModelClass): void { - Object.getOwnPropertyNames(this).filter(d => !(["draw", "svg"].includes(d))).forEach(key => { - this[key as svgKeys].draw(viewModel); - }) + Object.getOwnPropertyNames(this) + .filter(d => !(["draw", "svg"].includes(d))) + .forEach(key => this[key].draw(viewModel)); } constructor(options: VisualConstructorOptions) { From 342f05eddd8002e5af7f68114d9c2d1f45d8d1fc Mon Sep 17 00:00:00 2001 From: Andrew Johnson Date: Mon, 17 Jul 2023 02:17:03 +0300 Subject: [PATCH 4/6] Simplify --- src/Classes/plottingClass.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/Classes/plottingClass.ts b/src/Classes/plottingClass.ts index cab152c..94488d5 100644 --- a/src/Classes/plottingClass.ts +++ b/src/Classes/plottingClass.ts @@ -1,8 +1,6 @@ import * as d3 from "d3"; import powerbi from "powerbi-visuals-api"; -import extensibility = powerbi.extensibility; -import ex_visual = extensibility.visual; -import VisualConstructorOptions = ex_visual.VisualConstructorOptions; +import VisualConstructorOptions = powerbi.extensibility.visual.VisualConstructorOptions; import svgDotsClass from "./svgDotsClass"; import svgIconClass from "./svgIconClass"; import svgLinesClass from "./svgLinesClass"; From 0b553057909475931abaae15ef8423916a150805 Mon Sep 17 00:00:00 2001 From: Andrew Johnson Date: Mon, 17 Jul 2023 02:30:41 +0300 Subject: [PATCH 5/6] Simplify --- src/Classes/plottingClass.ts | 2 ++ src/visual.ts | 48 ++++++++++++++---------------------- 2 files changed, 21 insertions(+), 29 deletions(-) diff --git a/src/Classes/plottingClass.ts b/src/Classes/plottingClass.ts index 94488d5..dc9a7a2 100644 --- a/src/Classes/plottingClass.ts +++ b/src/Classes/plottingClass.ts @@ -19,6 +19,8 @@ class plottingClass { svgIcons: svgIconClass; draw(viewModel: viewModelClass): void { + this.svg.attr("width", viewModel.plotProperties.width) + .attr("height", viewModel.plotProperties.height); Object.getOwnPropertyNames(this) .filter(d => !(["draw", "svg"].includes(d))) .forEach(key => this[key].draw(viewModel)); diff --git a/src/visual.ts b/src/visual.ts index 131a040..a37302a 100644 --- a/src/visual.ts +++ b/src/visual.ts @@ -4,18 +4,15 @@ import "core-js/stable"; import "regenerator-runtime/runtime"; import "../style/visual.less"; import powerbi from "powerbi-visuals-api"; -import extensibility = powerbi.extensibility; -import visuals = powerbi.visuals; -import ex_visual = extensibility.visual; -import IVisual = extensibility.IVisual; -import VisualConstructorOptions = ex_visual.VisualConstructorOptions; -import VisualUpdateOptions = ex_visual.VisualUpdateOptions; +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 = ex_visual.IVisualHost; -import ISelectionManager = extensibility.ISelectionManager; -import ISelectionId = visuals.ISelectionId; -import IVisualEventService = extensibility.IVisualEventService; +import IVisualHost = powerbi.extensibility.visual.IVisualHost; +import ISelectionManager = powerbi.extensibility.ISelectionManager; +import ISelectionId = powerbi.visuals.ISelectionId; +import IVisualEventService = powerbi.extensibility.IVisualEventService; import viewModelClass from "./Classes/viewModelClass" import { plotData } from "./Classes/viewModelClass"; import * as d3 from "d3"; @@ -41,9 +38,7 @@ export class Visual implements IVisual { this.selectionManager = this.host.createSelectionManager(); - this.selectionManager.registerOnSelectCallback(() => { - this.updateHighlighting(); - }) + this.selectionManager.registerOnSelectCallback(() => this.updateHighlighting()); console.log("Constructor finish") } @@ -55,16 +50,11 @@ export class Visual implements IVisual { this.updateOptions = options; console.log("viewModel start") - this.viewModel.update({ options: options, - host: this.host }); - console.log(this.viewModel) + this.viewModel.update({ options: options, host: this.host }); - console.log("svg scale start") - this.plotting.svg - .attr("width", this.viewModel.plotProperties.width) - .attr("height", this.viewModel.plotProperties.height); + console.log("Draw plot") + this.plotting.draw(this.viewModel); - this.plotting.draw(this.viewModel) this.addInteractivity(); this.updateHighlighting(); this.addContextMenu(); @@ -138,17 +128,17 @@ export class Visual implements IVisual { const y = event.pageY; this.host.tooltipService.show({ - dataItems: d.tooltip, - identities: [d.identity], - coordinates: [x, y], - isTouchEvent: false + dataItems: d.tooltip, + identities: [d.identity], + coordinates: [x, y], + isTouchEvent: false }); }) // Hide tooltip when mouse moves out of dot .on("mouseout", () => { this.host.tooltipService.hide({ - immediately: true, - isTouchEvent: false + immediately: true, + isTouchEvent: false }) }); @@ -192,8 +182,8 @@ export class Visual implements IVisual { const eventTarget: EventTarget = event.target; const dataPoint: plotData = (d3.select(eventTarget).datum()); this.selectionManager.showContextMenu(dataPoint ? dataPoint.identity : {}, { - x: event.clientX, - y: event.clientY + x: event.clientX, + y: event.clientY }); event.preventDefault(); }); From 952d9dfee9dd47b5bdda516eaad5b7bc19f7f3b0 Mon Sep 17 00:00:00 2001 From: Andrew Johnson Date: Mon, 17 Jul 2023 02:33:53 +0300 Subject: [PATCH 6/6] Simplify --- src/Classes/svgXAxisClass.ts | 4 ++-- src/Classes/svgYAxisClass.ts | 28 ++++++++++++---------------- 2 files changed, 14 insertions(+), 18 deletions(-) diff --git a/src/Classes/svgXAxisClass.ts b/src/Classes/svgXAxisClass.ts index d0bb695..1574540 100644 --- a/src/Classes/svgXAxisClass.ts +++ b/src/Classes/svgXAxisClass.ts @@ -39,7 +39,7 @@ class svgXAxisClass { .append('g') .classed("xaxisgroup", true) .call(xAxis) - .attr("color", viewModel.plotProperties.displayPlot ? xAxisProperties.colour : "#FFFFFF") + .attr("color", xAxisProperties.colour) // Plots the axis at the correct height .attr("transform", "translate(0, " + axisHeight + ")") .selectAll(".tick text") @@ -52,7 +52,7 @@ class svgXAxisClass { // Scale font .style("font-size", xAxisProperties.tick_size) .style("font-family", xAxisProperties.tick_font) - .style("fill", viewModel.plotProperties.displayPlot ? xAxisProperties.tick_colour : "#FFFFFF"); + .style("fill", xAxisProperties.tick_colour); const currNode: SVGGElement = this.xAxisGroup.selectAll(".xaxisgroup").selectChildren().node() as SVGGElement; const xAxisCoordinates: DOMRect = currNode.getBoundingClientRect() as DOMRect; diff --git a/src/Classes/svgYAxisClass.ts b/src/Classes/svgYAxisClass.ts index b2a0a6e..21d65c7 100644 --- a/src/Classes/svgYAxisClass.ts +++ b/src/Classes/svgYAxisClass.ts @@ -19,24 +19,20 @@ class svgYAxisClass { const sig_figs: number = yaxis_sig_figs === null ? viewModel.inputSettings.spc.sig_figs : yaxis_sig_figs; const multiplier: number = viewModel.inputSettings.spc.multiplier; - if (viewModel.plotProperties.displayPlot) { - if (yAxisProperties.ticks) { - yAxis = d3.axisLeft(viewModel.plotProperties.yScale); - if (yAxisProperties.tick_count) { - yAxis.ticks(yAxisProperties.tick_count) - } - yAxis.tickFormat( - d => { - return viewModel.inputData.percentLabels - ? (d * (multiplier === 100 ? 1 : (multiplier === 1 ? 100 : multiplier))).toFixed(sig_figs) + "%" - : (d).toFixed(sig_figs); - } - ); - } else { - yAxis = d3.axisLeft(viewModel.plotProperties.yScale).tickValues([]); + if (yAxisProperties.ticks) { + yAxis = d3.axisLeft(viewModel.plotProperties.yScale); + if (yAxisProperties.tick_count) { + yAxis.ticks(yAxisProperties.tick_count) } + yAxis.tickFormat( + d => { + return viewModel.inputData.percentLabels + ? (d * (multiplier === 100 ? 1 : (multiplier === 1 ? 100 : multiplier))).toFixed(sig_figs) + "%" + : (d).toFixed(sig_figs); + } + ); } else { - yAxis = d3.axisLeft(viewModel.plotProperties.yScale) + yAxis = d3.axisLeft(viewModel.plotProperties.yScale).tickValues([]); } this.yAxisGroup