From 3870dedc06f373762329853729ab3609e6f7138f Mon Sep 17 00:00:00 2001 From: Andrew Johnson Date: Sun, 7 Apr 2024 15:29:12 +0300 Subject: [PATCH] Summary Table: Improve dynamic sizing, support interactivity --- src/Classes/viewModelClass.ts | 12 ++- src/D3 Plotting Functions/addContextMenu.ts | 4 +- src/D3 Plotting Functions/drawSummaryTable.ts | 57 +++++++++++--- .../updateHighlighting.ts | 9 ++- src/Functions/buildTooltip.ts | 75 +++++++------------ src/visual.ts | 15 +++- 6 files changed, 107 insertions(+), 65 deletions(-) diff --git a/src/Classes/viewModelClass.ts b/src/Classes/viewModelClass.ts index 6245fb8..3262a08 100644 --- a/src/Classes/viewModelClass.ts +++ b/src/Classes/viewModelClass.ts @@ -30,6 +30,10 @@ export type summaryTableRowData = { ul99: number; speclimits_lower: number; speclimits_upper: number; + astpoint: string; + trend: string; + shift: string; + two_in_three: string; } export type plotData = { @@ -237,7 +241,11 @@ export default class viewModelClass { ul95: this.controlLimits?.ul95?.[i], ul99: this.controlLimits?.ul99?.[i], speclimits_lower: this.controlLimits?.speclimits_lower?.[i], - speclimits_upper: this.controlLimits?.speclimits_upper?.[i] + speclimits_upper: this.controlLimits?.speclimits_upper?.[i], + astpoint: this.outliers.astpoint[i], + trend: this.outliers.trend[i], + shift: this.outliers.shift[i], + two_in_three: this.outliers.two_in_three[i] } this.plotPoints.push({ @@ -250,7 +258,7 @@ export default class viewModelClass { this.inputData.limitInputArgs.keys[i].id) .createSelectionId(), highlighted: !isNullOrUndefined(this.inputData.highlights?.[index]), - tooltip: buildTooltip(i, this.controlLimits, this.outliers, this.inputData, + tooltip: buildTooltip(table_row, this.inputData?.tooltips?.[index], this.inputSettings.settings, this.inputSettings.derivedSettings) }) this.tickLabels.push({x: index, label: this.controlLimits.keys[i].label}); diff --git a/src/D3 Plotting Functions/addContextMenu.ts b/src/D3 Plotting Functions/addContextMenu.ts index 6f283d4..884b332 100644 --- a/src/D3 Plotting Functions/addContextMenu.ts +++ b/src/D3 Plotting Functions/addContextMenu.ts @@ -1,8 +1,8 @@ import * as d3 from "./D3 Modules"; import type { plotData } from "../Classes"; -import type { svgBaseType, Visual } from "../visual"; +import type { divBaseType, svgBaseType, Visual } from "../visual"; -export default function addContextMenu(selection: svgBaseType, visualObj: Visual) { +export default function addContextMenu(selection: svgBaseType | divBaseType, visualObj: Visual) { if (!(visualObj.viewModel.plotProperties.displayPlot)) { selection.on("contextmenu", () => { return; }); return; diff --git a/src/D3 Plotting Functions/drawSummaryTable.ts b/src/D3 Plotting Functions/drawSummaryTable.ts index dc6d5aa..940352a 100644 --- a/src/D3 Plotting Functions/drawSummaryTable.ts +++ b/src/D3 Plotting Functions/drawSummaryTable.ts @@ -1,16 +1,20 @@ import { plotData } from "../Classes/viewModelClass"; import type { divBaseType, Visual } from "../visual"; +import * as d3 from "./D3 Modules"; +import updateHighlighting from "./updateHighlighting"; export default function drawSummaryTable(selection: divBaseType, visualObj: Visual) { - selection.select("svg").attr("width", 0).attr("height", 0); - + selection.style("height", "100%").style("width", "100%"); if (selection.select(".table-group").empty()) { - const table = selection.append("table").classed("table-group", true); - table.append("thead") - .append("tr") - .classed("table-header", true); - table.append('tbody') - .classed("table-body", true); + const table = selection.append("table") + .classed("table-group", true) + .style("border-collapse", "collapse") + .style("border", "2px black solid") + .style("width", "100%") + .style("height", "100%"); + + table.append("thead").append("tr").classed("table-header", true); + table.append('tbody').classed("table-body", true); } const plotPoints: plotData[] = visualObj.viewModel.plotPoints; @@ -19,14 +23,47 @@ export default function drawSummaryTable(selection: divBaseType, visualObj: Visu .selectAll("th") .data(Object.keys(plotPoints[0].table_row)) .join("th") - .text((d) => d); + .text((d) => d) + .style("border", "1px black solid") + .style("padding", "5px") + .style("background-color", "lightgray") + .style("font-weight", "bold") + .style("text-transform", "uppercase"); selection.select(".table-body") .selectAll('tr') .data(plotPoints) .join('tr') + .on("click", (event, d: plotData) => { + if (visualObj.host.hostCapabilities.allowInteractions) { + visualObj.selectionManager + .select(d.identity, event.ctrlKey || event.metaKey) + .then(() => { + visualObj.svg.call(updateHighlighting, visualObj); + }); + event.stopPropagation(); + } + }) .selectAll('td') .data((d) => Object.values(d.table_row)) .join('td') - .text((d) => d); + .text((d) => { + return typeof d === "number" ? d.toFixed(visualObj.viewModel.inputSettings.settings.spc.sig_figs) : d; + }) + .on("mouseover", (event) => { + d3.select(event.target).style("background-color", "lightgray"); + }) + .on("mouseout", (event) => { + d3.select(event.target).style("background-color", "white"); + }) + .style("border", "1px black solid") + .style("padding", "5px") + .style("font-size", "12px") + + selection.on('click', () => { + visualObj.selectionManager.clear(); + visualObj.svg.call(updateHighlighting, visualObj); + }); + + visualObj.svg.call(updateHighlighting, visualObj); } diff --git a/src/D3 Plotting Functions/updateHighlighting.ts b/src/D3 Plotting Functions/updateHighlighting.ts index 800d571..79049e6 100644 --- a/src/D3 Plotting Functions/updateHighlighting.ts +++ b/src/D3 Plotting Functions/updateHighlighting.ts @@ -6,7 +6,6 @@ import type { svgBaseType, Visual } from "../visual"; export default function updateHighlighting(selection: svgBaseType, visualObj: Visual) { const anyHighlights: boolean = visualObj.viewModel.inputData ? visualObj.viewModel.inputData.anyHighlights : false; const allSelectionIDs: ISelectionId[] = visualObj.selectionManager.getSelectionIds() as ISelectionId[]; - const opacityFull: number = visualObj.viewModel.inputSettings.settings.scatter.opacity; const opacityReduced: number = visualObj.viewModel.inputSettings.settings.scatter.opacity_unselected; @@ -15,6 +14,7 @@ export default function updateHighlighting(selection: svgBaseType, visualObj: Vi : opacityFull; selection.selectAll(".dotsgroup").selectChildren().style("fill-opacity", defaultOpacity); selection.selectAll(".linesgroup").style("stroke-opacity", defaultOpacity); + visualObj.div.selectAll(".table-body").selectChildren().style("opacity", defaultOpacity); if (anyHighlights || (allSelectionIDs.length > 0)) { selection.selectAll(".dotsgroup").selectChildren().style("fill-opacity", (dot: plotData) => { const currentPointSelected: boolean = allSelectionIDs.some((currentSelectionId: ISelectionId) => { @@ -23,5 +23,12 @@ export default function updateHighlighting(selection: svgBaseType, visualObj: Vi const currentPointHighlighted: boolean = dot.highlighted; return (currentPointSelected || currentPointHighlighted) ? dot.aesthetics.opacity : dot.aesthetics.opacity_unselected; }) + visualObj.div.selectAll(".table-body").selectChildren().style("opacity", (dot: plotData) => { + const currentPointSelected: boolean = allSelectionIDs.some((currentSelectionId: ISelectionId) => { + return currentSelectionId.includes(dot.identity); + }); + const currentPointHighlighted: boolean = dot.highlighted; + return (currentPointSelected || currentPointHighlighted) ? dot.aesthetics.opacity : dot.aesthetics.opacity_unselected; + }) } } diff --git a/src/Functions/buildTooltip.ts b/src/Functions/buildTooltip.ts index 13bd82a..18cb5b9 100644 --- a/src/Functions/buildTooltip.ts +++ b/src/Functions/buildTooltip.ts @@ -1,8 +1,8 @@ import type powerbi from "powerbi-visuals-api"; type VisualTooltipDataItem = powerbi.extensibility.VisualTooltipDataItem; -import type { controlLimitsObject, defaultSettingsType, derivedSettingsClass, outliersObject } from "../Classes"; -import type { dataObject } from "./extractInputData"; +import type { defaultSettingsType, derivedSettingsClass } from "../Classes"; import isNullOrUndefined from "./isNullOrUndefined"; +import { summaryTableRowData } from "../Classes/viewModelClass"; /** * Builds the tooltip data for a specific index in the chart. @@ -17,70 +17,51 @@ import isNullOrUndefined from "./isNullOrUndefined"; */ // ESLint errors due to number of lines in function, but would reduce readability to separate further /* eslint-disable max-lines-per-function */ -export default function buildTooltip(index: number, - controlLimits: controlLimitsObject, - outliers: outliersObject, - inputData: dataObject, +export default function buildTooltip(table_row: summaryTableRowData, + inputTooltips: powerbi.extensibility.VisualTooltipDataItem[], inputSettings: defaultSettingsType, derivedSettings: derivedSettingsClass): VisualTooltipDataItem[] { - const numerator: number = controlLimits.numerators?.[index]; - const denominator: number = controlLimits.denominators?.[index]; - const target: number = controlLimits.targets[index]; - const alt_target: number = controlLimits?.alt_targets?.[index]; - const speclimits_lower: number = controlLimits?.speclimits_lower?.[index]; - const speclimits_upper: number = controlLimits?.speclimits_upper?.[index]; - const limits = { - ll99: controlLimits?.ll99?.[index], - ll95: controlLimits?.ll95?.[index], - ll68: controlLimits?.ll68?.[index], - ul68: controlLimits?.ul68?.[index], - ul95: controlLimits?.ul95?.[index], - ul99: controlLimits?.ul99?.[index] - }; - const astpoint: string = outliers.astpoint[index]; - const trend: string = outliers.trend[index]; - const shift: string = outliers.shift[index]; - const two_in_three: string = outliers.two_in_three[index]; + const ast_limit: string = inputSettings.outliers.astronomical_limit; const two_in_three_limit: string = inputSettings.outliers.two_in_three_limit; const suffix: string = derivedSettings.percentLabels ? "%" : ""; - const sig_figs: number = inputSettings.spc.sig_figs; + const tooltip: VisualTooltipDataItem[] = new Array(); tooltip.push({ displayName: "Date", - value: controlLimits.keys[index].label + value: table_row.date }); if (inputSettings.spc.ttip_show_value) { const ttip_label_value: string = inputSettings.spc.ttip_label_value; tooltip.push({ displayName: ttip_label_value === "Automatic" ? derivedSettings.chart_type_props.value_name : ttip_label_value, - value: (controlLimits.values[index]).toFixed(sig_figs) + suffix + value: (table_row.value).toFixed(sig_figs) + suffix }) } - if(inputSettings.spc.ttip_show_numerator && !isNullOrUndefined(numerator)) { + if(inputSettings.spc.ttip_show_numerator && !isNullOrUndefined(table_row.numerator)) { tooltip.push({ displayName: inputSettings.spc.ttip_label_numerator, - value: (numerator).toFixed(derivedSettings.chart_type_props.integer_num_den ? 0 : sig_figs) + value: (table_row.numerator).toFixed(derivedSettings.chart_type_props.integer_num_den ? 0 : sig_figs) }) } - if(inputSettings.spc.ttip_show_denominator && !isNullOrUndefined(denominator)) { + if(inputSettings.spc.ttip_show_denominator && !isNullOrUndefined(table_row.denominator)) { tooltip.push({ displayName: inputSettings.spc.ttip_label_denominator, - value: (denominator).toFixed(derivedSettings.chart_type_props.integer_num_den ? 0 : sig_figs) + value: (table_row.denominator).toFixed(derivedSettings.chart_type_props.integer_num_den ? 0 : sig_figs) }) } if (inputSettings.lines.show_specification && inputSettings.lines.ttip_show_specification) { - if (!isNullOrUndefined(speclimits_upper)) { + if (!isNullOrUndefined(table_row.speclimits_upper)) { tooltip.push({ displayName: `Upper ${inputSettings.lines.ttip_label_specification}`, - value: (speclimits_upper).toFixed(sig_figs) + suffix + value: (table_row.speclimits_upper).toFixed(sig_figs) + suffix }) } - if (!isNullOrUndefined(speclimits_lower)) { + if (!isNullOrUndefined(table_row.speclimits_lower)) { tooltip.push({ displayName: `Lower ${inputSettings.lines.ttip_label_specification}`, - value: (speclimits_lower).toFixed(sig_figs) + suffix + value: (table_row.speclimits_lower).toFixed(sig_figs) + suffix }) } } @@ -89,7 +70,7 @@ export default function buildTooltip(index: number, if (inputSettings.lines[`ttip_show_${limit}`] && inputSettings.lines[`show_${limit}`]) { tooltip.push({ displayName: `Upper ${inputSettings.lines[`ttip_label_${limit}`]}`, - value: (limits[`ul${limit}`]).toFixed(sig_figs) + suffix + value: (table_row[`ul${limit}`]).toFixed(sig_figs) + suffix }) } }) @@ -97,13 +78,13 @@ export default function buildTooltip(index: number, if (inputSettings.lines.show_target && inputSettings.lines.ttip_show_target) { tooltip.push({ displayName: inputSettings.lines.ttip_label_target, - value: (target).toFixed(sig_figs) + suffix + value: (table_row.target).toFixed(sig_figs) + suffix }) } - if (inputSettings.lines.show_alt_target && inputSettings.lines.ttip_show_alt_target && !isNullOrUndefined(alt_target)) { + if (inputSettings.lines.show_alt_target && inputSettings.lines.ttip_show_alt_target && !isNullOrUndefined(table_row.alt_target)) { tooltip.push({ displayName: inputSettings.lines.ttip_label_alt_target, - value: (alt_target).toFixed(sig_figs) + suffix + value: (table_row.alt_target).toFixed(sig_figs) + suffix }) } if (derivedSettings.chart_type_props.has_control_limits) { @@ -111,15 +92,15 @@ export default function buildTooltip(index: number, if (inputSettings.lines[`ttip_show_${limit}`] && inputSettings.lines[`show_${limit}`]) { tooltip.push({ displayName: `Lower ${inputSettings.lines[`ttip_label_${limit}`]}`, - value: (limits[`ll${limit}`]).toFixed(sig_figs) + suffix + value: (table_row[`ll${limit}`]).toFixed(sig_figs) + suffix }) } }) } - if (astpoint !== "none" || trend !== "none" || shift !== "none" || two_in_three !== "none") { + if ([table_row.astpoint, table_row.trend, table_row.shift, table_row.two_in_three].some(d => d !== "none")){ const patterns: string[] = new Array(); - if (astpoint !== "none") { + if (table_row.astpoint !== "none") { // Note if flagged according to non-default limit let flag_text: string = "Astronomical Point"; if (ast_limit !== "3 Sigma") { @@ -127,9 +108,9 @@ export default function buildTooltip(index: number, } patterns.push(flag_text) } - if (trend !== "none") { patterns.push("Trend") } - if (shift !== "none") { patterns.push("Shift") } - if (two_in_three !== "none") { + if (table_row.trend !== "none") { patterns.push("Trend") } + if (table_row.shift !== "none") { patterns.push("Shift") } + if (table_row.two_in_three !== "none") { // Note if flagged according to non-default limit let flag_text: string = "Two-in-Three"; if (two_in_three_limit !== "2 Sigma") { @@ -143,8 +124,8 @@ export default function buildTooltip(index: number, }) } - if (inputData.tooltips.length > 0) { - inputData.tooltips[index].forEach(customTooltip => tooltip.push(customTooltip)) + if (!isNullOrUndefined(inputTooltips) && inputTooltips.length > 0) { + inputTooltips.forEach(customTooltip => tooltip.push(customTooltip)) } return tooltip; diff --git a/src/visual.ts b/src/visual.ts index 855485c..260c166 100644 --- a/src/visual.ts +++ b/src/visual.ts @@ -23,7 +23,9 @@ export class Visual implements powerbi.extensibility.IVisual { constructor(options: powerbi.extensibility.visual.VisualConstructorOptions) { this.div = d3.select(options.element).append("div"); - this.svg = this.div.append("svg"); + this.div.style("overflow", "auto"); + + this.svg = d3.select(options.element).append("svg"); this.host = options.host; this.viewModel = new viewModelClass(); @@ -63,8 +65,11 @@ export class Visual implements powerbi.extensibility.IVisual { } if (this.viewModel.inputSettings.settings.summary_table.show_table) { - this.div.call(drawSummaryTable, this); + this.svg.attr("width", 0).attr("height", 0); + this.div.call(drawSummaryTable, this) + .call(addContextMenu, this); } else { + this.div.style("width", "0%").style("height", "0%"); this.svg.attr("width", options.viewport.width) .attr("height", options.viewport.height) .call(drawXAxis, this) @@ -84,13 +89,17 @@ export class Visual implements powerbi.extensibility.IVisual { this.host.eventService.renderingFinished(options); } catch (caught_error) { - this.svg.call(drawErrors, options, caught_error.message, "internal"); + this.div.style("width", "0%").style("height", "0%"); + this.svg.attr("width", options.viewport.width) + .attr("height", options.viewport.height) + .call(drawErrors, options, caught_error.message, "internal"); console.error(caught_error) this.host.eventService.renderingFailed(options); } } processVisualError(options: VisualUpdateOptions, message: string, type: string = null): void { + this.div.style("width", "0%").style("height", "0%"); this.svg.attr("width", options.viewport.width) .attr("height", options.viewport.height) if (this.viewModel.inputSettings.settings.canvas.show_errors) {