From 6fa2e5faa44f3469cfabc5dd4973de890867e7dc Mon Sep 17 00:00:00 2001 From: Andrew Johnson Date: Wed, 31 Jul 2024 21:43:15 +0300 Subject: [PATCH 01/18] Initial grouped handling for grouped pre-processing and calculation --- src/Classes/viewModelClass.ts | 139 +++++++++++++++++++++++------- src/Functions/extractInputData.ts | 5 +- src/visual.ts | 3 + 3 files changed, 115 insertions(+), 32 deletions(-) diff --git a/src/Classes/viewModelClass.ts b/src/Classes/viewModelClass.ts index 5e38dd5..b9cd9aa 100644 --- a/src/Classes/viewModelClass.ts +++ b/src/Classes/viewModelClass.ts @@ -92,15 +92,19 @@ export type colourPaletteType = { export default class viewModelClass { inputData: dataObject; + inputDataGrouped: dataObject[]; inputSettings: settingsClass; controlLimits: controlLimitsObject; + controlLimitsGrouped: controlLimitsObject[]; outliers: outliersObject; + outliersGrouped: outliersObject[]; plotPoints: plotData[]; groupedLines: [string, lineData[]][]; tickLabels: { x: number; label: string; }[]; plotProperties: plotPropertiesClass; splitIndexes: number[]; groupStartEndIndexes: number[][]; + groupStartEndIndexesGrouped: number[][][]; firstRun: boolean; colourPalette: colourPaletteType; tableColumns: string[]; @@ -127,10 +131,32 @@ export default class viewModelClass { hyperlinkColour: host.colorPalette.hyperlink.value } } + + if (options.dataViews[0].categorical.values?.source?.roles?.indicator) { + this.groupStartEndIndexesGrouped = new Array(); + this.inputDataGrouped = options.dataViews[0].categorical.values.grouped().map(d => { + (d).categories = options.dataViews[0].categorical.categories; + const inpData = extractInputData(d, this.inputSettings); + const allIndexes: number[] = [-1].concat(inpData.groupingIndexes) + .concat([inpData.limitInputArgs.keys.length - 1]) + .filter((d, idx, arr) => arr.indexOf(d) === idx) + .sort((a,b) => a - b); + const groupStartEndIndexes = new Array(); + for (let i: number = 0; i < allIndexes.length - 1; i++) { + groupStartEndIndexes.push([allIndexes[i] + 1, allIndexes[i + 1] + 1]) + } + this.groupStartEndIndexesGrouped.push(groupStartEndIndexes); + return inpData; + }) + } else { + this.inputDataGrouped = null; + } + console.log(this.inputDataGrouped) // Only re-construct data and re-calculate limits if they have changed - if (options.type === 2 || this.firstRun) { - const split_indexes: string = (options.dataViews[0]?.metadata?.objects?.split_indexes_storage?.split_indexes) ?? "[]"; - this.splitIndexes = JSON.parse(split_indexes); + //if (options.type === 2 || this.firstRun) { + const split_indexes_str: string = (options.dataViews[0]?.metadata?.objects?.split_indexes_storage?.split_indexes) ?? "[]"; + const split_indexes: number[] = JSON.parse(split_indexes_str); + this.splitIndexes = split_indexes; this.inputData = extractInputData(options.dataViews[0].categorical, this.inputSettings); if (this.inputData.validationStatus.status === 0) { @@ -148,13 +174,18 @@ export default class viewModelClass { this.calculateLimits(); this.scaleAndTruncateLimits(); - this.flagOutliers(); + this.outliers = this.flagOutliers(this.controlLimits, this.groupStartEndIndexes); + if (this.inputDataGrouped) { + this.outliersGrouped = this.controlLimitsGrouped.map((limits, idx) => { + return this.flagOutliers(limits, this.groupStartEndIndexesGrouped[idx]); + }); + } // Structure the data and calculated limits to the format needed for plotting this.initialisePlotData(host); this.initialiseGroupedLines(); } - } + //} this.plotProperties.update( options, @@ -169,10 +200,44 @@ export default class viewModelClass { } calculateLimits(): void { - this.inputData.limitInputArgs.outliers_in_limits = this.inputSettings.settings.spc.outliers_in_limits; const limitFunction: (args: controlLimitsArgs) => controlLimitsObject = limitFunctions[this.inputSettings.settings.spc.chart_type]; + if (this.inputDataGrouped) { + this.controlLimitsGrouped = this.inputDataGrouped.map((d, idx) => { + let limits: controlLimitsObject; + d.limitInputArgs.outliers_in_limits = this.inputSettings.settings.spc.outliers_in_limits; + + if (this.groupStartEndIndexesGrouped[idx].length > 1) { + const groupedData: dataObject[] = this.groupStartEndIndexesGrouped[idx].map((indexes) => { + // Force a deep copy + const data: dataObject = JSON.parse(JSON.stringify(d)); + data.limitInputArgs.denominators = data.limitInputArgs.denominators.slice(indexes[0], indexes[1]) + data.limitInputArgs.numerators = data.limitInputArgs.numerators.slice(indexes[0], indexes[1]) + data.limitInputArgs.keys = data.limitInputArgs.keys.slice(indexes[0], indexes[1]) + return data; + }) + const calcLimitsGrouped: controlLimitsObject[] = groupedData.map(d => limitFunction(d.limitInputArgs)); + limits = calcLimitsGrouped.reduce((all: controlLimitsObject, curr: controlLimitsObject) => { + const allInner: controlLimitsObject = all; + Object.entries(all).forEach((entry, idx) => { + allInner[entry[0]] = entry[1]?.concat(Object.entries(curr)[idx][1]); + }) + return allInner; + }); + } else { + limits = limitFunction(d.limitInputArgs); + } + limits.alt_targets = d.alt_targets; + limits.speclimits_lower = d.speclimits_lower; + limits.speclimits_upper = d.speclimits_upper; + return limits; + }); + } else { + this.controlLimitsGrouped = null; + } + + this.inputData.limitInputArgs.outliers_in_limits = this.inputSettings.settings.spc.outliers_in_limits; if (this.groupStartEndIndexes.length > 1) { const groupedData: dataObject[] = this.groupStartEndIndexes.map((indexes) => { // Force a deep copy @@ -394,38 +459,49 @@ export default class viewModelClass { } } - lines_to_scale.forEach(limit => { - this.controlLimits[limit] = multiply(this.controlLimits[limit], multiplier) - }) - const limits: truncateInputs = { lower: this.inputSettings.settings.spc.ll_truncate, upper: this.inputSettings.settings.spc.ul_truncate }; + if (this.controlLimitsGrouped) { + this.controlLimitsGrouped.forEach(limitGroup => { + lines_to_scale.forEach(limit => { + limitGroup[limit] = multiply(limitGroup[limit], multiplier) + }) + lines_to_truncate.forEach(limit => { + limitGroup[limit] = truncate(limitGroup[limit], limits) + }) + }) + } + + lines_to_scale.forEach(limit => { + this.controlLimits[limit] = multiply(this.controlLimits[limit], multiplier) + }) + lines_to_truncate.forEach(limit => { this.controlLimits[limit] = truncate(this.controlLimits[limit], limits) }) } - flagOutliers() { + flagOutliers(controlLimits: controlLimitsObject, groupStartEndIndexes: number[][]): outliersObject { const process_flag_type: string = this.inputSettings.settings.outliers.process_flag_type; const improvement_direction: string = this.inputSettings.settings.outliers.improvement_direction; const trend_n: number = this.inputSettings.settings.outliers.trend_n; const shift_n: number = this.inputSettings.settings.outliers.shift_n; const ast_specification: boolean = this.inputSettings.settings.outliers.astronomical_limit === "Specification"; const two_in_three_specification: boolean = this.inputSettings.settings.outliers.two_in_three_limit === "Specification"; - this.outliers = { - astpoint: rep("none", this.inputData.limitInputArgs.keys.length), - two_in_three: rep("none", this.inputData.limitInputArgs.keys.length), - trend: rep("none", this.inputData.limitInputArgs.keys.length), - shift: rep("none", this.inputData.limitInputArgs.keys.length) + let outliers = { + astpoint: rep("none", controlLimits.values.length), + two_in_three: rep("none", controlLimits.values.length), + trend: rep("none", controlLimits.values.length), + shift: rep("none", controlLimits.values.length) } - for (let i: number = 0; i < this.groupStartEndIndexes.length; i++) { - const start: number = this.groupStartEndIndexes[i][0]; - const end: number = this.groupStartEndIndexes[i][1]; - const group_values: number[] = this.controlLimits.values.slice(start, end); - const group_targets: number[] = this.controlLimits.targets.slice(start, end); + for (let i: number = 0; i < groupStartEndIndexes.length; i++) { + const start: number = groupStartEndIndexes[i][0]; + const end: number = groupStartEndIndexes[i][1]; + const group_values: number[] = controlLimits.values.slice(start, end); + const group_targets: number[] = controlLimits.targets.slice(start, end); if (this.inputSettings.derivedSettings.chart_type_props.has_control_limits || ast_specification || two_in_three_specification) { const limit_map: Record = { @@ -438,34 +514,35 @@ export default class viewModelClass { const ast_limit: string = limit_map[this.inputSettings.settings.outliers.astronomical_limit]; const ll_prefix: string = ast_specification ? "speclimits_lower" : "ll"; const ul_prefix: string = ast_specification ? "speclimits_upper" : "ul"; - const lower_limits: number[] = this.controlLimits?.[`${ll_prefix}${ast_limit}`]?.slice(start, end); - const upper_limits: number[] = this.controlLimits?.[`${ul_prefix}${ast_limit}`]?.slice(start, end); + const lower_limits: number[] = controlLimits?.[`${ll_prefix}${ast_limit}`]?.slice(start, end); + const upper_limits: number[] = controlLimits?.[`${ul_prefix}${ast_limit}`]?.slice(start, end); astronomical(group_values, lower_limits, upper_limits) - .forEach((flag, idx) => this.outliers.astpoint[start + idx] = flag) + .forEach((flag, idx) => outliers.astpoint[start + idx] = flag) } if (this.inputSettings.settings.outliers.two_in_three) { const highlight_series: boolean = this.inputSettings.settings.outliers.two_in_three_highlight_series; const two_in_three_limit: string = limit_map[this.inputSettings.settings.outliers.two_in_three_limit]; const ll_prefix: string = two_in_three_specification ? "speclimits_lower" : "ll"; const ul_prefix: string = two_in_three_specification ? "speclimits_upper" : "ul"; - const lower_warn_limits: number[] = this.controlLimits?.[`${ll_prefix}${two_in_three_limit}`]?.slice(start, end); - const upper_warn_limits: number[] = this.controlLimits?.[`${ul_prefix}${two_in_three_limit}`]?.slice(start, end); + const lower_warn_limits: number[] = controlLimits?.[`${ll_prefix}${two_in_three_limit}`]?.slice(start, end); + const upper_warn_limits: number[] = controlLimits?.[`${ul_prefix}${two_in_three_limit}`]?.slice(start, end); twoInThree(group_values, lower_warn_limits, upper_warn_limits, highlight_series) - .forEach((flag, idx) => this.outliers.two_in_three[start + idx] = flag) + .forEach((flag, idx) => outliers.two_in_three[start + idx] = flag) } } if (this.inputSettings.settings.outliers.trend) { trend(group_values, trend_n) - .forEach((flag, idx) => this.outliers.trend[start + idx] = flag) + .forEach((flag, idx) => outliers.trend[start + idx] = flag) } if (this.inputSettings.settings.outliers.shift) { shift(group_values, group_targets, shift_n) - .forEach((flag, idx) => this.outliers.shift[start + idx] = flag) + .forEach((flag, idx) => outliers.shift[start + idx] = flag) } } - Object.keys(this.outliers).forEach(key => { - this.outliers[key] = checkFlagDirection(this.outliers[key], + Object.keys(outliers).forEach(key => { + outliers[key] = checkFlagDirection(outliers[key], { process_flag_type, improvement_direction }); }) + return outliers; } } diff --git a/src/Functions/extractInputData.ts b/src/Functions/extractInputData.ts index 90c2de1..b077b07 100644 --- a/src/Functions/extractInputData.ts +++ b/src/Functions/extractInputData.ts @@ -9,6 +9,7 @@ import type { ValidationT } from "./validateInputData"; export type dataObject = { limitInputArgs: controlLimitsArgs; + spcSettings: defaultSettingsType["spc"]; highlights: PrimitiveValue[]; anyHighlights: boolean; categories: DataViewCategoryColumn; @@ -26,6 +27,7 @@ export type dataObject = { function invalidInputData(inputValidStatus: ValidationT): dataObject { return { limitInputArgs: null, + spcSettings: null, highlights: null, anyHighlights: false, categories: null, @@ -60,7 +62,7 @@ export default function extractInputData(inputView: DataViewCategorical, inputSe const speclimits_upper: number[] = extractConditionalFormatting(inputView, "lines", inputSettings) ?.values .map(d => d.show_specification ? d.specification_upper : null); - + const spcSettings = extractConditionalFormatting(inputView, "spc", inputSettings)?.values?.[0]; const inputValidStatus: ValidationT = validateInputData(keys, numerators, denominators, xbar_sds, groupings, inputSettingsClass.derivedSettings.chart_type_props); @@ -125,6 +127,7 @@ export default function extractInputData(inputView: DataViewCategorical, inputSe xbar_sds: extractValues(xbar_sds, valid_ids), outliers_in_limits: false, }, + spcSettings: spcSettings, tooltips: extractValues(tooltips, valid_ids), highlights: extractValues(highlights, valid_ids), anyHighlights: !isNullOrUndefined(highlights), diff --git a/src/visual.ts b/src/visual.ts index 720e575..8868257 100644 --- a/src/visual.ts +++ b/src/visual.ts @@ -40,6 +40,8 @@ export class Visual implements powerbi.extensibility.IVisual { public update(options: VisualUpdateOptions): void { try { + console.log(options) + console.log(options.dataViews[0].categorical.values.grouped()) this.host.eventService.renderingStarted(options); // Remove printed error if refreshing after a previous error run this.svg.select(".errormessage").remove(); @@ -60,6 +62,7 @@ export class Visual implements powerbi.extensibility.IVisual { } this.viewModel.update(options, this.host); + console.log(this.viewModel) if (this.viewModel.inputData.validationStatus.status !== 0) { this.processVisualError(options, this.viewModel.inputData.validationStatus.error); From f56424a8bbbf2cc06447f9e9cb0d75e226d702c0 Mon Sep 17 00:00:00 2001 From: Andrew Johnson Date: Wed, 31 Jul 2024 21:55:40 +0300 Subject: [PATCH 02/18] flagOutliers to fully functional --- src/Classes/viewModelClass.ts | 35 ++++++++++++++++++----------------- 1 file changed, 18 insertions(+), 17 deletions(-) diff --git a/src/Classes/viewModelClass.ts b/src/Classes/viewModelClass.ts index b9cd9aa..620f170 100644 --- a/src/Classes/viewModelClass.ts +++ b/src/Classes/viewModelClass.ts @@ -134,6 +134,7 @@ export default class viewModelClass { if (options.dataViews[0].categorical.values?.source?.roles?.indicator) { this.groupStartEndIndexesGrouped = new Array(); + this.inputDataGrouped = options.dataViews[0].categorical.values.grouped().map(d => { (d).categories = options.dataViews[0].categorical.categories; const inpData = extractInputData(d, this.inputSettings); @@ -174,10 +175,10 @@ export default class viewModelClass { this.calculateLimits(); this.scaleAndTruncateLimits(); - this.outliers = this.flagOutliers(this.controlLimits, this.groupStartEndIndexes); + this.outliers = this.flagOutliers(this.controlLimits, this.groupStartEndIndexes, this.inputSettings); if (this.inputDataGrouped) { this.outliersGrouped = this.controlLimitsGrouped.map((limits, idx) => { - return this.flagOutliers(limits, this.groupStartEndIndexesGrouped[idx]); + return this.flagOutliers(limits, this.groupStartEndIndexesGrouped[idx], this.inputSettings); }); } @@ -484,13 +485,13 @@ export default class viewModelClass { }) } - flagOutliers(controlLimits: controlLimitsObject, groupStartEndIndexes: number[][]): outliersObject { - const process_flag_type: string = this.inputSettings.settings.outliers.process_flag_type; - const improvement_direction: string = this.inputSettings.settings.outliers.improvement_direction; - const trend_n: number = this.inputSettings.settings.outliers.trend_n; - const shift_n: number = this.inputSettings.settings.outliers.shift_n; - const ast_specification: boolean = this.inputSettings.settings.outliers.astronomical_limit === "Specification"; - const two_in_three_specification: boolean = this.inputSettings.settings.outliers.two_in_three_limit === "Specification"; + flagOutliers(controlLimits: controlLimitsObject, groupStartEndIndexes: number[][], inputSettings: settingsClass): outliersObject { + const process_flag_type: string = inputSettings.settings.outliers.process_flag_type; + const improvement_direction: string = inputSettings.settings.outliers.improvement_direction; + const trend_n: number = inputSettings.settings.outliers.trend_n; + const shift_n: number = inputSettings.settings.outliers.shift_n; + const ast_specification: boolean = inputSettings.settings.outliers.astronomical_limit === "Specification"; + const two_in_three_specification: boolean = inputSettings.settings.outliers.two_in_three_limit === "Specification"; let outliers = { astpoint: rep("none", controlLimits.values.length), two_in_three: rep("none", controlLimits.values.length), @@ -503,15 +504,15 @@ export default class viewModelClass { const group_values: number[] = controlLimits.values.slice(start, end); const group_targets: number[] = controlLimits.targets.slice(start, end); - if (this.inputSettings.derivedSettings.chart_type_props.has_control_limits || ast_specification || two_in_three_specification) { + if (inputSettings.derivedSettings.chart_type_props.has_control_limits || ast_specification || two_in_three_specification) { const limit_map: Record = { "1 Sigma": "68", "2 Sigma": "95", "3 Sigma": "99", "Specification": "", }; - if (this.inputSettings.settings.outliers.astronomical) { - const ast_limit: string = limit_map[this.inputSettings.settings.outliers.astronomical_limit]; + if (inputSettings.settings.outliers.astronomical) { + const ast_limit: string = limit_map[inputSettings.settings.outliers.astronomical_limit]; const ll_prefix: string = ast_specification ? "speclimits_lower" : "ll"; const ul_prefix: string = ast_specification ? "speclimits_upper" : "ul"; const lower_limits: number[] = controlLimits?.[`${ll_prefix}${ast_limit}`]?.slice(start, end); @@ -519,9 +520,9 @@ export default class viewModelClass { astronomical(group_values, lower_limits, upper_limits) .forEach((flag, idx) => outliers.astpoint[start + idx] = flag) } - if (this.inputSettings.settings.outliers.two_in_three) { - const highlight_series: boolean = this.inputSettings.settings.outliers.two_in_three_highlight_series; - const two_in_three_limit: string = limit_map[this.inputSettings.settings.outliers.two_in_three_limit]; + if (inputSettings.settings.outliers.two_in_three) { + const highlight_series: boolean = inputSettings.settings.outliers.two_in_three_highlight_series; + const two_in_three_limit: string = limit_map[inputSettings.settings.outliers.two_in_three_limit]; const ll_prefix: string = two_in_three_specification ? "speclimits_lower" : "ll"; const ul_prefix: string = two_in_three_specification ? "speclimits_upper" : "ul"; const lower_warn_limits: number[] = controlLimits?.[`${ll_prefix}${two_in_three_limit}`]?.slice(start, end); @@ -530,11 +531,11 @@ export default class viewModelClass { .forEach((flag, idx) => outliers.two_in_three[start + idx] = flag) } } - if (this.inputSettings.settings.outliers.trend) { + if (inputSettings.settings.outliers.trend) { trend(group_values, trend_n) .forEach((flag, idx) => outliers.trend[start + idx] = flag) } - if (this.inputSettings.settings.outliers.shift) { + if (inputSettings.settings.outliers.shift) { shift(group_values, group_targets, shift_n) .forEach((flag, idx) => outliers.shift[start + idx] = flag) } From 576021e7411e3b2a261e4a3d3e9d7fa748ca794b Mon Sep 17 00:00:00 2001 From: Andrew Johnson Date: Wed, 31 Jul 2024 22:02:36 +0300 Subject: [PATCH 03/18] calculateLimits to functional grouped --- src/Classes/viewModelClass.ts | 69 +++++++++-------------------------- 1 file changed, 18 insertions(+), 51 deletions(-) diff --git a/src/Classes/viewModelClass.ts b/src/Classes/viewModelClass.ts index 620f170..629f094 100644 --- a/src/Classes/viewModelClass.ts +++ b/src/Classes/viewModelClass.ts @@ -134,10 +134,12 @@ export default class viewModelClass { if (options.dataViews[0].categorical.values?.source?.roles?.indicator) { this.groupStartEndIndexesGrouped = new Array(); + this.controlLimitsGrouped = new Array(); this.inputDataGrouped = options.dataViews[0].categorical.values.grouped().map(d => { (d).categories = options.dataViews[0].categorical.categories; const inpData = extractInputData(d, this.inputSettings); + const allIndexes: number[] = [-1].concat(inpData.groupingIndexes) .concat([inpData.limitInputArgs.keys.length - 1]) .filter((d, idx, arr) => arr.indexOf(d) === idx) @@ -147,6 +149,8 @@ export default class viewModelClass { groupStartEndIndexes.push([allIndexes[i] + 1, allIndexes[i + 1] + 1]) } this.groupStartEndIndexesGrouped.push(groupStartEndIndexes); + this.controlLimitsGrouped.push(this.calculateLimits(inpData, groupStartEndIndexes, this.inputSettings)); + return inpData; }) } else { @@ -173,14 +177,9 @@ export default class viewModelClass { this.groupStartEndIndexes.push([allIndexes[i] + 1, allIndexes[i + 1] + 1]) } - this.calculateLimits(); + this.controlLimits = this.calculateLimits(this.inputData, this.groupStartEndIndexes, this.inputSettings); this.scaleAndTruncateLimits(); this.outliers = this.flagOutliers(this.controlLimits, this.groupStartEndIndexes, this.inputSettings); - if (this.inputDataGrouped) { - this.outliersGrouped = this.controlLimitsGrouped.map((limits, idx) => { - return this.flagOutliers(limits, this.groupStartEndIndexesGrouped[idx], this.inputSettings); - }); - } // Structure the data and calculated limits to the format needed for plotting this.initialisePlotData(host); @@ -200,49 +199,16 @@ export default class viewModelClass { this.firstRun = false; } - calculateLimits(): void { + calculateLimits(inputData: dataObject, groupStartEndIndexes: number[][], inputSettings: settingsClass): controlLimitsObject { const limitFunction: (args: controlLimitsArgs) => controlLimitsObject - = limitFunctions[this.inputSettings.settings.spc.chart_type]; - - if (this.inputDataGrouped) { - this.controlLimitsGrouped = this.inputDataGrouped.map((d, idx) => { - let limits: controlLimitsObject; - d.limitInputArgs.outliers_in_limits = this.inputSettings.settings.spc.outliers_in_limits; - - if (this.groupStartEndIndexesGrouped[idx].length > 1) { - const groupedData: dataObject[] = this.groupStartEndIndexesGrouped[idx].map((indexes) => { - // Force a deep copy - const data: dataObject = JSON.parse(JSON.stringify(d)); - data.limitInputArgs.denominators = data.limitInputArgs.denominators.slice(indexes[0], indexes[1]) - data.limitInputArgs.numerators = data.limitInputArgs.numerators.slice(indexes[0], indexes[1]) - data.limitInputArgs.keys = data.limitInputArgs.keys.slice(indexes[0], indexes[1]) - return data; - }) - const calcLimitsGrouped: controlLimitsObject[] = groupedData.map(d => limitFunction(d.limitInputArgs)); - limits = calcLimitsGrouped.reduce((all: controlLimitsObject, curr: controlLimitsObject) => { - const allInner: controlLimitsObject = all; - Object.entries(all).forEach((entry, idx) => { - allInner[entry[0]] = entry[1]?.concat(Object.entries(curr)[idx][1]); - }) - return allInner; - }); - } else { - limits = limitFunction(d.limitInputArgs); - } - limits.alt_targets = d.alt_targets; - limits.speclimits_lower = d.speclimits_lower; - limits.speclimits_upper = d.speclimits_upper; - return limits; - }); - } else { - this.controlLimitsGrouped = null; - } + = limitFunctions[inputSettings.settings.spc.chart_type]; - this.inputData.limitInputArgs.outliers_in_limits = this.inputSettings.settings.spc.outliers_in_limits; - if (this.groupStartEndIndexes.length > 1) { - const groupedData: dataObject[] = this.groupStartEndIndexes.map((indexes) => { + inputData.limitInputArgs.outliers_in_limits = inputSettings.settings.spc.outliers_in_limits; + let controlLimits: controlLimitsObject; + if (groupStartEndIndexes.length > 1) { + const groupedData: dataObject[] = groupStartEndIndexes.map((indexes) => { // Force a deep copy - const data: dataObject = JSON.parse(JSON.stringify(this.inputData)); + const data: dataObject = JSON.parse(JSON.stringify(inputData)); data.limitInputArgs.denominators = data.limitInputArgs.denominators.slice(indexes[0], indexes[1]) data.limitInputArgs.numerators = data.limitInputArgs.numerators.slice(indexes[0], indexes[1]) data.limitInputArgs.keys = data.limitInputArgs.keys.slice(indexes[0], indexes[1]) @@ -250,7 +216,7 @@ export default class viewModelClass { }) const calcLimitsGrouped: controlLimitsObject[] = groupedData.map(d => limitFunction(d.limitInputArgs)); - this.controlLimits = calcLimitsGrouped.reduce((all: controlLimitsObject, curr: controlLimitsObject) => { + controlLimits = calcLimitsGrouped.reduce((all: controlLimitsObject, curr: controlLimitsObject) => { const allInner: controlLimitsObject = all; Object.entries(all).forEach((entry, idx) => { allInner[entry[0]] = entry[1]?.concat(Object.entries(curr)[idx][1]); @@ -259,12 +225,13 @@ export default class viewModelClass { }) } else { // Calculate control limits using user-specified type - this.controlLimits = limitFunction(this.inputData.limitInputArgs); + controlLimits = limitFunction(inputData.limitInputArgs); } - this.controlLimits.alt_targets = this.inputData.alt_targets; - this.controlLimits.speclimits_lower = this.inputData.speclimits_lower; - this.controlLimits.speclimits_upper = this.inputData.speclimits_upper; + controlLimits.alt_targets = inputData.alt_targets; + controlLimits.speclimits_lower = inputData.speclimits_lower; + controlLimits.speclimits_upper = inputData.speclimits_upper; + return controlLimits; } initialisePlotData(host: IVisualHost): void { From 63f30f01b2c4e2f97520352b2711dd0934392af7 Mon Sep 17 00:00:00 2001 From: Andrew Johnson Date: Wed, 31 Jul 2024 22:07:49 +0300 Subject: [PATCH 04/18] Grouping startEnd indices to functional grouped --- src/Classes/viewModelClass.ts | 38 ++++++++++++++++------------------- 1 file changed, 17 insertions(+), 21 deletions(-) diff --git a/src/Classes/viewModelClass.ts b/src/Classes/viewModelClass.ts index 629f094..93461c2 100644 --- a/src/Classes/viewModelClass.ts +++ b/src/Classes/viewModelClass.ts @@ -139,15 +139,8 @@ export default class viewModelClass { this.inputDataGrouped = options.dataViews[0].categorical.values.grouped().map(d => { (d).categories = options.dataViews[0].categorical.categories; const inpData = extractInputData(d, this.inputSettings); + const groupStartEndIndexes: number[][] = this.getGroupingIndexes(inpData, this.splitIndexes); - const allIndexes: number[] = [-1].concat(inpData.groupingIndexes) - .concat([inpData.limitInputArgs.keys.length - 1]) - .filter((d, idx, arr) => arr.indexOf(d) === idx) - .sort((a,b) => a - b); - const groupStartEndIndexes = new Array(); - for (let i: number = 0; i < allIndexes.length - 1; i++) { - groupStartEndIndexes.push([allIndexes[i] + 1, allIndexes[i + 1] + 1]) - } this.groupStartEndIndexesGrouped.push(groupStartEndIndexes); this.controlLimitsGrouped.push(this.calculateLimits(inpData, groupStartEndIndexes, this.inputSettings)); @@ -156,7 +149,6 @@ export default class viewModelClass { } else { this.inputDataGrouped = null; } - console.log(this.inputDataGrouped) // Only re-construct data and re-calculate limits if they have changed //if (options.type === 2 || this.firstRun) { const split_indexes_str: string = (options.dataViews[0]?.metadata?.objects?.split_indexes_storage?.split_indexes) ?? "[]"; @@ -165,18 +157,7 @@ export default class viewModelClass { this.inputData = extractInputData(options.dataViews[0].categorical, this.inputSettings); if (this.inputData.validationStatus.status === 0) { - const allIndexes: number[] = this.splitIndexes - .concat([-1]) - .concat(this.inputData.groupingIndexes) - .concat([this.inputData.limitInputArgs.keys.length - 1]) - .filter((d, idx, arr) => arr.indexOf(d) === idx) - .sort((a,b) => a - b); - - this.groupStartEndIndexes = new Array(); - for (let i: number = 0; i < allIndexes.length - 1; i++) { - this.groupStartEndIndexes.push([allIndexes[i] + 1, allIndexes[i + 1] + 1]) - } - + this.groupStartEndIndexes = this.getGroupingIndexes(this.inputData, this.splitIndexes); this.controlLimits = this.calculateLimits(this.inputData, this.groupStartEndIndexes, this.inputSettings); this.scaleAndTruncateLimits(); this.outliers = this.flagOutliers(this.controlLimits, this.groupStartEndIndexes, this.inputSettings); @@ -199,6 +180,21 @@ export default class viewModelClass { this.firstRun = false; } + getGroupingIndexes(inputData: dataObject, splitIndexes?: number[]): number[][] { + const allIndexes: number[] = (splitIndexes ?? []) + .concat([-1]) + .concat(inputData.groupingIndexes) + .concat([inputData.limitInputArgs.keys.length - 1]) + .filter((d, idx, arr) => arr.indexOf(d) === idx) + .sort((a,b) => a - b); + + const groupStartEndIndexes = new Array(); + for (let i: number = 0; i < allIndexes.length - 1; i++) { + groupStartEndIndexes.push([allIndexes[i] + 1, allIndexes[i + 1] + 1]) + } + return groupStartEndIndexes; + } + calculateLimits(inputData: dataObject, groupStartEndIndexes: number[][], inputSettings: settingsClass): controlLimitsObject { const limitFunction: (args: controlLimitsArgs) => controlLimitsObject = limitFunctions[inputSettings.settings.spc.chart_type]; From 1bce1834364add94ccb40a1166416e6283f28d3c Mon Sep 17 00:00:00 2001 From: Andrew Johnson Date: Wed, 31 Jul 2024 22:12:58 +0300 Subject: [PATCH 05/18] scaleTruncate to (semi-) functional grouped --- src/Classes/viewModelClass.ts | 43 ++++++++++++++--------------------- 1 file changed, 17 insertions(+), 26 deletions(-) diff --git a/src/Classes/viewModelClass.ts b/src/Classes/viewModelClass.ts index 93461c2..93ba28e 100644 --- a/src/Classes/viewModelClass.ts +++ b/src/Classes/viewModelClass.ts @@ -138,11 +138,13 @@ export default class viewModelClass { this.inputDataGrouped = options.dataViews[0].categorical.values.grouped().map(d => { (d).categories = options.dataViews[0].categorical.categories; - const inpData = extractInputData(d, this.inputSettings); - const groupStartEndIndexes: number[][] = this.getGroupingIndexes(inpData, this.splitIndexes); + const inpData: dataObject = extractInputData(d, this.inputSettings); + const groupStartEndIndexes: number[][] = this.getGroupingIndexes(inpData); + const limits: controlLimitsObject = this.calculateLimits(inpData, groupStartEndIndexes, this.inputSettings); + this.scaleAndTruncateLimits(limits, this.inputSettings); this.groupStartEndIndexesGrouped.push(groupStartEndIndexes); - this.controlLimitsGrouped.push(this.calculateLimits(inpData, groupStartEndIndexes, this.inputSettings)); + this.controlLimitsGrouped.push(limits); return inpData; }) @@ -159,7 +161,7 @@ export default class viewModelClass { if (this.inputData.validationStatus.status === 0) { this.groupStartEndIndexes = this.getGroupingIndexes(this.inputData, this.splitIndexes); this.controlLimits = this.calculateLimits(this.inputData, this.groupStartEndIndexes, this.inputSettings); - this.scaleAndTruncateLimits(); + this.scaleAndTruncateLimits(this.controlLimits, this.inputSettings); this.outliers = this.flagOutliers(this.controlLimits, this.groupStartEndIndexes, this.inputSettings); // Structure the data and calculated limits to the format needed for plotting @@ -400,51 +402,40 @@ export default class viewModelClass { this.groupedLines = d3.groups(formattedLines, d => d.group); } - scaleAndTruncateLimits(): void { + scaleAndTruncateLimits(controlLimits: controlLimitsObject, inputSettings: settingsClass): void { // Scale limits using provided multiplier - const multiplier: number = this.inputSettings.derivedSettings.multiplier; + const multiplier: number = inputSettings.derivedSettings.multiplier; let lines_to_scale: string[] = ["values", "targets"]; - if (this.inputSettings.derivedSettings.chart_type_props.has_control_limits) { + if (inputSettings.derivedSettings.chart_type_props.has_control_limits) { lines_to_scale = lines_to_scale.concat(["ll99", "ll95", "ll68", "ul68", "ul95", "ul99"]); } let lines_to_truncate: string[] = lines_to_scale; - if (this.inputSettings.settings.lines.show_alt_target) { + if (inputSettings.settings.lines.show_alt_target) { lines_to_truncate = lines_to_truncate.concat(["alt_targets"]); - if (this.inputSettings.settings.lines.multiplier_alt_target) { + if (inputSettings.settings.lines.multiplier_alt_target) { lines_to_scale = lines_to_scale.concat(["alt_targets"]); } } - if (this.inputSettings.settings.lines.show_specification) { + if (inputSettings.settings.lines.show_specification) { lines_to_truncate = lines_to_truncate.concat(["speclimits_lower", "speclimits_upper"]); - if (this.inputSettings.settings.lines.multiplier_specification) { + if (inputSettings.settings.lines.multiplier_specification) { lines_to_scale = lines_to_scale.concat(["speclimits_lower", "speclimits_upper"]); } } const limits: truncateInputs = { - lower: this.inputSettings.settings.spc.ll_truncate, - upper: this.inputSettings.settings.spc.ul_truncate + lower: inputSettings.settings.spc.ll_truncate, + upper: inputSettings.settings.spc.ul_truncate }; - if (this.controlLimitsGrouped) { - this.controlLimitsGrouped.forEach(limitGroup => { - lines_to_scale.forEach(limit => { - limitGroup[limit] = multiply(limitGroup[limit], multiplier) - }) - lines_to_truncate.forEach(limit => { - limitGroup[limit] = truncate(limitGroup[limit], limits) - }) - }) - } - lines_to_scale.forEach(limit => { - this.controlLimits[limit] = multiply(this.controlLimits[limit], multiplier) + controlLimits[limit] = multiply(controlLimits[limit], multiplier) }) lines_to_truncate.forEach(limit => { - this.controlLimits[limit] = truncate(this.controlLimits[limit], limits) + controlLimits[limit] = truncate(controlLimits[limit], limits) }) } From 30152f36b56896a24d0ca9b039ee54c27a45a500 Mon Sep 17 00:00:00 2001 From: Andrew Johnson Date: Wed, 31 Jul 2024 22:54:15 +0300 Subject: [PATCH 06/18] Cleanup grouped handling, record names --- src/Classes/viewModelClass.ts | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/src/Classes/viewModelClass.ts b/src/Classes/viewModelClass.ts index 93ba28e..510e135 100644 --- a/src/Classes/viewModelClass.ts +++ b/src/Classes/viewModelClass.ts @@ -92,23 +92,26 @@ export type colourPaletteType = { export default class viewModelClass { inputData: dataObject; - inputDataGrouped: dataObject[]; inputSettings: settingsClass; controlLimits: controlLimitsObject; - controlLimitsGrouped: controlLimitsObject[]; outliers: outliersObject; - outliersGrouped: outliersObject[]; plotPoints: plotData[]; groupedLines: [string, lineData[]][]; tickLabels: { x: number; label: string; }[]; plotProperties: plotPropertiesClass; splitIndexes: number[]; groupStartEndIndexes: number[][]; - groupStartEndIndexesGrouped: number[][][]; firstRun: boolean; colourPalette: colourPaletteType; tableColumns: string[]; + groupNames: string[]; + inputDataGrouped: dataObject[]; + controlLimitsGrouped: controlLimitsObject[]; + outliersGrouped: outliersObject[]; + groupStartEndIndexesGrouped: number[][][]; + tableColumnsGrouped: string[][]; + constructor() { this.inputData = null; this.inputSettings = new settingsClass(); @@ -133,23 +136,28 @@ export default class viewModelClass { } if (options.dataViews[0].categorical.values?.source?.roles?.indicator) { + this.groupNames = new Array(); + this.inputDataGrouped = new Array(); this.groupStartEndIndexesGrouped = new Array(); this.controlLimitsGrouped = new Array(); - this.inputDataGrouped = options.dataViews[0].categorical.values.grouped().map(d => { + options.dataViews[0].categorical.values.grouped().forEach(d => { (d).categories = options.dataViews[0].categorical.categories; const inpData: dataObject = extractInputData(d, this.inputSettings); const groupStartEndIndexes: number[][] = this.getGroupingIndexes(inpData); const limits: controlLimitsObject = this.calculateLimits(inpData, groupStartEndIndexes, this.inputSettings); - this.scaleAndTruncateLimits(limits, this.inputSettings); + + this.groupNames.push(d.name); + this.inputDataGrouped.push(inpData); this.groupStartEndIndexesGrouped.push(groupStartEndIndexes); this.controlLimitsGrouped.push(limits); - - return inpData; }) } else { + this.groupNames = null; this.inputDataGrouped = null; + this.groupStartEndIndexesGrouped = null; + this.controlLimitsGrouped = null; } // Only re-construct data and re-calculate limits if they have changed //if (options.type === 2 || this.firstRun) { From 5764bee7545646935711a829622a099626959024 Mon Sep 17 00:00:00 2001 From: Andrew Johnson Date: Thu, 1 Aug 2024 11:51:06 +0300 Subject: [PATCH 07/18] Initial grouped table rendering --- src/Classes/viewModelClass.ts | 89 +++++++++++++++---- src/D3 Plotting Functions/drawSummaryTable.ts | 18 ++-- src/visual.ts | 3 +- 3 files changed, 87 insertions(+), 23 deletions(-) diff --git a/src/Classes/viewModelClass.ts b/src/Classes/viewModelClass.ts index 510e135..6c45e67 100644 --- a/src/Classes/viewModelClass.ts +++ b/src/Classes/viewModelClass.ts @@ -36,6 +36,16 @@ export type summaryTableRowData = { two_in_three: string; } +export type summaryTableRowDataGrouped = { + indicator: string; + latest_date: string; + value: number; + target: number; + alt_target: number; + upl: number; + lpl: number; +} + export type plotData = { x: number; value: number; @@ -49,6 +59,10 @@ export type plotData = { tooltip: VisualTooltipDataItem[]; } +export type plotDataGrouped = { + table_row: summaryTableRowDataGrouped; +} + export type controlLimitsObject = { keys: { x: number, id: number, label: string }[]; values: number[]; @@ -103,14 +117,16 @@ export default class viewModelClass { groupStartEndIndexes: number[][]; firstRun: boolean; colourPalette: colourPaletteType; - tableColumns: string[]; + tableColumns: { name: string; label: string; }[]; + showGrouped: boolean; groupNames: string[]; inputDataGrouped: dataObject[]; controlLimitsGrouped: controlLimitsObject[]; outliersGrouped: outliersObject[]; groupStartEndIndexesGrouped: number[][][]; - tableColumnsGrouped: string[][]; + tableColumnsGrouped: { name: string; label: string; }[]; + plotPointsGrouped: plotDataGrouped[]; constructor() { this.inputData = null; @@ -136,6 +152,7 @@ export default class viewModelClass { } if (options.dataViews[0].categorical.values?.source?.roles?.indicator) { + this.showGrouped = true; this.groupNames = new Array(); this.inputDataGrouped = new Array(); this.groupStartEndIndexesGrouped = new Array(); @@ -153,7 +170,9 @@ export default class viewModelClass { this.groupStartEndIndexesGrouped.push(groupStartEndIndexes); this.controlLimitsGrouped.push(limits); }) + this.initialisePlotDataGrouped(); } else { + this.showGrouped = false; this.groupNames = null; this.inputDataGrouped = null; this.groupStartEndIndexesGrouped = null; @@ -240,48 +259,86 @@ export default class viewModelClass { return controlLimits; } + initialisePlotDataGrouped(/*host: IVisualHost*/): void { + this.plotPointsGrouped = new Array(); + this.tableColumnsGrouped = [ + { name: "indicator", label: "Indicator" }, + { name: "latest_date", label: "Latest Date" }, + { name: "value", label: "Value" }, + { name: "target", label: "Target" }, + { name: "alt_target", label: "Alt. Target" }, + { name: "upl", label: "UPL" }, + { name: "lpl", label: "LPL" } + ]; + + for (let i: number = 0; i < this.groupNames.length; i++) { + //const inputData: dataObject = this.inputDataGrouped[i]; + const limits: controlLimitsObject = this.controlLimitsGrouped[i]; + //const outliers: outliersObject = this.outliersGrouped[i]; + const lastIndex: number = limits.keys.length - 1; + + const table_row: summaryTableRowDataGrouped = { + indicator: this.groupNames[i], + latest_date: limits.keys[lastIndex].label, + value: limits.values[lastIndex], + target: limits.targets[lastIndex], + alt_target: limits.alt_targets[lastIndex], + upl: limits.ul99[lastIndex], + lpl: limits.ll99[lastIndex] + } + + this.plotPointsGrouped.push({ + table_row: table_row + }) + } + } + initialisePlotData(host: IVisualHost): void { this.plotPoints = new Array(); this.tickLabels = new Array<{ x: number; label: string; }>(); - this.tableColumns = new Array(); - this.tableColumns.push("date"); - this.tableColumns.push("value"); + this.tableColumns = new Array<{ name: string; label: string; }>(); + this.tableColumns.push({ name: "date", label: "Date" }); + this.tableColumns.push({ name: "value", label: "Value" }); if (!isNullOrUndefined(this.controlLimits.numerators)) { - this.tableColumns.push("numerator"); + this.tableColumns.push({ name: "numerator", label: "Numerator" }); } if (!isNullOrUndefined(this.controlLimits.denominators)) { - this.tableColumns.push("denominator"); + this.tableColumns.push({ name: "denominator", label: "Denominator" }); } if (this.inputSettings.settings.lines.show_target) { - this.tableColumns.push("target"); + this.tableColumns.push({ name: "target", label: "Target" }); } if (this.inputSettings.settings.lines.show_alt_target) { - this.tableColumns.push("alt_target"); + this.tableColumns.push({ name: "alt_target", label: "Alt. Target" }); } if (this.inputSettings.settings.lines.show_specification) { - this.tableColumns.push("speclimits_lower", "speclimits_upper"); + this.tableColumns.push({ name: "speclimits_lower", label: "Spec. Lower" }, + { name: "speclimits_upper", label: "Spec. Upper" }); } if (this.inputSettings.derivedSettings.chart_type_props.has_control_limits) { if (this.inputSettings.settings.lines.show_99) { - this.tableColumns.push("ll99", "ul99"); + this.tableColumns.push({ name: "ll99", label: "LL 99%" }, + { name: "ul99", label: "UL 99%" }); } if (this.inputSettings.settings.lines.show_95) { - this.tableColumns.push("ll95", "ul95"); + this.tableColumns.push({ name: "ll95", label: "LL 95%" }, + { name: "ul95", label: "UL 95%" }); } if (this.inputSettings.settings.lines.show_68) { - this.tableColumns.push("ll68", "ul68"); + this.tableColumns.push({ name: "ll68", label: "LL 68%" }, + { name: "ul68", label: "UL 68%" }); } } if (this.inputSettings.settings.outliers.astronomical) { - this.tableColumns.push("astpoint"); + this.tableColumns.push({ name: "astpoint", label: "Ast. Point" }); } if (this.inputSettings.settings.outliers.trend) { - this.tableColumns.push("trend"); + this.tableColumns.push({ name: "trend", label: "Trend" }); } if (this.inputSettings.settings.outliers.shift) { - this.tableColumns.push("shift"); + this.tableColumns.push({ name: "shift", label: "Shift" }); } for (let i: number = 0; i < this.controlLimits.keys.length; i++) { diff --git a/src/D3 Plotting Functions/drawSummaryTable.ts b/src/D3 Plotting Functions/drawSummaryTable.ts index 991dc4c..fb4449a 100644 --- a/src/D3 Plotting Functions/drawSummaryTable.ts +++ b/src/D3 Plotting Functions/drawSummaryTable.ts @@ -1,4 +1,4 @@ -import { plotData } from "../Classes/viewModelClass"; +import { plotData, type plotDataGrouped } from "../Classes/viewModelClass"; import type { divBaseType, Visual } from "../visual"; import * as d3 from "./D3 Modules"; @@ -16,15 +16,19 @@ export default function drawSummaryTable(selection: divBaseType, visualObj: Visu table.append('tbody').classed("table-body", true); } - const plotPoints: plotData[] = visualObj.viewModel.plotPoints; + let plotPoints: plotData[] | plotDataGrouped[]; + let cols: { name: string; label: string; }[]; - const cols: string[] = visualObj.viewModel.tableColumns; + if (visualObj.viewModel.showGrouped){ + plotPoints = visualObj.viewModel.plotPointsGrouped; + cols = visualObj.viewModel.tableColumnsGrouped; + } selection.select(".table-header") .selectAll("th") .data(cols) .join("th") - .text((d) => d) + .text((d) => d.label) .style("border", "1px black solid") .style("padding", "5px") .style("background-color", "lightgray") @@ -33,8 +37,9 @@ export default function drawSummaryTable(selection: divBaseType, visualObj: Visu selection.select(".table-body") .selectAll('tr') - .data(plotPoints) + .data(plotPoints) .join('tr') + /* .on("click", (event, d: plotData) => { if (visualObj.host.hostCapabilities.allowInteractions) { visualObj.selectionManager @@ -45,8 +50,9 @@ export default function drawSummaryTable(selection: divBaseType, visualObj: Visu event.stopPropagation(); } }) + */ .selectAll('td') - .data((d) => cols.map(col => d.table_row[col])) + .data((d) => cols.map(col => d.table_row[col.name])) .join('td') .text((d) => { return typeof d === "number" ? d.toFixed(visualObj.viewModel.inputSettings.settings.spc.sig_figs) : d; diff --git a/src/visual.ts b/src/visual.ts index 8868257..86325ab 100644 --- a/src/visual.ts +++ b/src/visual.ts @@ -69,7 +69,8 @@ export class Visual implements powerbi.extensibility.IVisual { return; } - if (this.viewModel.inputSettings.settings.summary_table.show_table) { + if (this.viewModel.inputSettings.settings.summary_table.show_table || + this.viewModel.showGrouped) { this.svg.attr("width", 0).attr("height", 0); this.tableDiv.call(drawSummaryTable, this) .call(addContextMenu, this); From bcc33d7bf425d311b2d46b44eb58a7b1aa86e128 Mon Sep 17 00:00:00 2001 From: Andrew Johnson Date: Thu, 1 Aug 2024 22:37:34 +0300 Subject: [PATCH 08/18] Initial icon rendering --- src/D3 Plotting Functions/drawIcons.ts | 9 ++-- src/D3 Plotting Functions/drawSummaryTable.ts | 49 +++++++++++++++++-- .../initialiseIconSVG.ts | 31 +++++++----- src/Outlier Flagging/assuranceIconToDraw.ts | 16 +++--- src/Outlier Flagging/variationIconsToDraw.ts | 15 +++--- src/visual.ts | 2 + 6 files changed, 85 insertions(+), 37 deletions(-) diff --git a/src/D3 Plotting Functions/drawIcons.ts b/src/D3 Plotting Functions/drawIcons.ts index 6c0a189..23bc8d0 100644 --- a/src/D3 Plotting Functions/drawIcons.ts +++ b/src/D3 Plotting Functions/drawIcons.ts @@ -1,5 +1,6 @@ import * as nhsIcons from "./NHS Icons" import initialiseIconSVG from "./initialiseIconSVG"; +import { iconTransformSpec } from "./initialiseIconSVG"; import { assuranceIconToDraw, variationIconsToDraw } from "../Functions"; import type { svgBaseType, Visual } from "../visual"; import type { defaultSettingsType } from "../Classes"; @@ -18,10 +19,10 @@ export default function drawIcons(selection: svgBaseType, visualObj: Visual): vo if (draw_variation) { const variation_scaling: number = nhsIconSettings.variation_icons_scaling; - const variationIconsPresent: string[] = variationIconsToDraw(visualObj.viewModel); + const variationIconsPresent: string[] = variationIconsToDraw(visualObj.viewModel.outliers, visualObj.viewModel.inputSettings); variationIconsPresent.forEach((icon: string, idx: number) => { selection - .call(initialiseIconSVG, icon, svg_width, svg_height, variation_location, variation_scaling, idx) + .call(initialiseIconSVG, icon, iconTransformSpec(svg_width, svg_height, variation_location, variation_scaling, idx)) .selectAll(`.${icon}`) .call(nhsIcons[icon as keyof typeof nhsIcons]) }) @@ -32,7 +33,7 @@ export default function drawIcons(selection: svgBaseType, visualObj: Visual): vo if (draw_assurance) { const assurance_location: string = nhsIconSettings.assurance_icons_locations; const assurance_scaling: number = nhsIconSettings.assurance_icons_scaling; - const assuranceIconPresent: string = assuranceIconToDraw(visualObj.viewModel); + const assuranceIconPresent: string = assuranceIconToDraw(visualObj.viewModel.controlLimits, visualObj.viewModel.inputSettings); if (assuranceIconPresent === "none") { return; } @@ -41,7 +42,7 @@ export default function drawIcons(selection: svgBaseType, visualObj: Visual): vo ? numVariationIcons : 0; selection - .call(initialiseIconSVG, assuranceIconPresent, svg_width, svg_height, assurance_location, assurance_scaling, currIconCount) + .call(initialiseIconSVG, assuranceIconPresent, iconTransformSpec(svg_width, svg_height, assurance_location, assurance_scaling, currIconCount)) .selectAll(`.${assuranceIconPresent}`) .call(nhsIcons[assuranceIconPresent as keyof typeof nhsIcons]) } diff --git a/src/D3 Plotting Functions/drawSummaryTable.ts b/src/D3 Plotting Functions/drawSummaryTable.ts index fb4449a..d46afa7 100644 --- a/src/D3 Plotting Functions/drawSummaryTable.ts +++ b/src/D3 Plotting Functions/drawSummaryTable.ts @@ -1,9 +1,14 @@ import { plotData, type plotDataGrouped } from "../Classes/viewModelClass"; import type { divBaseType, Visual } from "../visual"; +import initialiseIconSVG from "./initialiseIconSVG"; +import * as nhsIcons from "./NHS Icons" import * as d3 from "./D3 Modules"; export default function drawSummaryTable(selection: divBaseType, visualObj: Visual) { selection.style("height", "100%").style("width", "100%"); + selection.selectAll(".iconrow").remove(); + selection.selectAll(".rowsvg").remove(); + if (selection.select(".table-group").empty()) { const table = selection.append("table") .classed("table-group", true) @@ -21,7 +26,9 @@ export default function drawSummaryTable(selection: divBaseType, visualObj: Visu if (visualObj.viewModel.showGrouped){ plotPoints = visualObj.viewModel.plotPointsGrouped; - cols = visualObj.viewModel.tableColumnsGrouped; + cols = visualObj.viewModel.tableColumnsGrouped + .concat({name: "variation", label: "Variation"}) + .concat({name: "assurance", label: "Assurance"}); } selection.select(".table-header") @@ -35,7 +42,7 @@ export default function drawSummaryTable(selection: divBaseType, visualObj: Visu .style("font-weight", "bold") .style("text-transform", "uppercase"); - selection.select(".table-body") + const tableSelect = selection.select(".table-body") .selectAll('tr') .data(plotPoints) .join('tr') @@ -54,9 +61,6 @@ export default function drawSummaryTable(selection: divBaseType, visualObj: Visu .selectAll('td') .data((d) => cols.map(col => d.table_row[col.name])) .join('td') - .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"); }) @@ -67,6 +71,41 @@ export default function drawSummaryTable(selection: divBaseType, visualObj: Visu .style("padding", "5px") .style("font-size", "12px") + tableSelect.filter((_, i) => i < (cols.length - 2)) + .text((d) => { + return typeof d === "number" ? d.toFixed(visualObj.viewModel.inputSettings.settings.spc.sig_figs) : d; + }) + + //const nhsIconSettings = visualObj.viewModel.inputSettings.settings.nhs_icons; + //const variation_location: string = nhsIconSettings.variation_icons_locations; + + const varSelection = tableSelect.filter((_, i) => i === (cols.length - 1)) + const selHeight = (varSelection.node() as SVGGElement).getBoundingClientRect() + const svg_width: number = selHeight.width + const svg_height: number = selHeight.height + console.log(visualObj.viewModel.plotProperties.height) + console.log(visualObj.viewModel.plotProperties.width) + console.log(selHeight) + + const cellSVG = varSelection.append("svg") + .attr("width", svg_width * 0.8) + .attr("height", svg_height * 0.8) + .classed("rowsvg", true) + + const scaling = visualObj.viewModel.inputSettings.settings.nhs_icons.variation_icons_scaling + //const scaling_factor: number = (0.08 * (svg_height / 378)) * scaling + cellSVG.call(initialiseIconSVG, "commonCause") + .selectAll(".icongroup") + .selectAll(".commonCause") + .attr("transform", `scale(${0.1 * scaling}) translate(0, ${ ((svg_height * 0.8) / 2) + 179 })`) + .call(nhsIcons.commonCause) + + console.log(selection) + console.log(varSelection) + //console.log(varSelection.enter()) + + + selection.on('click', () => { visualObj.selectionManager.clear(); visualObj.updateHighlighting(); diff --git a/src/D3 Plotting Functions/initialiseIconSVG.ts b/src/D3 Plotting Functions/initialiseIconSVG.ts index 7a5c221..86c86f1 100644 --- a/src/D3 Plotting Functions/initialiseIconSVG.ts +++ b/src/D3 Plotting Functions/initialiseIconSVG.ts @@ -1,5 +1,18 @@ import type { svgBaseType } from "../visual"; +export function iconTransformSpec(svg_width: number, svg_height: number, location: string, scaling: number, count: number): string { + const scaling_factor: number = (0.08 * (svg_height / 378)) * scaling + const icon_x: number = location.includes("Right") + ? (svg_width / scaling_factor) - (378 + (count * 378)) + : location.includes("Centre") ? (svg_width / scaling_factor) / 2 - 189 + : (count * 378); + const icon_y: number = location.includes("Bottom") + ? (svg_height / scaling_factor) - 378 + : location.includes("Centre") ? (svg_height / scaling_factor) / 2 - 189 + : 0; + return `scale(${scaling_factor}) translate(${icon_x}, ${icon_y})`; +} + /** * This method initialises a plotting space for rendering a given NHS SVG icon. * The method uses the current number of the icon (i.e., whether it's the first, @@ -9,19 +22,13 @@ import type { svgBaseType } from "../visual"; * icon rendering function from the "Icons" folder. * */ -export default function initialiseIconSVG(selection: svgBaseType, icon_name: string, svg_width: number, svg_height: number, location: string, scaling: number, count: number): void { - const scaling_factor: number = (0.08 * (svg_height / 378)) * scaling - const scale: string = `scale(${scaling_factor})` - const icon_x: number = location.includes("Right") - ? (svg_width / scaling_factor) - (378 + (count * 378)) - : (count * 378); - const icon_y: number = location.includes("Bottom") - ? (svg_height / scaling_factor) - 378 - : 0; - +export default function initialiseIconSVG(selection: svgBaseType, icon_name: string, transform_spec?: string): void { const icon_group = selection.append('g') - .classed("icongroup", true) - .attr("transform", `${scale} translate(${icon_x}, ${icon_y})`) + .classed("icongroup", true) + + if (transform_spec) { + icon_group.attr("transform", transform_spec) + } const icon_defs = icon_group.append("defs") const icon_defs_filter = icon_defs.append("filter") diff --git a/src/Outlier Flagging/assuranceIconToDraw.ts b/src/Outlier Flagging/assuranceIconToDraw.ts index 91f22c9..42bc46c 100644 --- a/src/Outlier Flagging/assuranceIconToDraw.ts +++ b/src/Outlier Flagging/assuranceIconToDraw.ts @@ -1,13 +1,13 @@ -import type { viewModelClass } from "../Classes"; +import type { controlLimitsObject, settingsClass } from "../Classes"; import { isNullOrUndefined } from "../Functions"; -export default function assuranceIconToDraw(viewModel: viewModelClass): string { - if (!(viewModel.inputSettings.derivedSettings.chart_type_props.has_control_limits)) { +export default function assuranceIconToDraw(controlLimits: controlLimitsObject, inputSettings: settingsClass): string { + if (!(inputSettings.derivedSettings.chart_type_props.has_control_limits)) { return "none"; } - const imp_direction: string = viewModel.inputSettings.settings.outliers.improvement_direction; - const N: number = viewModel.controlLimits.ll99.length - 1; - const alt_target: number = viewModel.controlLimits?.alt_targets?.[N]; + const imp_direction: string = inputSettings.settings.outliers.improvement_direction; + const N: number = controlLimits.ll99.length - 1; + const alt_target: number = controlLimits?.alt_targets?.[N]; if (isNullOrUndefined(alt_target) || imp_direction === "neutral") { return "none"; @@ -15,9 +15,9 @@ export default function assuranceIconToDraw(viewModel: viewModelClass): string { const impDirectionIncrease: boolean = imp_direction === "increase"; - if (alt_target > viewModel.controlLimits.ul99[N]) { + if (alt_target > controlLimits.ul99[N]) { return impDirectionIncrease ? "consistentFail" : "consistentPass"; - } else if (alt_target < viewModel.controlLimits.ll99[N]) { + } else if (alt_target < controlLimits.ll99[N]) { return impDirectionIncrease ? "consistentPass" : "consistentFail"; } else { return "inconsistent"; diff --git a/src/Outlier Flagging/variationIconsToDraw.ts b/src/Outlier Flagging/variationIconsToDraw.ts index 48e7eff..8f3ce94 100644 --- a/src/Outlier Flagging/variationIconsToDraw.ts +++ b/src/Outlier Flagging/variationIconsToDraw.ts @@ -1,8 +1,7 @@ -import type { viewModelClass, outliersObject } from "../Classes"; +import type { outliersObject, settingsClass } from "../Classes"; -export default function variationIconsToDraw(viewModel: viewModelClass): string[] { - const currLimits: outliersObject = viewModel.outliers; - const imp_direction: string = viewModel.inputSettings.settings.outliers.improvement_direction; +export default function variationIconsToDraw(outliers: outliersObject, inputSettings: settingsClass): string[] { + const imp_direction: string = inputSettings.settings.outliers.improvement_direction; const suffix_map: Record = { "increase" : "High", "decrease" : "Low", @@ -14,13 +13,13 @@ export default function variationIconsToDraw(viewModel: viewModelClass): string[ "" : "" } const suffix: string = suffix_map[imp_direction]; - const flag_last: boolean = viewModel.inputSettings.settings.nhs_icons.flag_last_point; + const flag_last: boolean = inputSettings.settings.nhs_icons.flag_last_point; let allFlags: string[]; if (flag_last) { - const N: number = currLimits.astpoint.length - 1; - allFlags = [currLimits.astpoint[N], currLimits.shift[N], currLimits.trend[N], currLimits.two_in_three[N]]; + const N: number = outliers.astpoint.length - 1; + allFlags = [outliers.astpoint[N], outliers.shift[N], outliers.trend[N], outliers.two_in_three[N]]; } else { - allFlags = currLimits.astpoint.concat(currLimits.shift, currLimits.trend, currLimits.two_in_three); + allFlags = outliers.astpoint.concat(outliers.shift, outliers.trend, outliers.two_in_three); } const iconsPresent: string[] = new Array(); diff --git a/src/visual.ts b/src/visual.ts index 86325ab..56b9955 100644 --- a/src/visual.ts +++ b/src/visual.ts @@ -74,6 +74,7 @@ export class Visual implements powerbi.extensibility.IVisual { this.svg.attr("width", 0).attr("height", 0); this.tableDiv.call(drawSummaryTable, this) .call(addContextMenu, this); + console.log(this) } else { this.tableDiv.style("width", "0%").style("height", "0%"); this.svg.attr("width", options.viewport.width) @@ -86,6 +87,7 @@ export class Visual implements powerbi.extensibility.IVisual { .call(drawIcons, this) .call(addContextMenu, this) .call(drawDownloadButton, this) + } this.updateHighlighting(); From 3c0d7de44431dc26ab45fa697acc9d6fee88f6dc Mon Sep 17 00:00:00 2001 From: Andrew Johnson Date: Fri, 2 Aug 2024 18:46:43 +0300 Subject: [PATCH 09/18] Fix icon rendering in cells, sizing --- src/Classes/viewModelClass.ts | 6 ++- src/D3 Plotting Functions/drawSummaryTable.ts | 44 +++++++------------ 2 files changed, 22 insertions(+), 28 deletions(-) diff --git a/src/Classes/viewModelClass.ts b/src/Classes/viewModelClass.ts index 6c45e67..471ee21 100644 --- a/src/Classes/viewModelClass.ts +++ b/src/Classes/viewModelClass.ts @@ -44,6 +44,8 @@ export type summaryTableRowDataGrouped = { alt_target: number; upl: number; lpl: number; + variation: string; + assurance: string; } export type plotData = { @@ -284,7 +286,9 @@ export default class viewModelClass { target: limits.targets[lastIndex], alt_target: limits.alt_targets[lastIndex], upl: limits.ul99[lastIndex], - lpl: limits.ll99[lastIndex] + lpl: limits.ll99[lastIndex], + variation: i === 0 ? "commonCause" : "concernHigh", + assurance: "inconsistent" } this.plotPointsGrouped.push({ diff --git a/src/D3 Plotting Functions/drawSummaryTable.ts b/src/D3 Plotting Functions/drawSummaryTable.ts index d46afa7..d827a95 100644 --- a/src/D3 Plotting Functions/drawSummaryTable.ts +++ b/src/D3 Plotting Functions/drawSummaryTable.ts @@ -76,35 +76,25 @@ export default function drawSummaryTable(selection: divBaseType, visualObj: Visu return typeof d === "number" ? d.toFixed(visualObj.viewModel.inputSettings.settings.spc.sig_figs) : d; }) - //const nhsIconSettings = visualObj.viewModel.inputSettings.settings.nhs_icons; - //const variation_location: string = nhsIconSettings.variation_icons_locations; - - const varSelection = tableSelect.filter((_, i) => i === (cols.length - 1)) - const selHeight = (varSelection.node() as SVGGElement).getBoundingClientRect() - const svg_width: number = selHeight.width - const svg_height: number = selHeight.height - console.log(visualObj.viewModel.plotProperties.height) - console.log(visualObj.viewModel.plotProperties.width) - console.log(selHeight) - - const cellSVG = varSelection.append("svg") - .attr("width", svg_width * 0.8) - .attr("height", svg_height * 0.8) - .classed("rowsvg", true) - + const varSelection = tableSelect.filter((_, i) => i === (cols.length - 2)) + const thisSelDims = (varSelection.node() as SVGGElement).getBoundingClientRect() const scaling = visualObj.viewModel.inputSettings.settings.nhs_icons.variation_icons_scaling - //const scaling_factor: number = (0.08 * (svg_height / 378)) * scaling - cellSVG.call(initialiseIconSVG, "commonCause") - .selectAll(".icongroup") - .selectAll(".commonCause") - .attr("transform", `scale(${0.1 * scaling}) translate(0, ${ ((svg_height * 0.8) / 2) + 179 })`) - .call(nhsIcons.commonCause) - - console.log(selection) - console.log(varSelection) - //console.log(varSelection.enter()) - + const icon_x: number = (thisSelDims.width * 0.8) / 0.08 / 2 - 189; + const icon_y: number = (thisSelDims.height * 0.8) / 0.08 / 2 - 189; + varSelection.each(function(d) { + d3.select(this) + .append("svg") + .attr("width", thisSelDims.width * 0.8) + .attr("height", thisSelDims.height * 0.8) + .classed("rowsvg", true) + .call(initialiseIconSVG, d) + .selectAll(".icongroup") + .attr("viewBox", "0 0 378 378") + .selectAll(`.${d}`) + .attr("transform", `scale(${0.08 * scaling}) translate(${icon_x}, ${icon_y})`) + .call(nhsIcons[d]) + }) selection.on('click', () => { visualObj.selectionManager.clear(); From fb2e20926cadb0100f4245731f4f4ac9ed084be1 Mon Sep 17 00:00:00 2001 From: Andrew Johnson Date: Mon, 5 Aug 2024 23:43:05 +0300 Subject: [PATCH 10/18] Improve handling and switching --- src/Classes/derivedSettingsClass.ts | 10 +- src/Classes/settingsClass.ts | 43 +++- src/Classes/viewModelClass.ts | 29 ++- src/D3 Plotting Functions/drawSummaryTable.ts | 7 +- src/Functions/extractInputData.ts | 22 +- src/Functions/validateDataView.ts | 27 ++- src/Functions/validateInputData.ts | 196 ++++++++++-------- src/visual.ts | 26 ++- 8 files changed, 235 insertions(+), 125 deletions(-) diff --git a/src/Classes/derivedSettingsClass.ts b/src/Classes/derivedSettingsClass.ts index 8e14d5c..7db7536 100644 --- a/src/Classes/derivedSettingsClass.ts +++ b/src/Classes/derivedSettingsClass.ts @@ -21,6 +21,7 @@ export default class derivedSettingsClass { multiplier: number percentLabels: boolean chart_type_props: { + name: string, needs_denominator: boolean, denominator_optional: boolean, numerator_non_negative: boolean, @@ -31,11 +32,11 @@ export default class derivedSettingsClass { value_name: string } - update(inputSettings: defaultSettingsType) { - const chartType: string = inputSettings.spc.chart_type; + update(inputSettingsSpc: defaultSettingsType["spc"]) { + const chartType: string = inputSettingsSpc.chart_type; const pChartType: boolean = ["p", "pp"].includes(chartType); - const percentSettingString: string = inputSettings.spc.perc_labels; - let multiplier: number = inputSettings.spc.multiplier; + const percentSettingString: string = inputSettingsSpc.perc_labels; + let multiplier: number = inputSettingsSpc.multiplier; let percentLabels: boolean; if (percentSettingString === "Yes") { @@ -53,6 +54,7 @@ export default class derivedSettingsClass { } this.chart_type_props = { + name: chartType, needs_denominator: ["p", "pp", "u", "up", "xbar", "s"].includes(chartType), denominator_optional: ["i", "i_m", "i_mm", "run", "mr"].includes(chartType), numerator_non_negative: ["p", "pp", "u", "up", "s", "c", "g", "t"].includes(chartType), diff --git a/src/Classes/settingsClass.ts b/src/Classes/settingsClass.ts index d666303..d0d6a4a 100644 --- a/src/Classes/settingsClass.ts +++ b/src/Classes/settingsClass.ts @@ -4,7 +4,7 @@ type DataViewPropertyValue = powerbi.default.DataViewPropertyValue type VisualObjectInstanceEnumerationObject = powerbi.default.VisualObjectInstanceEnumerationObject; type VisualObjectInstance = powerbi.default.VisualObjectInstance; type VisualObjectInstanceContainer = powerbi.default.VisualObjectInstanceContainer; -import { extractConditionalFormatting } from "../Functions"; +import { extractConditionalFormatting, isNullOrUndefined } from "../Functions"; import { default as defaultSettings, type settingsValueTypes, settingsPaneGroupings, settingsPaneToggles } from "../defaultSettings"; import derivedSettingsClass from "./derivedSettingsClass"; import { type ConditionalReturnT, type SettingsValidationT } from "../Functions/extractConditionalFormatting"; @@ -19,6 +19,10 @@ export type defaultSettingsKey = keyof defaultSettingsType; export type defaultSettingsNestedKey = NestedKeysOf; export type settingsScalarTypes = number | string | boolean; +export type optionalSettingsTypes = Partial<{ + [K in keyof typeof defaultSettings]: Partial; +}>; + export type paneGroupingsNestedKey = "all" | NestedKeysOf; export type paneTogglesNestedKey = "all" | NestedKeysOf; @@ -33,6 +37,8 @@ export default class settingsClass { settings: defaultSettingsType; derivedSettings: derivedSettingsClass; validationStatus: SettingsValidationT; + settingsGrouped: defaultSettingsType[]; + derivedSettingsGrouped: derivedSettingsClass[]; /** * Function to read the values from the settings pane and update the @@ -46,6 +52,22 @@ export default class settingsClass { // Get the names of all classes in settingsObject which have values to be updated const allSettingGroups: string[] = Object.keys(this.settings); + const is_grouped: boolean = inputView.categorical.values?.source?.roles?.indicator; + let group_idxs: number[] = new Array(); + this.settingsGrouped = new Array(); + if (is_grouped) { + group_idxs = inputView.categorical.values.grouped().map(d => { + this.settingsGrouped.push(Object.fromEntries(Object.keys(defaultSettings).map((settingGroupName) => { + return [settingGroupName, Object.fromEntries(Object.keys(defaultSettings[settingGroupName]).map((settingName) => { + return [settingName, defaultSettings[settingGroupName][settingName]]; + }))]; + })) as settingsValueTypes); + + return d.values[0].values.findIndex(d_in => !isNullOrUndefined(d_in)); + }); + } + + allSettingGroups.forEach((settingGroup: defaultSettingsKey) => { const condFormatting: ConditionalReturnT = extractConditionalFormatting(inputView?.categorical, settingGroup, this.settings); @@ -73,10 +95,27 @@ export default class settingsClass { = condFormatting?.values ? condFormatting?.values[0][settingName] : defaultSettings[settingGroup][settingName]["default"] + + if (is_grouped) { + group_idxs.forEach((idx, idx_idx) => { + this.settingsGrouped[idx_idx][settingGroup][settingName] + = condFormatting?.values + ? condFormatting?.values[idx][settingName] + : defaultSettings[settingGroup][settingName]["default"] + }) + } }) }) - this.derivedSettings.update(this.settings) + this.derivedSettings.update(this.settings.spc) + this.derivedSettingsGrouped = new Array(); + if (is_grouped) { + this.settingsGrouped.forEach((d) => { + const newDerived = new derivedSettingsClass(); + newDerived.update(d.spc); + this.derivedSettingsGrouped.push(newDerived); + }) + } } /** diff --git a/src/Classes/viewModelClass.ts b/src/Classes/viewModelClass.ts index 471ee21..ce516f0 100644 --- a/src/Classes/viewModelClass.ts +++ b/src/Classes/viewModelClass.ts @@ -159,18 +159,28 @@ export default class viewModelClass { this.inputDataGrouped = new Array(); this.groupStartEndIndexesGrouped = new Array(); this.controlLimitsGrouped = new Array(); + this.outliersGrouped = new Array(); - options.dataViews[0].categorical.values.grouped().forEach(d => { + options.dataViews[0].categorical.values.grouped().forEach((d, idx) => { (d).categories = options.dataViews[0].categorical.categories; - const inpData: dataObject = extractInputData(d, this.inputSettings); + let first_idx: number = d.values[0].values.findIndex(d_in => !isNullOrUndefined(d_in)); + let last_idx: number = d.values[0].values.map(d_in => !isNullOrUndefined(d_in)).lastIndexOf(true); + const inpData: dataObject = extractInputData(d, + this.inputSettings.settingsGrouped[idx], + this.inputSettings.derivedSettingsGrouped[idx], + this.inputSettings.validationStatus.messages, + first_idx, last_idx); + console.log(first_idx, last_idx, inpData) const groupStartEndIndexes: number[][] = this.getGroupingIndexes(inpData); const limits: controlLimitsObject = this.calculateLimits(inpData, groupStartEndIndexes, this.inputSettings); - this.scaleAndTruncateLimits(limits, this.inputSettings); + const outliers: outliersObject = this.flagOutliers(limits, groupStartEndIndexes, this.inputSettings); + this.scaleAndTruncateLimits(limits, this.inputSettings); this.groupNames.push(d.name); this.inputDataGrouped.push(inpData); this.groupStartEndIndexesGrouped.push(groupStartEndIndexes); this.controlLimitsGrouped.push(limits); + this.outliersGrouped.push(outliers); }) this.initialisePlotDataGrouped(); } else { @@ -179,13 +189,16 @@ export default class viewModelClass { this.inputDataGrouped = null; this.groupStartEndIndexesGrouped = null; this.controlLimitsGrouped = null; - } // Only re-construct data and re-calculate limits if they have changed //if (options.type === 2 || this.firstRun) { const split_indexes_str: string = (options.dataViews[0]?.metadata?.objects?.split_indexes_storage?.split_indexes) ?? "[]"; const split_indexes: number[] = JSON.parse(split_indexes_str); this.splitIndexes = split_indexes; - this.inputData = extractInputData(options.dataViews[0].categorical, this.inputSettings); + this.inputData = extractInputData(options.dataViews[0].categorical, + this.inputSettings.settings, + this.inputSettings.derivedSettings, + this.inputSettings.validationStatus.messages, + 0, options.dataViews[0].categorical.values[0].values.length - 1); if (this.inputData.validationStatus.status === 0) { this.groupStartEndIndexes = this.getGroupingIndexes(this.inputData, this.splitIndexes); @@ -197,6 +210,8 @@ export default class viewModelClass { this.initialisePlotData(host); this.initialiseGroupedLines(); } + + } //} this.plotProperties.update( @@ -270,7 +285,9 @@ export default class viewModelClass { { name: "target", label: "Target" }, { name: "alt_target", label: "Alt. Target" }, { name: "upl", label: "UPL" }, - { name: "lpl", label: "LPL" } + { name: "lpl", label: "LPL" }, + { name: "variation", label: "Variation" }, + { name: "assurance", label: "Assurance" } ]; for (let i: number = 0; i < this.groupNames.length; i++) { diff --git a/src/D3 Plotting Functions/drawSummaryTable.ts b/src/D3 Plotting Functions/drawSummaryTable.ts index d827a95..fbca76f 100644 --- a/src/D3 Plotting Functions/drawSummaryTable.ts +++ b/src/D3 Plotting Functions/drawSummaryTable.ts @@ -26,9 +26,10 @@ export default function drawSummaryTable(selection: divBaseType, visualObj: Visu if (visualObj.viewModel.showGrouped){ plotPoints = visualObj.viewModel.plotPointsGrouped; - cols = visualObj.viewModel.tableColumnsGrouped - .concat({name: "variation", label: "Variation"}) - .concat({name: "assurance", label: "Assurance"}); + cols = visualObj.viewModel.tableColumnsGrouped; + } else { + plotPoints = visualObj.viewModel.plotPoints; + cols = visualObj.viewModel.tableColumns; } selection.select(".table-header") diff --git a/src/Functions/extractInputData.ts b/src/Functions/extractInputData.ts index b077b07..037e97e 100644 --- a/src/Functions/extractInputData.ts +++ b/src/Functions/extractInputData.ts @@ -4,7 +4,7 @@ type PrimitiveValue = powerbi.PrimitiveValue; type DataViewCategorical = powerbi.DataViewCategorical; type VisualTooltipDataItem = powerbi.extensibility.VisualTooltipDataItem; import { extractDataColumn, extractValues, extractConditionalFormatting, validateInputData, isNullOrUndefined } from "../Functions" -import { type defaultSettingsType, type controlLimitsArgs, settingsClass } from "../Classes"; +import { type defaultSettingsType, type controlLimitsArgs, type derivedSettingsClass } from "../Classes"; import type { ValidationT } from "./validateInputData"; export type dataObject = { @@ -43,8 +43,11 @@ function invalidInputData(inputValidStatus: ValidationT): dataObject { } } -export default function extractInputData(inputView: DataViewCategorical, inputSettingsClass: settingsClass): dataObject { - const inputSettings: defaultSettingsType = inputSettingsClass.settings; +export default function extractInputData(inputView: DataViewCategorical, + inputSettings: defaultSettingsType, + derivedSettings: derivedSettingsClass, + validationMessages: string[][], + first_idx: number, last_idx: number): dataObject { const numerators: number[] = extractDataColumn(inputView, "numerators", inputSettings); const denominators: number[] = extractDataColumn(inputView, "denominators", inputSettings); const xbar_sds: number[] = extractDataColumn(inputView, "xbar_sds", inputSettings); @@ -62,9 +65,8 @@ export default function extractInputData(inputView: DataViewCategorical, inputSe const speclimits_upper: number[] = extractConditionalFormatting(inputView, "lines", inputSettings) ?.values .map(d => d.show_specification ? d.specification_upper : null); - const spcSettings = extractConditionalFormatting(inputView, "spc", inputSettings)?.values?.[0]; - - const inputValidStatus: ValidationT = validateInputData(keys, numerators, denominators, xbar_sds, groupings, inputSettingsClass.derivedSettings.chart_type_props); + const spcSettings: defaultSettingsType["spc"][] = extractConditionalFormatting(inputView, "spc", inputSettings)?.values + const inputValidStatus: ValidationT = validateInputData(keys, numerators, denominators, xbar_sds, groupings, derivedSettings.chart_type_props, first_idx, last_idx); if (inputValidStatus.status !== 0) { return invalidInputData(inputValidStatus); @@ -74,9 +76,9 @@ export default function extractInputData(inputView: DataViewCategorical, inputSe const valid_keys: { x: number, id: number, label: string }[] = new Array<{ x: number, id: number, label: string }>(); const removalMessages: string[] = new Array(); const groupVarName: string = inputView.categories[0].source.displayName; - const settingsMessages = inputSettingsClass.validationStatus.messages; + const settingsMessages = validationMessages; let valid_x: number = 0; - for (let i: number = 0; i < numerators.length; i++) { + for (let i: number = first_idx; i <= last_idx; i++) { if (inputValidStatus.messages[i] === "") { valid_ids.push(i); valid_keys.push({ x: valid_x, id: i, label: keys[i] }) @@ -114,7 +116,7 @@ export default function extractInputData(inputView: DataViewCategorical, inputSe } } - if (!inputSettingsClass.derivedSettings.chart_type_props.has_control_limits) { + if (!derivedSettings.chart_type_props.has_control_limits) { removalMessages.push("NHS Assurance icon requires chart with control limits.") } } @@ -127,7 +129,7 @@ export default function extractInputData(inputView: DataViewCategorical, inputSe xbar_sds: extractValues(xbar_sds, valid_ids), outliers_in_limits: false, }, - spcSettings: spcSettings, + spcSettings: spcSettings[first_idx], tooltips: extractValues(tooltips, valid_ids), highlights: extractValues(highlights, valid_ids), anyHighlights: !isNullOrUndefined(highlights), diff --git a/src/Functions/validateDataView.ts b/src/Functions/validateDataView.ts index 8691218..b8d48ce 100644 --- a/src/Functions/validateDataView.ts +++ b/src/Functions/validateDataView.ts @@ -17,8 +17,29 @@ export default function validateDataView(inputDV: powerbi.DataView[], inputSetti if (!numeratorsPresent) { return "No Numerators passed!"; } - const chart_type: string = inputSettingsClass.settings.spc.chart_type; - if (inputSettingsClass.derivedSettings.chart_type_props.needs_denominator) { + + let needs_denominator: boolean; + let needs_sd: boolean; + let chart_type: string; + + if (inputSettingsClass?.derivedSettingsGrouped) { + inputSettingsClass?.derivedSettingsGrouped.forEach((d) => { + if (d.chart_type_props.needs_denominator) { + chart_type = d.chart_type_props.name; + needs_denominator = true; + } + if (d.chart_type_props.needs_sd) { + chart_type = d.chart_type_props.name; + needs_sd = true; + } + }); + } else { + chart_type = inputSettingsClass.settings.spc.chart_type; + needs_denominator = inputSettingsClass.derivedSettings.chart_type_props.needs_denominator; + needs_sd = inputSettingsClass.derivedSettings.chart_type_props.needs_sd; + } + + if (needs_denominator) { const denominatorsPresent: boolean = inputDV[0].categorical ?.values @@ -29,7 +50,7 @@ export default function validateDataView(inputDV: powerbi.DataView[], inputSetti } } - if (inputSettingsClass.derivedSettings.chart_type_props.needs_sd) { + if (needs_sd) { const xbarSDPresent: boolean = inputDV[0].categorical ?.values diff --git a/src/Functions/validateInputData.ts b/src/Functions/validateInputData.ts index b578dc9..2ab0f4a 100644 --- a/src/Functions/validateInputData.ts +++ b/src/Functions/validateInputData.ts @@ -1,9 +1,61 @@ import { derivedSettingsClass } from "../Classes"; import isNullOrUndefined from "./isNullOrUndefined"; -import rep from "./rep"; export type ValidationT = { status: number, messages: string[], error?: string }; +const enum ValidationFailTypes { + Valid = 0, + GroupingMissing = 1, + DateMissing = 2, + NumeratorMissing = 3, + NumeratorNegative = 4, + DenominatorMissing = 5, + DenominatorNegative = 6, + DenominatorLessThanNumerator = 7, + SDMissing = 8, + SDNegative = 9 +} + +function validateInputDataImpl(key: string, numerator: number, denominator: number, + xbar_sd: number, grouping: string, + chart_type_props: derivedSettingsClass["chart_type_props"]): { message: string, type: ValidationFailTypes } { + + let rtn = { message: "", type: ValidationFailTypes.Valid }; + if (isNullOrUndefined(grouping)) { + //rtn.message = "Grouping missing"; + //rtn.type = ValidationFailTypes.GroupingMissing; + } else if (isNullOrUndefined(key)) { + rtn.message = "Date missing"; + rtn.type = ValidationFailTypes.DateMissing; + } else if (isNullOrUndefined(numerator)) { + rtn.message = "Numerator missing"; + rtn.type = ValidationFailTypes.NumeratorMissing; + } else if (chart_type_props.numerator_non_negative && numerator < 0) { + rtn.message = "Numerator negative"; + rtn.type = ValidationFailTypes.NumeratorNegative; + } else if (chart_type_props.needs_denominator || chart_type_props.denominator_optional) { + if (isNullOrUndefined(denominator)) { + rtn.message = "Denominator missing"; + rtn.type = ValidationFailTypes.DenominatorMissing; + } else if (denominator < 0) { + rtn.message = "Denominator negative"; + rtn.type = ValidationFailTypes.DenominatorNegative; + } else if (chart_type_props.numerator_leq_denominator && denominator < numerator) { + rtn.message = "Denominator < numerator"; + rtn.type = ValidationFailTypes.DenominatorLessThanNumerator; + } + } else if (chart_type_props.needs_sd) { + if (isNullOrUndefined(xbar_sd)) { + rtn.message = "SD missing"; + rtn.type = ValidationFailTypes.SDMissing; + } else if (xbar_sd < 0) { + rtn.message = "SD negative"; + rtn.type = ValidationFailTypes.SDNegative; + } + } + return rtn; +} + // ESLint errors due to number of lines in function, but would reduce readability to separate further /* eslint-disable */ export default function validateInputData(keys: string[], @@ -11,100 +63,64 @@ export default function validateInputData(keys: string[], denominators: number[], xbar_sds: number[], groupings: string[], - chart_type_props: derivedSettingsClass["chart_type_props"]): { status: number, messages: string[], error?: string } { - - const check_optional: boolean = chart_type_props.denominator_optional && !isNullOrUndefined(denominators); - - const validationRtn: ValidationT = { status: 0, messages: rep("", keys.length) }; - - if (!isNullOrUndefined(groupings)) { - groupings.forEach((d, idx) => { - validationRtn.messages[idx] = validationRtn.messages[idx] === "" - ? (!isNullOrUndefined(d) ? "" : "Grouping missing") - : validationRtn.messages[idx] - }); - } - keys.forEach((d, idx) => { - validationRtn.messages[idx] = validationRtn.messages[idx] === "" - ? (!isNullOrUndefined(d) ? "" : "Date missing") - : validationRtn.messages[idx]}); - if (!validationRtn.messages.some(d => d == "")) { - validationRtn.status = 1; - validationRtn.error = "All dates/IDs are missing or null!"; - return validationRtn; + chart_type_props: derivedSettingsClass["chart_type_props"], + first_idx: number, last_idx: number): { status: number, messages: string[], error?: string } { + let allSameType: boolean = false; + let messages: string[] = new Array(); + let all_status: ValidationFailTypes[] = new Array(); + for (let i = first_idx; i <= last_idx; i++) { + const validation = validateInputDataImpl(keys[i], numerators?.[i], denominators?.[i], xbar_sds?.[i], groupings?.[i], chart_type_props); + messages.push(validation.message); + all_status.push(validation.type); } - numerators.forEach((d, idx) => { - validationRtn.messages[idx] = validationRtn.messages[idx] === "" - ? (!isNullOrUndefined(d) ? "" : "Numerator missing") - : validationRtn.messages[idx]}); - if (!validationRtn.messages.some(d => d == "")) { - validationRtn.status = 1; - validationRtn.error = "All numerators are missing or null!"; - return validationRtn; - } - if (chart_type_props.numerator_non_negative) { - numerators.forEach((d, idx) => { - validationRtn.messages[idx] = validationRtn.messages[idx] === "" - ? ((d >= 0) ? "" : "Numerator negative") - : validationRtn.messages[idx]}); - if (!validationRtn.messages.some(d => d == "")) { - validationRtn.status = 1; - validationRtn.error = "All numerators are negative!"; - return validationRtn; - } - } + let allSameTypeSet = new Set(all_status); + allSameType = allSameTypeSet.size === 1; + let commonType = Array.from(allSameTypeSet)[0]; - if (chart_type_props.needs_denominator || check_optional) { - denominators.forEach((d, idx) => { - validationRtn.messages[idx] = validationRtn.messages[idx] === "" - ? (!isNullOrUndefined(d) ? "" : "Denominator missing") - : validationRtn.messages[idx]}); - if (!validationRtn.messages.some(d => d == "")) { - validationRtn.status = 1; - validationRtn.error = "All denominators missing or null!"; - return validationRtn; - } - denominators.forEach((d, idx) => { - validationRtn.messages[idx] = validationRtn.messages[idx] === "" - ? ((d >= 0) ? "" : "Denominator negative") - : validationRtn.messages[idx]}); - if (!validationRtn.messages.some(d => d == "")) { - validationRtn.status = 1; - validationRtn.error = "All denominators are negative!"; - return validationRtn; - } - if (chart_type_props.numerator_leq_denominator) { - denominators.forEach((d, idx) => { - validationRtn.messages[idx] = validationRtn.messages[idx] === "" - ? ((d >= numerators[idx]) ? "" : "Denominator < numerator") - : validationRtn.messages[idx]}); - if (!validationRtn.messages.some(d => d == "")) { - validationRtn.status = 1; + let validationRtn: ValidationT = { + status: (allSameType && commonType === ValidationFailTypes.Valid) ? 0 : 1, + messages: messages + }; + + if (allSameType && commonType !== ValidationFailTypes.Valid) { + switch(commonType) { + case 1: { + validationRtn.error = "Grouping missing" + break; + } + case 2: { + validationRtn.error = "All dates/IDs are missing or null!" + break; + } + case 3: { + validationRtn.error = "All numerators are missing or null!" + break; + } + case 4: { + validationRtn.error = "All numerators are negative!" + break; + } + case 5: { + validationRtn.error = "All denominators missing or null!" + break; + } + case 6: { + validationRtn.error = "All denominators are negative!" + break; + } + case 7: { validationRtn.error = "All denominators are smaller than numerators!"; - return validationRtn; + break; + } + case 8: { + validationRtn.error = "All SDs missing or null!"; + break; + } + case 9: { + validationRtn.error = "All SDs are negative!"; + break; } - } - } - - if (chart_type_props.needs_sd) { - xbar_sds.forEach((d, idx) => { - validationRtn.messages[idx] = validationRtn.messages[idx] === "" - ? (!isNullOrUndefined(d) ? "" : "SD missing") - : validationRtn.messages[idx]}); - if (!validationRtn.messages.some(d => d == "")) { - validationRtn.status = 1; - validationRtn.error = "All SDs missing or null!"; - return validationRtn; - } - xbar_sds.forEach((d, idx) => { - validationRtn.messages[idx] = validationRtn.messages[idx] === "" - ? ((d >=0) ? "" : "SD negative") - : validationRtn.messages[idx]}); - if (!validationRtn.messages.some(d => d == "")) { - validationRtn.status = 1; - validationRtn.error = "All SDs are negative!"; - return validationRtn; } } return validationRtn; diff --git a/src/visual.ts b/src/visual.ts index 56b9955..e0c42d7 100644 --- a/src/visual.ts +++ b/src/visual.ts @@ -62,19 +62,31 @@ export class Visual implements powerbi.extensibility.IVisual { } this.viewModel.update(options, this.host); - console.log(this.viewModel) + + if (this.viewModel.showGrouped) { + if (this.viewModel.inputDataGrouped.map(d => d.validationStatus.status).some(d => d !== 0)) { + this.processVisualError(options, + this.viewModel.inputDataGrouped.map(d => d.validationStatus.error).join("\n")); + return; + } + + this.svg.attr("width", 0).attr("height", 0); + this.tableDiv.call(drawSummaryTable, this); + + if (this.viewModel.inputDataGrouped.some(d => d.warningMessage !== "")) { + this.host.displayWarningIcon("Invalid inputs or settings ignored.\n", + this.viewModel.inputDataGrouped.map(d => d.warningMessage).join("\n")); + } + } else { if (this.viewModel.inputData.validationStatus.status !== 0) { - this.processVisualError(options, - this.viewModel.inputData.validationStatus.error); + this.processVisualError(options, this.viewModel.inputData.validationStatus.error); return; } - if (this.viewModel.inputSettings.settings.summary_table.show_table || - this.viewModel.showGrouped) { + if (this.viewModel.inputSettings.settings.summary_table.show_table) { this.svg.attr("width", 0).attr("height", 0); this.tableDiv.call(drawSummaryTable, this) .call(addContextMenu, this); - console.log(this) } else { this.tableDiv.style("width", "0%").style("height", "0%"); this.svg.attr("width", options.viewport.width) @@ -95,7 +107,7 @@ export class Visual implements powerbi.extensibility.IVisual { this.host.displayWarningIcon("Invalid inputs or settings ignored.\n", this.viewModel.inputData.warningMessage); } - + } this.host.eventService.renderingFinished(options); } catch (caught_error) { this.tableDiv.style("width", "0%").style("height", "0%"); From 8fdf6aeb41124a6e6b26ac216f5dece838cc9e8b Mon Sep 17 00:00:00 2001 From: Andrew Johnson Date: Tue, 6 Aug 2024 00:08:46 +0300 Subject: [PATCH 11/18] Fix svg dim change --- src/Classes/plotPropertiesClass.ts | 14 ++----- src/Classes/viewModelClass.ts | 39 +++++++++++-------- .../drawDownloadButton.ts | 4 +- src/D3 Plotting Functions/drawIcons.ts | 4 +- src/D3 Plotting Functions/drawTooltipLine.ts | 4 +- src/D3 Plotting Functions/drawXAxis.ts | 8 ++-- src/D3 Plotting Functions/drawYAxis.ts | 5 ++- 7 files changed, 40 insertions(+), 38 deletions(-) diff --git a/src/Classes/plotPropertiesClass.ts b/src/Classes/plotPropertiesClass.ts index d7ef178..25145c5 100644 --- a/src/Classes/plotPropertiesClass.ts +++ b/src/Classes/plotPropertiesClass.ts @@ -24,8 +24,6 @@ export type axisProperties = { }; export default class plotPropertiesClass { - width: number; - height: number; displayPlot: boolean; xAxis: axisProperties; yAxis: axisProperties; @@ -33,15 +31,15 @@ export default class plotPropertiesClass { yScale: d3.ScaleLinear; // Separate function so that the axis can be re-calculated on changes to padding - initialiseScale(): void { + initialiseScale(svgWidth: number, svgHeight: number): void { this.xScale = d3.scaleLinear() .domain([this.xAxis.lower, this.xAxis.upper]) .range([this.xAxis.start_padding, - this.width - this.xAxis.end_padding]); + svgWidth - this.xAxis.end_padding]); this.yScale = d3.scaleLinear() .domain([this.yAxis.lower, this.yAxis.upper]) - .range([this.height - this.yAxis.start_padding, + .range([svgHeight - this.yAxis.start_padding, this.yAxis.end_padding]); } @@ -53,10 +51,6 @@ export default class plotPropertiesClass { derivedSettings: derivedSettingsClass, colorPalette: colourPaletteType): void { - // Get the width and height of plotting space - this.width = options.viewport.width; - this.height = options.viewport.height; - this.displayPlot = plotPoints ? plotPoints.length > 1 : null; @@ -154,6 +148,6 @@ export default class plotPropertiesClass { label_colour: colorPalette.isHighContrast ? colorPalette.foregroundColour : inputSettings.y_axis.ylimit_label_colour }; - this.initialiseScale(); + this.initialiseScale(options.viewport.width, options.viewport.height); } } diff --git a/src/Classes/viewModelClass.ts b/src/Classes/viewModelClass.ts index ce516f0..8bc914d 100644 --- a/src/Classes/viewModelClass.ts +++ b/src/Classes/viewModelClass.ts @@ -120,6 +120,8 @@ export default class viewModelClass { firstRun: boolean; colourPalette: colourPaletteType; tableColumns: { name: string; label: string; }[]; + svgWidth: number; + svgHeight: number; showGrouped: boolean; groupNames: string[]; @@ -153,6 +155,9 @@ export default class viewModelClass { } } + this.svgWidth = options.viewport.width; + this.svgHeight = options.viewport.height; + if (options.dataViews[0].categorical.values?.source?.roles?.indicator) { this.showGrouped = true; this.groupNames = new Array(); @@ -170,12 +175,14 @@ export default class viewModelClass { this.inputSettings.derivedSettingsGrouped[idx], this.inputSettings.validationStatus.messages, first_idx, last_idx); - console.log(first_idx, last_idx, inpData) - const groupStartEndIndexes: number[][] = this.getGroupingIndexes(inpData); - const limits: controlLimitsObject = this.calculateLimits(inpData, groupStartEndIndexes, this.inputSettings); - const outliers: outliersObject = this.flagOutliers(limits, groupStartEndIndexes, this.inputSettings); + const invalidData: boolean = inpData.validationStatus.status !== 0; + const groupStartEndIndexes: number[][] = invalidData ? new Array() : this.getGroupingIndexes(inpData); + const limits: controlLimitsObject = invalidData ? null : this.calculateLimits(inpData, groupStartEndIndexes, this.inputSettings); + const outliers: outliersObject = invalidData ? null : this.flagOutliers(limits, groupStartEndIndexes, this.inputSettings); - this.scaleAndTruncateLimits(limits, this.inputSettings); + if (!invalidData) { + this.scaleAndTruncateLimits(limits, this.inputSettings); + } this.groupNames.push(d.name); this.inputDataGrouped.push(inpData); this.groupStartEndIndexesGrouped.push(groupStartEndIndexes); @@ -210,19 +217,17 @@ export default class viewModelClass { this.initialisePlotData(host); this.initialiseGroupedLines(); } - + //} + this.plotProperties.update( + options, + this.plotPoints, + this.controlLimits, + this.inputData, + this.inputSettings.settings, + this.inputSettings.derivedSettings, + this.colourPalette + ) } - //} - - this.plotProperties.update( - options, - this.plotPoints, - this.controlLimits, - this.inputData, - this.inputSettings.settings, - this.inputSettings.derivedSettings, - this.colourPalette - ) this.firstRun = false; } diff --git a/src/D3 Plotting Functions/drawDownloadButton.ts b/src/D3 Plotting Functions/drawDownloadButton.ts index f4199d0..61ffa15 100644 --- a/src/D3 Plotting Functions/drawDownloadButton.ts +++ b/src/D3 Plotting Functions/drawDownloadButton.ts @@ -16,8 +16,8 @@ export default function drawDownloadButton(selection: svgBaseType, visualObj: Vi csv_rows.push(Object.values(row).join(",")); }); selection.select(".download-btn-group") - .attr("x", visualObj.viewModel.plotProperties.width - 50) - .attr("y", visualObj.viewModel.plotProperties.height - 5) + .attr("x", visualObj.viewModel.svgWidth - 50) + .attr("y", visualObj.viewModel.svgHeight - 5) .text("Download") .style("font-size", "10px") .style("text-decoration", "underline") diff --git a/src/D3 Plotting Functions/drawIcons.ts b/src/D3 Plotting Functions/drawIcons.ts index 23bc8d0..5bc10cd 100644 --- a/src/D3 Plotting Functions/drawIcons.ts +++ b/src/D3 Plotting Functions/drawIcons.ts @@ -13,8 +13,8 @@ export default function drawIcons(selection: svgBaseType, visualObj: Visual): vo const nhsIconSettings: defaultSettingsType["nhs_icons"] = visualObj.viewModel.inputSettings.settings.nhs_icons; const draw_variation: boolean = nhsIconSettings.show_variation_icons; const variation_location: string = nhsIconSettings.variation_icons_locations; - const svg_width: number = visualObj.viewModel.plotProperties.width - const svg_height: number = visualObj.viewModel.plotProperties.height + const svg_width: number = visualObj.viewModel.svgWidth + const svg_height: number = visualObj.viewModel.svgHeight let numVariationIcons: number = 0; if (draw_variation) { diff --git a/src/D3 Plotting Functions/drawTooltipLine.ts b/src/D3 Plotting Functions/drawTooltipLine.ts index f6cf85e..fd49f14 100644 --- a/src/D3 Plotting Functions/drawTooltipLine.ts +++ b/src/D3 Plotting Functions/drawTooltipLine.ts @@ -9,14 +9,14 @@ export default function drawTooltipLine(selection: svgBaseType, visualObj: Visua .attr("x1", 0) .attr("x2", 0) .attr("y1", plotProperties.yAxis.end_padding) - .attr("y2", plotProperties.height - plotProperties.yAxis.start_padding) + .attr("y2", visualObj.viewModel.svgHeight - plotProperties.yAxis.start_padding) .attr("stroke-width", "1px") .attr("stroke", "black") .style("stroke-opacity", 0); const yAxisLine = selection .select(".ttip-line-y") .attr("x1", plotProperties.xAxis.start_padding) - .attr("x2", plotProperties.width - plotProperties.xAxis.end_padding) + .attr("x2", visualObj.viewModel.svgWidth - plotProperties.xAxis.end_padding) .attr("y1", 0) .attr("y2", 0) .attr("stroke-width", "1px") diff --git a/src/D3 Plotting Functions/drawXAxis.ts b/src/D3 Plotting Functions/drawXAxis.ts index d109296..600e210 100644 --- a/src/D3 Plotting Functions/drawXAxis.ts +++ b/src/D3 Plotting Functions/drawXAxis.ts @@ -22,7 +22,7 @@ export default function drawXAxis(selection: svgBaseType, visualObj: Visual, ref xAxis.tickValues([]); } - const plotHeight: number = visualObj.viewModel.plotProperties.height; + const plotHeight: number = visualObj.viewModel.svgHeight; const xAxisHeight: number = plotHeight - visualObj.viewModel.plotProperties.yAxis.start_padding; const displayPlot: boolean = visualObj.viewModel.plotProperties.displayPlot; const xAxisGroup = selection.select(".xaxisgroup") as d3.Selection; @@ -64,7 +64,9 @@ export default function drawXAxis(selection: svgBaseType, visualObj: Visual, ref if (tickLeftofPadding < 0) { visualObj.viewModel.plotProperties.xAxis.start_padding += abs(tickLeftofPadding) } - visualObj.viewModel.plotProperties.initialiseScale(); + visualObj.viewModel.plotProperties.initialiseScale(visualObj.viewModel.svgWidth, + visualObj.viewModel.svgHeight + ); selection.call(drawXAxis, visualObj, true); return; } @@ -73,7 +75,7 @@ export default function drawXAxis(selection: svgBaseType, visualObj: Visual, ref const bottomMidpoint: number = plotHeight - ((plotHeight - xAxisCoordinates.bottom) / 2); selection.select(".xaxislabel") - .attr("x",visualObj.viewModel.plotProperties.width / 2) + .attr("x",visualObj.viewModel.svgWidth / 2) .attr("y", bottomMidpoint) .style("text-anchor", "middle") .text(xAxisProperties.label) diff --git a/src/D3 Plotting Functions/drawYAxis.ts b/src/D3 Plotting Functions/drawYAxis.ts index fa065f6..c30af52 100644 --- a/src/D3 Plotting Functions/drawYAxis.ts +++ b/src/D3 Plotting Functions/drawYAxis.ts @@ -56,14 +56,15 @@ export default function drawYAxis(selection: svgBaseType, visualObj: Visual, ref if (tickLeftofPadding < 0) { if (!refresh) { visualObj.viewModel.plotProperties.xAxis.start_padding += abs(tickLeftofPadding) - visualObj.viewModel.plotProperties.initialiseScale(); + visualObj.viewModel.plotProperties.initialiseScale(visualObj.viewModel.svgWidth, + visualObj.viewModel.svgHeight); selection.call(drawYAxis, visualObj, true).call(drawXAxis, visualObj, true); return; } } const leftMidpoint: number = yAxisCoordinates.x * 0.7; - const y: number = visualObj.viewModel.plotProperties.height / 2; + const y: number = visualObj.viewModel.svgHeight / 2; selection.select(".yaxislabel") .attr("x",leftMidpoint) From f92ed9bd9c32b4ba7da345210727506f38c00e15 Mon Sep 17 00:00:00 2001 From: Andrew Johnson Date: Tue, 6 Aug 2024 11:14:11 +0300 Subject: [PATCH 12/18] Cleanup rendering updates and overflow --- src/Classes/viewModelClass.ts | 70 +++++++++++++++---- src/D3 Plotting Functions/drawIcons.ts | 6 +- src/D3 Plotting Functions/drawSummaryTable.ts | 58 +++++++++------ src/Outlier Flagging/assuranceIconToDraw.ts | 10 +-- src/Outlier Flagging/variationIconsToDraw.ts | 8 +-- 5 files changed, 106 insertions(+), 46 deletions(-) diff --git a/src/Classes/viewModelClass.ts b/src/Classes/viewModelClass.ts index 8bc914d..583d880 100644 --- a/src/Classes/viewModelClass.ts +++ b/src/Classes/viewModelClass.ts @@ -6,7 +6,7 @@ type ISelectionId = powerbi.visuals.ISelectionId; import * as d3 from "../D3 Plotting Functions/D3 Modules"; import * as limitFunctions from "../Limit Calculations" import { settingsClass, type defaultSettingsType, plotPropertiesClass } from "../Classes"; -import { buildTooltip, getAesthetic, checkFlagDirection, truncate, type truncateInputs, multiply, rep, type dataObject, extractInputData, isNullOrUndefined } from "../Functions" +import { buildTooltip, getAesthetic, checkFlagDirection, truncate, type truncateInputs, multiply, rep, type dataObject, extractInputData, isNullOrUndefined, variationIconsToDraw, assuranceIconToDraw } from "../Functions" import { astronomical, trend, twoInThree, shift } from "../Outlier Flagging" export type lineData = { @@ -42,8 +42,12 @@ export type summaryTableRowDataGrouped = { value: number; target: number; alt_target: number; - upl: number; - lpl: number; + ucl99: number; + ucl95: number; + ucl68: number; + lcl68: number; + lcl95: number; + lcl99: number; variation: string; assurance: string; } @@ -285,21 +289,57 @@ export default class viewModelClass { this.plotPointsGrouped = new Array(); this.tableColumnsGrouped = [ { name: "indicator", label: "Indicator" }, - { name: "latest_date", label: "Latest Date" }, - { name: "value", label: "Value" }, - { name: "target", label: "Target" }, - { name: "alt_target", label: "Alt. Target" }, + { name: "latest_date", label: "Latest Date" } + ]; + const lineSettings = this.inputSettings.settings.lines; + if (lineSettings.show_main) { + this.tableColumnsGrouped.push({ name: "value", label: this.inputSettings.derivedSettings.chart_type_props.value_name }); + } + if (lineSettings.show_target) { + this.tableColumnsGrouped.push({ name: "target", label: lineSettings.ttip_label_target }); + } + if (lineSettings.show_alt_target) { + this.tableColumnsGrouped.push({ name: "alt_target", label: lineSettings.ttip_label_alt_target }); + } + ["99", "95", "68"].forEach(limit => { + if (lineSettings[`show_${limit}`]) { + this.tableColumnsGrouped.push({ + name: `ucl${limit}`, + label: `Upper ${lineSettings[`ttip_label_${limit}`]}` + }) + } + }); + ["68", "95", "99"].forEach(limit => { + if (lineSettings[`show_${limit}`]) { + this.tableColumnsGrouped.push({ + name: `lcl${limit}`, + label: `Lower ${lineSettings[`ttip_label_${limit}`]}` + }) + } + }) + const nhsIconSettings: defaultSettingsType["nhs_icons"] = this.inputSettings.settings.nhs_icons; + if (nhsIconSettings.show_variation_icons) { + this.tableColumnsGrouped.push({ name: "variation", label: "Variation" }); + } + if (nhsIconSettings.show_assurance_icons) { + this.tableColumnsGrouped.push({ name: "assurance", label: "Assurance" }); + } + + /* { name: "upl", label: "UPL" }, { name: "lpl", label: "LPL" }, { name: "variation", label: "Variation" }, { name: "assurance", label: "Assurance" } ]; - + */ for (let i: number = 0; i < this.groupNames.length; i++) { //const inputData: dataObject = this.inputDataGrouped[i]; const limits: controlLimitsObject = this.controlLimitsGrouped[i]; - //const outliers: outliersObject = this.outliersGrouped[i]; + const outliers: outliersObject = this.outliersGrouped[i]; const lastIndex: number = limits.keys.length - 1; + const varIcons: string[] = variationIconsToDraw(outliers, this.inputSettings.settingsGrouped[i]); + const assIcon: string = assuranceIconToDraw(limits, this.inputSettings.settingsGrouped[i], + this.inputSettings.derivedSettingsGrouped[i]); const table_row: summaryTableRowDataGrouped = { indicator: this.groupNames[i], @@ -307,10 +347,14 @@ export default class viewModelClass { value: limits.values[lastIndex], target: limits.targets[lastIndex], alt_target: limits.alt_targets[lastIndex], - upl: limits.ul99[lastIndex], - lpl: limits.ll99[lastIndex], - variation: i === 0 ? "commonCause" : "concernHigh", - assurance: "inconsistent" + ucl99: limits.ul99[lastIndex], + ucl95: limits.ul95[lastIndex], + ucl68: limits.ul68[lastIndex], + lcl68: limits.ll68[lastIndex], + lcl95: limits.ll95[lastIndex], + lcl99: limits.ll99[lastIndex], + variation: varIcons[0], + assurance: assIcon } this.plotPointsGrouped.push({ diff --git a/src/D3 Plotting Functions/drawIcons.ts b/src/D3 Plotting Functions/drawIcons.ts index 5bc10cd..2330ce4 100644 --- a/src/D3 Plotting Functions/drawIcons.ts +++ b/src/D3 Plotting Functions/drawIcons.ts @@ -19,7 +19,7 @@ export default function drawIcons(selection: svgBaseType, visualObj: Visual): vo if (draw_variation) { const variation_scaling: number = nhsIconSettings.variation_icons_scaling; - const variationIconsPresent: string[] = variationIconsToDraw(visualObj.viewModel.outliers, visualObj.viewModel.inputSettings); + const variationIconsPresent: string[] = variationIconsToDraw(visualObj.viewModel.outliers, visualObj.viewModel.inputSettings.settings); variationIconsPresent.forEach((icon: string, idx: number) => { selection .call(initialiseIconSVG, icon, iconTransformSpec(svg_width, svg_height, variation_location, variation_scaling, idx)) @@ -33,7 +33,9 @@ export default function drawIcons(selection: svgBaseType, visualObj: Visual): vo if (draw_assurance) { const assurance_location: string = nhsIconSettings.assurance_icons_locations; const assurance_scaling: number = nhsIconSettings.assurance_icons_scaling; - const assuranceIconPresent: string = assuranceIconToDraw(visualObj.viewModel.controlLimits, visualObj.viewModel.inputSettings); + const assuranceIconPresent: string = assuranceIconToDraw(visualObj.viewModel.controlLimits, + visualObj.viewModel.inputSettings.settings, + visualObj.viewModel.inputSettings.derivedSettings); if (assuranceIconPresent === "none") { return; } diff --git a/src/D3 Plotting Functions/drawSummaryTable.ts b/src/D3 Plotting Functions/drawSummaryTable.ts index fbca76f..2ef3862 100644 --- a/src/D3 Plotting Functions/drawSummaryTable.ts +++ b/src/D3 Plotting Functions/drawSummaryTable.ts @@ -7,7 +7,7 @@ import * as d3 from "./D3 Modules"; export default function drawSummaryTable(selection: divBaseType, visualObj: Visual) { selection.style("height", "100%").style("width", "100%"); selection.selectAll(".iconrow").remove(); - selection.selectAll(".rowsvg").remove(); + selection.selectAll(".cell-text").remove(); if (selection.select(".table-group").empty()) { const table = selection.append("table") @@ -32,6 +32,8 @@ export default function drawSummaryTable(selection: divBaseType, visualObj: Visu cols = visualObj.viewModel.tableColumns; } + let maxWidth: number = visualObj.viewModel.svgWidth / cols.length; + selection.select(".table-header") .selectAll("th") .data(cols) @@ -41,7 +43,10 @@ export default function drawSummaryTable(selection: divBaseType, visualObj: Visu .style("padding", "5px") .style("background-color", "lightgray") .style("font-weight", "bold") - .style("text-transform", "uppercase"); + .style("text-transform", "uppercase") + .style("overflow", "hidden") + .style("text-overflow", "ellipsis") + .style("max-width", `${maxWidth}px`); const tableSelect = selection.select(".table-body") .selectAll('tr') @@ -60,7 +65,9 @@ export default function drawSummaryTable(selection: divBaseType, visualObj: Visu }) */ .selectAll('td') - .data((d) => cols.map(col => d.table_row[col.name])) + .data((d) => cols.map(col => { + return {column: col.name, value: d.table_row[col.name]} + })) .join('td') .on("mouseover", (event) => { d3.select(event.target).style("background-color", "lightgray"); @@ -70,31 +77,36 @@ export default function drawSummaryTable(selection: divBaseType, visualObj: Visu }) .style("border", "1px black solid") .style("padding", "5px") - .style("font-size", "12px") - - tableSelect.filter((_, i) => i < (cols.length - 2)) - .text((d) => { - return typeof d === "number" ? d.toFixed(visualObj.viewModel.inputSettings.settings.spc.sig_figs) : d; - }) + .style("font-size", "12px"); - const varSelection = tableSelect.filter((_, i) => i === (cols.length - 2)) - const thisSelDims = (varSelection.node() as SVGGElement).getBoundingClientRect() + const nhsIconSettings = visualObj.viewModel.inputSettings.settings.nhs_icons; + const draw_icons: boolean = nhsIconSettings.show_variation_icons || nhsIconSettings.show_assurance_icons; + const thisSelDims = (tableSelect.node() as SVGGElement).getBoundingClientRect() const scaling = visualObj.viewModel.inputSettings.settings.nhs_icons.variation_icons_scaling const icon_x: number = (thisSelDims.width * 0.8) / 0.08 / 2 - 189; const icon_y: number = (thisSelDims.height * 0.8) / 0.08 / 2 - 189; - varSelection.each(function(d) { - d3.select(this) - .append("svg") - .attr("width", thisSelDims.width * 0.8) - .attr("height", thisSelDims.height * 0.8) - .classed("rowsvg", true) - .call(initialiseIconSVG, d) - .selectAll(".icongroup") - .attr("viewBox", "0 0 378 378") - .selectAll(`.${d}`) - .attr("transform", `scale(${0.08 * scaling}) translate(${icon_x}, ${icon_y})`) - .call(nhsIcons[d]) + + tableSelect.each(function(d) { + if (draw_icons && (d.column === "variation" || d.column === "assurance")) { + d3.select(this) + .append("svg") + .attr("width", thisSelDims.width * 0.8) + .attr("height", thisSelDims.height * 0.8) + .classed("rowsvg", true) + .call(initialiseIconSVG, d.value) + .selectAll(".icongroup") + .attr("viewBox", "0 0 378 378") + .selectAll(`.${d.value}`) + .attr("transform", `scale(${0.08 * scaling}) translate(${icon_x}, ${icon_y})`) + .call(nhsIcons[d.value]); + } else { + let value: string = typeof d.value === "number" + ? d.value.toFixed(visualObj.viewModel.inputSettings.settings.spc.sig_figs) + : d.value; + + d3.select(this).text(value).classed("cell-text", true); + } }) selection.on('click', () => { diff --git a/src/Outlier Flagging/assuranceIconToDraw.ts b/src/Outlier Flagging/assuranceIconToDraw.ts index 42bc46c..bd5503c 100644 --- a/src/Outlier Flagging/assuranceIconToDraw.ts +++ b/src/Outlier Flagging/assuranceIconToDraw.ts @@ -1,11 +1,13 @@ -import type { controlLimitsObject, settingsClass } from "../Classes"; +import type { controlLimitsObject, defaultSettingsType, derivedSettingsClass } from "../Classes"; import { isNullOrUndefined } from "../Functions"; -export default function assuranceIconToDraw(controlLimits: controlLimitsObject, inputSettings: settingsClass): string { - if (!(inputSettings.derivedSettings.chart_type_props.has_control_limits)) { +export default function assuranceIconToDraw(controlLimits: controlLimitsObject, + inputSettings: defaultSettingsType, + derivedSettings: derivedSettingsClass): string { + if (!(derivedSettings.chart_type_props.has_control_limits)) { return "none"; } - const imp_direction: string = inputSettings.settings.outliers.improvement_direction; + const imp_direction: string = inputSettings.outliers.improvement_direction; const N: number = controlLimits.ll99.length - 1; const alt_target: number = controlLimits?.alt_targets?.[N]; diff --git a/src/Outlier Flagging/variationIconsToDraw.ts b/src/Outlier Flagging/variationIconsToDraw.ts index 8f3ce94..636ee85 100644 --- a/src/Outlier Flagging/variationIconsToDraw.ts +++ b/src/Outlier Flagging/variationIconsToDraw.ts @@ -1,7 +1,7 @@ -import type { outliersObject, settingsClass } from "../Classes"; +import type { defaultSettingsType, outliersObject } from "../Classes"; -export default function variationIconsToDraw(outliers: outliersObject, inputSettings: settingsClass): string[] { - const imp_direction: string = inputSettings.settings.outliers.improvement_direction; +export default function variationIconsToDraw(outliers: outliersObject, inputSettings: defaultSettingsType): string[] { + const imp_direction: string = inputSettings.outliers.improvement_direction; const suffix_map: Record = { "increase" : "High", "decrease" : "Low", @@ -13,7 +13,7 @@ export default function variationIconsToDraw(outliers: outliersObject, inputSett "" : "" } const suffix: string = suffix_map[imp_direction]; - const flag_last: boolean = inputSettings.settings.nhs_icons.flag_last_point; + const flag_last: boolean = inputSettings.nhs_icons.flag_last_point; let allFlags: string[]; if (flag_last) { const N: number = outliers.astpoint.length - 1; From 5a496589ef45fd34db7d119ead99263dc6e12176 Mon Sep 17 00:00:00 2001 From: Andrew Johnson Date: Tue, 6 Aug 2024 11:40:59 +0300 Subject: [PATCH 13/18] Fixes for highlighting --- src/D3 Plotting Functions/drawSummaryTable.ts | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/D3 Plotting Functions/drawSummaryTable.ts b/src/D3 Plotting Functions/drawSummaryTable.ts index 2ef3862..963ea9b 100644 --- a/src/D3 Plotting Functions/drawSummaryTable.ts +++ b/src/D3 Plotting Functions/drawSummaryTable.ts @@ -70,14 +70,21 @@ export default function drawSummaryTable(selection: divBaseType, visualObj: Visu })) .join('td') .on("mouseover", (event) => { - d3.select(event.target).style("background-color", "lightgray"); + d3.select(event.target).select(function(){ + return this.closest("td"); + }).style("background-color", "lightgray"); }) .on("mouseout", (event) => { - d3.select(event.target).style("background-color", "white"); + d3.select(event.target).select(function(){ + return this.closest("td"); + }).style("background-color", "white"); }) .style("border", "1px black solid") .style("padding", "5px") - .style("font-size", "12px"); + .style("font-size", "12px") + .style("overflow", "hidden") + .style("text-overflow", "ellipsis") + .style("max-width", `${maxWidth}px`); const nhsIconSettings = visualObj.viewModel.inputSettings.settings.nhs_icons; const draw_icons: boolean = nhsIconSettings.show_variation_icons || nhsIconSettings.show_assurance_icons; From 14cc68f283ac173897682254bbfbd331466a0c06 Mon Sep 17 00:00:00 2001 From: Andrew Johnson Date: Tue, 6 Aug 2024 13:26:30 +0300 Subject: [PATCH 14/18] Cross-plot interactivity --- src/Classes/viewModelClass.ts | 24 +++++++++++++++---- src/D3 Plotting Functions/drawSummaryTable.ts | 13 ++++++---- src/Functions/extractInputData.ts | 13 ++++++---- src/visual.ts | 13 ++++++---- 4 files changed, 44 insertions(+), 19 deletions(-) diff --git a/src/Classes/viewModelClass.ts b/src/Classes/viewModelClass.ts index 583d880..b614038 100644 --- a/src/Classes/viewModelClass.ts +++ b/src/Classes/viewModelClass.ts @@ -67,6 +67,9 @@ export type plotData = { export type plotDataGrouped = { table_row: summaryTableRowDataGrouped; + identity: ISelectionId; + aesthetics: defaultSettingsType["scatter"]; + highlighted: boolean; } export type controlLimitsObject = { @@ -135,6 +138,7 @@ export default class viewModelClass { groupStartEndIndexesGrouped: number[][][]; tableColumnsGrouped: { name: string; label: string; }[]; plotPointsGrouped: plotDataGrouped[]; + identitiesGrouped: ISelectionId[]; constructor() { this.inputData = null; @@ -169,6 +173,7 @@ export default class viewModelClass { this.groupStartEndIndexesGrouped = new Array(); this.controlLimitsGrouped = new Array(); this.outliersGrouped = new Array(); + this.identitiesGrouped = new Array(); options.dataViews[0].categorical.values.grouped().forEach((d, idx) => { (d).categories = options.dataViews[0].categorical.categories; @@ -179,6 +184,9 @@ export default class viewModelClass { this.inputSettings.derivedSettingsGrouped[idx], this.inputSettings.validationStatus.messages, first_idx, last_idx); + //let identities: ISelectionId[] = inpData.limitInputArgs.keys.map(keys => { + // return host.createSelectionIdBuilder().withCategory(inpData.categories, keys.id).createSelectionId() + //}); const invalidData: boolean = inpData.validationStatus.status !== 0; const groupStartEndIndexes: number[][] = invalidData ? new Array() : this.getGroupingIndexes(inpData); const limits: controlLimitsObject = invalidData ? null : this.calculateLimits(inpData, groupStartEndIndexes, this.inputSettings); @@ -187,6 +195,7 @@ export default class viewModelClass { if (!invalidData) { this.scaleAndTruncateLimits(limits, this.inputSettings); } + this.identitiesGrouped.push(host.createSelectionIdBuilder().withSeries(options.dataViews[0].categorical.values, d).createSelectionId()); this.groupNames.push(d.name); this.inputDataGrouped.push(inpData); this.groupStartEndIndexesGrouped.push(groupStartEndIndexes); @@ -358,7 +367,10 @@ export default class viewModelClass { } this.plotPointsGrouped.push({ - table_row: table_row + table_row: table_row, + identity: this.identitiesGrouped[i], + aesthetics: this.inputSettings.settingsGrouped[i].scatter, + highlighted: this.inputDataGrouped[i].anyHighlights }) } } @@ -434,6 +446,11 @@ export default class viewModelClass { "ast_colour", this.inputSettings.settings) as string; } + let identity: ISelectionId = host.createSelectionIdBuilder() + .withCategory(this.inputData.categories, + this.inputData.limitInputArgs.keys[i].id) + .createSelectionId(); + const table_row: summaryTableRowData = { date: this.controlLimits.keys[i].label, numerator: this.controlLimits.numerators?.[i], @@ -460,10 +477,7 @@ export default class viewModelClass { value: this.controlLimits.values[i], aesthetics: aesthetics, table_row: table_row, - identity: host.createSelectionIdBuilder() - .withCategory(this.inputData.categories, - this.inputData.limitInputArgs.keys[i].id) - .createSelectionId(), + identity: identity, highlighted: !isNullOrUndefined(this.inputData.highlights?.[index]), tooltip: buildTooltip(table_row, this.inputData?.tooltips?.[index], this.inputSettings.settings, this.inputSettings.derivedSettings) diff --git a/src/D3 Plotting Functions/drawSummaryTable.ts b/src/D3 Plotting Functions/drawSummaryTable.ts index 963ea9b..680291f 100644 --- a/src/D3 Plotting Functions/drawSummaryTable.ts +++ b/src/D3 Plotting Functions/drawSummaryTable.ts @@ -52,18 +52,21 @@ export default function drawSummaryTable(selection: divBaseType, visualObj: Visu .selectAll('tr') .data(plotPoints) .join('tr') - /* .on("click", (event, d: plotData) => { - if (visualObj.host.hostCapabilities.allowInteractions) { + //if (visualObj.host.hostCapabilities.allowInteractions) { visualObj.selectionManager .select(d.identity, event.ctrlKey || event.metaKey) - .then(() => { + .then((d2) => { + console.log("Selection made") + console.log(d2) visualObj.updateHighlighting(); + }, (d2) => { + console.log("Selection failed") + console.log(d2) }); event.stopPropagation(); - } + //} }) - */ .selectAll('td') .data((d) => cols.map(col => { return {column: col.name, value: d.table_row[col.name]} diff --git a/src/Functions/extractInputData.ts b/src/Functions/extractInputData.ts index 037e97e..a0a9373 100644 --- a/src/Functions/extractInputData.ts +++ b/src/Functions/extractInputData.ts @@ -67,7 +67,7 @@ export default function extractInputData(inputView: DataViewCategorical, .map(d => d.show_specification ? d.specification_upper : null); const spcSettings: defaultSettingsType["spc"][] = extractConditionalFormatting(inputView, "spc", inputSettings)?.values const inputValidStatus: ValidationT = validateInputData(keys, numerators, denominators, xbar_sds, groupings, derivedSettings.chart_type_props, first_idx, last_idx); - + console.log(inputValidStatus) if (inputValidStatus.status !== 0) { return invalidInputData(inputValidStatus); } @@ -79,7 +79,7 @@ export default function extractInputData(inputView: DataViewCategorical, const settingsMessages = validationMessages; let valid_x: number = 0; for (let i: number = first_idx; i <= last_idx; i++) { - if (inputValidStatus.messages[i] === "") { + if (inputValidStatus.messages[i - first_idx] === "") { valid_ids.push(i); valid_keys.push({ x: valid_x, id: i, label: keys[i] }) valid_x += 1; @@ -92,7 +92,7 @@ export default function extractInputData(inputView: DataViewCategorical, ); } } else { - removalMessages.push(`${groupVarName} ${keys[i]} removed due to: ${inputValidStatus.messages[i]}.`) + removalMessages.push(`${groupVarName} ${keys[i]} removed due to: ${inputValidStatus.messages[i - first_idx]}.`) } } @@ -121,6 +121,9 @@ export default function extractInputData(inputView: DataViewCategorical, } } + const curr_highlights = extractValues(highlights, valid_ids); + + return { limitInputArgs: { keys: valid_keys, @@ -131,8 +134,8 @@ export default function extractInputData(inputView: DataViewCategorical, }, spcSettings: spcSettings[first_idx], tooltips: extractValues(tooltips, valid_ids), - highlights: extractValues(highlights, valid_ids), - anyHighlights: !isNullOrUndefined(highlights), + highlights: curr_highlights, + anyHighlights: curr_highlights.filter(d => !isNullOrUndefined(d)).length > 0, categories: inputView.categories[0], groupings: valid_groupings, groupingIndexes: groupingIndexes, diff --git a/src/visual.ts b/src/visual.ts index e0c42d7..c1b88ca 100644 --- a/src/visual.ts +++ b/src/visual.ts @@ -62,6 +62,7 @@ export class Visual implements powerbi.extensibility.IVisual { } this.viewModel.update(options, this.host); + console.log(this.viewModel) if (this.viewModel.showGrouped) { if (this.viewModel.inputDataGrouped.map(d => d.validationStatus.status).some(d => d !== 0)) { @@ -99,15 +100,14 @@ export class Visual implements powerbi.extensibility.IVisual { .call(drawIcons, this) .call(addContextMenu, this) .call(drawDownloadButton, this) - } - this.updateHighlighting(); if (this.viewModel.inputData.warningMessage !== "") { this.host.displayWarningIcon("Invalid inputs or settings ignored.\n", this.viewModel.inputData.warningMessage); } } + this.updateHighlighting(); this.host.eventService.renderingFinished(options); } catch (caught_error) { this.tableDiv.style("width", "0%").style("height", "0%"); @@ -133,7 +133,11 @@ export class Visual implements powerbi.extensibility.IVisual { updateHighlighting(): void { const anyHighlights: boolean = this.viewModel.inputData ? this.viewModel.inputData.anyHighlights : false; + const anyHighlightsGrouped: boolean = this.viewModel.inputDataGrouped ? this.viewModel.inputDataGrouped.some(d => d.anyHighlights) : false; const allSelectionIDs: ISelectionId[] = this.selectionManager.getSelectionIds() as ISelectionId[]; + console.log("ids", allSelectionIDs) + console.log("anyHighlights", anyHighlights) + console.log("anyHighlightsGrouped", anyHighlightsGrouped) const opacityFull: number = this.viewModel.inputSettings.settings.scatter.opacity; const opacityReduced: number = this.viewModel.inputSettings.settings.scatter.opacity_unselected; @@ -147,7 +151,7 @@ export class Visual implements powerbi.extensibility.IVisual { dotsSelection.style("fill-opacity", defaultOpacity); tableSelection.style("opacity", defaultOpacity); - if (anyHighlights || (allSelectionIDs.length > 0)) { + if (anyHighlights || (allSelectionIDs.length > 0) || anyHighlightsGrouped) { const dotsNodes = dotsSelection.nodes(); const tableNodes = tableSelection.nodes(); // If either the table or dots haven't been initialised @@ -157,7 +161,8 @@ export class Visual implements powerbi.extensibility.IVisual { for (let i = 0; i < maxNodes; i++) { const currentDotNode = dotsNodes?.[i]; const currentTableNode = tableNodes?.[i]; - const dot: plotData = d3.select(currentDotNode ?? currentTableNode).datum() as plotData; + let dot: plotData = d3.select(currentDotNode ?? currentTableNode).datum() as plotData; + console.log("dot: ", dot) const currentPointSelected: boolean = allSelectionIDs.some((currentSelectionId: ISelectionId) => { return currentSelectionId.includes(dot.identity); }); From 48f918aa0f4214ed3c6433b6558060de49a7941e Mon Sep 17 00:00:00 2001 From: Andrew Johnson Date: Tue, 6 Aug 2024 22:57:11 +0300 Subject: [PATCH 15/18] Add text formatting options --- capabilities.json | 100 ++++++++++++- src/Classes/viewModelClass.ts | 138 ++++++++---------- src/D3 Plotting Functions/drawSummaryTable.ts | 101 +++++++++---- src/Functions/extractInputData.ts | 2 +- src/defaultSettings.ts | 24 ++- src/validSettingValues.ts | 16 ++ src/visual.ts | 11 +- 7 files changed, 277 insertions(+), 115 deletions(-) diff --git a/capabilities.json b/capabilities.json index d068192..55eff6e 100644 --- a/capabilities.json +++ b/capabilities.json @@ -17,7 +17,7 @@ { "displayName": "Outcome/Numerator", "name": "numerators", "kind": "Measure" }, { "displayName": "Denominator", "name": "denominators", "kind": "Measure" }, { "displayName": "ID Key (Date)", "name": "key", "kind": "Grouping" }, - { "displayName": "Indicator", "name": "indicator", "kind": "Grouping" }, + { "displayName": "Grouping (Summary Table)", "name": "indicator", "kind": "Grouping" }, { "displayName": "Rebaseline Groupings", "name": "groupings", "kind": "Measure" }, { "displayName": "SD (for xbar)", "name": "xbar_sds", "kind": "Measure" }, { "displayName": "Tooltips", "name": "tooltips", "kind": "Measure" } @@ -759,6 +759,104 @@ "show_table": { "displayName": "Show Summary Table", "type" : { "bool" : true } + }, + "table_header_font": { + "displayName": "Header Font", + "type": { "formatting": { "fontFamily": true } } + }, + "table_header_size": { + "displayName": "Header Font Size", + "type": { "formatting": { "fontSize": true } } + }, + "table_header_colour":{ + "displayName": "Header Font Colour", + "type": { "fill": { "solid": { "color": true } } } + }, + "table_header_bg_colour":{ + "displayName": "Header Background Colour", + "type": { "fill": { "solid": { "color": true } } } + }, + "table_header_font_weight": { + "displayName": "Header Font Weight", + "type": { + "enumeration" : [ + { "displayName" : "Normal", "value" : "normal" }, + { "displayName" : "Bold", "value" : "bold" } + ] + } + }, + "table_header_text_transform": { + "displayName": "Header Text Transform", + "type": { + "enumeration" : [ + { "displayName" : "Uppercase", "value" : "uppercase" }, + { "displayName" : "Lowercase", "value" : "lowercase" }, + { "displayName" : "Capitalise", "value" : "capitalize" }, + { "displayName" : "None", "value" : "none" } + ] + } + }, + "table_header_text_align": { + "displayName": "Text Alignment", + "type": { "formatting": { "alignment": true } } + }, + "table_body_font": { + "displayName": "Body Font", + "type": { "formatting": { "fontFamily": true } } + }, + "table_body_size": { + "displayName": "Body Font Size", + "type": { "formatting": { "fontSize": true } } + }, + "table_body_colour":{ + "displayName": "Body Font Colour", + "type": { "fill": { "solid": { "color": true } } } + }, + "table_body_bg_colour":{ + "displayName": "Body Background Colour", + "type": { "fill": { "solid": { "color": true } } } + }, + "table_body_font_weight": { + "displayName": "Font Weight", + "type": { + "enumeration" : [ + { "displayName" : "Normal", "value" : "normal" }, + { "displayName" : "Bold", "value" : "bold" } + ] + } + }, + "table_body_text_transform": { + "displayName": "Text Transform", + "type": { + "enumeration" : [ + { "displayName" : "Uppercase", "value" : "uppercase" }, + { "displayName" : "Lowercase", "value" : "lowercase" }, + { "displayName" : "Capitalise", "value" : "capitalize" }, + { "displayName" : "None", "value" : "none" } + ] + } + }, + "table_body_text_align": { + "displayName": "Text Alignment", + "type": { "formatting": { "alignment": true } } + }, + "table_opacity": { + "displayName": "Opacity", + "type": { "numeric": true } + }, + "table_opacity_unselected": { + "displayName": "Opacity if Unselected", + "type": { "numeric": true } + }, + "table_text_overflow": { + "displayName": "Text Overflow Handling", + "type": { + "enumeration" : [ + { "displayName" : "Ellipsis", "value" : "ellipsis" }, + { "displayName" : "Truncate", "value" : "clip" }, + { "displayName" : "None", "value" : "none" } + ] + } } } }, diff --git a/src/Classes/viewModelClass.ts b/src/Classes/viewModelClass.ts index b614038..f123abf 100644 --- a/src/Classes/viewModelClass.ts +++ b/src/Classes/viewModelClass.ts @@ -68,7 +68,7 @@ export type plotData = { export type plotDataGrouped = { table_row: summaryTableRowDataGrouped; identity: ISelectionId; - aesthetics: defaultSettingsType["scatter"]; + aesthetics: defaultSettingsType["summary_table"]; highlighted: boolean; } @@ -166,71 +166,68 @@ export default class viewModelClass { this.svgWidth = options.viewport.width; this.svgHeight = options.viewport.height; - if (options.dataViews[0].categorical.values?.source?.roles?.indicator) { - this.showGrouped = true; - this.groupNames = new Array(); - this.inputDataGrouped = new Array(); - this.groupStartEndIndexesGrouped = new Array(); - this.controlLimitsGrouped = new Array(); - this.outliersGrouped = new Array(); - this.identitiesGrouped = new Array(); - - options.dataViews[0].categorical.values.grouped().forEach((d, idx) => { - (d).categories = options.dataViews[0].categorical.categories; - let first_idx: number = d.values[0].values.findIndex(d_in => !isNullOrUndefined(d_in)); - let last_idx: number = d.values[0].values.map(d_in => !isNullOrUndefined(d_in)).lastIndexOf(true); - const inpData: dataObject = extractInputData(d, - this.inputSettings.settingsGrouped[idx], - this.inputSettings.derivedSettingsGrouped[idx], - this.inputSettings.validationStatus.messages, - first_idx, last_idx); - //let identities: ISelectionId[] = inpData.limitInputArgs.keys.map(keys => { - // return host.createSelectionIdBuilder().withCategory(inpData.categories, keys.id).createSelectionId() - //}); - const invalidData: boolean = inpData.validationStatus.status !== 0; - const groupStartEndIndexes: number[][] = invalidData ? new Array() : this.getGroupingIndexes(inpData); - const limits: controlLimitsObject = invalidData ? null : this.calculateLimits(inpData, groupStartEndIndexes, this.inputSettings); - const outliers: outliersObject = invalidData ? null : this.flagOutliers(limits, groupStartEndIndexes, this.inputSettings); - - if (!invalidData) { - this.scaleAndTruncateLimits(limits, this.inputSettings); - } - this.identitiesGrouped.push(host.createSelectionIdBuilder().withSeries(options.dataViews[0].categorical.values, d).createSelectionId()); - this.groupNames.push(d.name); - this.inputDataGrouped.push(inpData); - this.groupStartEndIndexesGrouped.push(groupStartEndIndexes); - this.controlLimitsGrouped.push(limits); - this.outliersGrouped.push(outliers); - }) - this.initialisePlotDataGrouped(); - } else { - this.showGrouped = false; - this.groupNames = null; - this.inputDataGrouped = null; - this.groupStartEndIndexesGrouped = null; - this.controlLimitsGrouped = null; // Only re-construct data and re-calculate limits if they have changed - //if (options.type === 2 || this.firstRun) { - const split_indexes_str: string = (options.dataViews[0]?.metadata?.objects?.split_indexes_storage?.split_indexes) ?? "[]"; - const split_indexes: number[] = JSON.parse(split_indexes_str); - this.splitIndexes = split_indexes; - this.inputData = extractInputData(options.dataViews[0].categorical, - this.inputSettings.settings, - this.inputSettings.derivedSettings, - this.inputSettings.validationStatus.messages, - 0, options.dataViews[0].categorical.values[0].values.length - 1); - - if (this.inputData.validationStatus.status === 0) { - this.groupStartEndIndexes = this.getGroupingIndexes(this.inputData, this.splitIndexes); - this.controlLimits = this.calculateLimits(this.inputData, this.groupStartEndIndexes, this.inputSettings); - this.scaleAndTruncateLimits(this.controlLimits, this.inputSettings); - this.outliers = this.flagOutliers(this.controlLimits, this.groupStartEndIndexes, this.inputSettings); - - // Structure the data and calculated limits to the format needed for plotting - this.initialisePlotData(host); - this.initialiseGroupedLines(); + if (options.type === 2 || this.firstRun) { + if (options.dataViews[0].categorical.values?.source?.roles?.indicator) { + this.showGrouped = true; + this.groupNames = new Array(); + this.inputDataGrouped = new Array(); + this.groupStartEndIndexesGrouped = new Array(); + this.controlLimitsGrouped = new Array(); + this.outliersGrouped = new Array(); + this.identitiesGrouped = new Array(); + + options.dataViews[0].categorical.values.grouped().forEach((d, idx) => { + (d).categories = options.dataViews[0].categorical.categories; + let first_idx: number = d.values[0].values.findIndex(d_in => !isNullOrUndefined(d_in)); + let last_idx: number = d.values[0].values.map(d_in => !isNullOrUndefined(d_in)).lastIndexOf(true); + const inpData: dataObject = extractInputData(d, + this.inputSettings.settingsGrouped[idx], + this.inputSettings.derivedSettingsGrouped[idx], + this.inputSettings.validationStatus.messages, + first_idx, last_idx); + const invalidData: boolean = inpData.validationStatus.status !== 0; + const groupStartEndIndexes: number[][] = invalidData ? new Array() : this.getGroupingIndexes(inpData); + const limits: controlLimitsObject = invalidData ? null : this.calculateLimits(inpData, groupStartEndIndexes, this.inputSettings); + const outliers: outliersObject = invalidData ? null : this.flagOutliers(limits, groupStartEndIndexes, this.inputSettings); + + if (!invalidData) { + this.scaleAndTruncateLimits(limits, this.inputSettings); + } + this.identitiesGrouped.push(host.createSelectionIdBuilder().withSeries(options.dataViews[0].categorical.values, d).createSelectionId()); + this.groupNames.push(d.name); + this.inputDataGrouped.push(inpData); + this.groupStartEndIndexesGrouped.push(groupStartEndIndexes); + this.controlLimitsGrouped.push(limits); + this.outliersGrouped.push(outliers); + }) + this.initialisePlotDataGrouped(); + } else { + this.showGrouped = false; + this.groupNames = null; + this.inputDataGrouped = null; + this.groupStartEndIndexesGrouped = null; + this.controlLimitsGrouped = null; + const split_indexes_str: string = (options.dataViews[0]?.metadata?.objects?.split_indexes_storage?.split_indexes) ?? "[]"; + const split_indexes: number[] = JSON.parse(split_indexes_str); + this.splitIndexes = split_indexes; + this.inputData = extractInputData(options.dataViews[0].categorical, + this.inputSettings.settings, + this.inputSettings.derivedSettings, + this.inputSettings.validationStatus.messages, + 0, options.dataViews[0].categorical.values[0].values.length - 1); + + if (this.inputData.validationStatus.status === 0) { + this.groupStartEndIndexes = this.getGroupingIndexes(this.inputData, this.splitIndexes); + this.controlLimits = this.calculateLimits(this.inputData, this.groupStartEndIndexes, this.inputSettings); + this.scaleAndTruncateLimits(this.controlLimits, this.inputSettings); + this.outliers = this.flagOutliers(this.controlLimits, this.groupStartEndIndexes, this.inputSettings); + + // Structure the data and calculated limits to the format needed for plotting + this.initialisePlotData(host); + this.initialiseGroupedLines(); + } } - //} this.plotProperties.update( options, this.plotPoints, @@ -302,7 +299,7 @@ export default class viewModelClass { ]; const lineSettings = this.inputSettings.settings.lines; if (lineSettings.show_main) { - this.tableColumnsGrouped.push({ name: "value", label: this.inputSettings.derivedSettings.chart_type_props.value_name }); + this.tableColumnsGrouped.push({ name: "value", label: "Value" }); } if (lineSettings.show_target) { this.tableColumnsGrouped.push({ name: "target", label: lineSettings.ttip_label_target }); @@ -333,16 +330,7 @@ export default class viewModelClass { if (nhsIconSettings.show_assurance_icons) { this.tableColumnsGrouped.push({ name: "assurance", label: "Assurance" }); } - - /* - { name: "upl", label: "UPL" }, - { name: "lpl", label: "LPL" }, - { name: "variation", label: "Variation" }, - { name: "assurance", label: "Assurance" } - ]; - */ for (let i: number = 0; i < this.groupNames.length; i++) { - //const inputData: dataObject = this.inputDataGrouped[i]; const limits: controlLimitsObject = this.controlLimitsGrouped[i]; const outliers: outliersObject = this.outliersGrouped[i]; const lastIndex: number = limits.keys.length - 1; @@ -369,7 +357,7 @@ export default class viewModelClass { this.plotPointsGrouped.push({ table_row: table_row, identity: this.identitiesGrouped[i], - aesthetics: this.inputSettings.settingsGrouped[i].scatter, + aesthetics: this.inputSettings.settingsGrouped[i].summary_table, highlighted: this.inputDataGrouped[i].anyHighlights }) } diff --git a/src/D3 Plotting Functions/drawSummaryTable.ts b/src/D3 Plotting Functions/drawSummaryTable.ts index 680291f..15f8bfa 100644 --- a/src/D3 Plotting Functions/drawSummaryTable.ts +++ b/src/D3 Plotting Functions/drawSummaryTable.ts @@ -13,7 +13,6 @@ export default function drawSummaryTable(selection: divBaseType, visualObj: Visu const table = selection.append("table") .classed("table-group", true) .style("border-collapse", "collapse") - .style("border", "2px black solid") .style("width", "100%") .style("height", "100%"); @@ -33,46 +32,95 @@ export default function drawSummaryTable(selection: divBaseType, visualObj: Visu } let maxWidth: number = visualObj.viewModel.svgWidth / cols.length; + let tableSettings = visualObj.viewModel.inputSettings.settings.summary_table; - selection.select(".table-header") + const tableHeaders = selection.select(".table-header") .selectAll("th") .data(cols) .join("th") - .text((d) => d.label) .style("border", "1px black solid") .style("padding", "5px") - .style("background-color", "lightgray") - .style("font-weight", "bold") - .style("text-transform", "uppercase") - .style("overflow", "hidden") - .style("text-overflow", "ellipsis") - .style("max-width", `${maxWidth}px`); - - const tableSelect = selection.select(".table-body") + .style("background-color", tableSettings.table_header_bg_colour) + .style("font-weight", tableSettings.table_header_font_weight) + .style("text-transform", tableSettings.table_header_text_transform) + .style("text-align", tableSettings.table_header_text_align) + + if (tableSettings.table_text_overflow !== "none") { + tableHeaders.style("overflow", "hidden") + .style("max-width", `${maxWidth}px`) + .style("text-overflow", tableSettings.table_text_overflow); + } else { + tableHeaders.style("overflow", "auto") + .style("max-width", "none") + } + + tableHeaders.selectAll("text") + .data(d => [d.label]) + .join("text") + .text(d => d) + .style("font-size", `${tableSettings.table_header_size}px`) + .style("font-family", tableSettings.table_header_font) + .style("color", tableSettings.table_header_colour) + + const tableRows = selection.select(".table-body") .selectAll('tr') .data(plotPoints) .join('tr') .on("click", (event, d: plotData) => { - //if (visualObj.host.hostCapabilities.allowInteractions) { + if (visualObj.host.hostCapabilities.allowInteractions) { visualObj.selectionManager .select(d.identity, event.ctrlKey || event.metaKey) - .then((d2) => { - console.log("Selection made") - console.log(d2) + .then(() => { visualObj.updateHighlighting(); - }, (d2) => { - console.log("Selection failed") - console.log(d2) }); event.stopPropagation(); - //} + } + }) + .style("background-color", (d) => { + let groupBGColour: string = d.aesthetics?.["table_body_bg_colour"] ?? tableSettings.table_body_bg_colour; + return groupBGColour; + }) + .style("font-weight", (d) => { + let groupWeight: string = d.aesthetics?.["table_body_font_weight"] ?? tableSettings.table_body_font_weight; + return groupWeight; + }) + .style("text-transform", (d) => { + let groupTransform: string = d.aesthetics?.["table_body_text_transform"] ?? tableSettings.table_body_text_transform; + return groupTransform; }) - .selectAll('td') + .style("text-align", (d) => { + let groupAlign: string = d.aesthetics?.["table_body_text_align"] ?? tableSettings.table_body_text_align; + return groupAlign; + }) + .style("font-size", (d) => { + let groupSize: number = d.aesthetics?.["table_body_size"] ?? tableSettings.table_body_size; + return `${groupSize}px`; + }) + .style("font-family", (d) => { + let groupFont: string = d.aesthetics?.["table_body_font"] ?? tableSettings.table_body_font; + return groupFont; + }) + .style("color", (d) => { + let groupColour: string = d.aesthetics?.["table_body_colour"] ?? tableSettings.table_body_colour; + return groupColour; + }); + + if (tableSettings.table_text_overflow !== "none") { + tableRows.style("overflow", "hidden") + .style("max-width", `${maxWidth}px`) + .style("text-overflow", tableSettings.table_text_overflow); + } else { + tableRows.style("overflow", "auto") + .style("max-width", "none") + } + + const tableSelect = tableRows.selectAll('td') .data((d) => cols.map(col => { return {column: col.name, value: d.table_row[col.name]} })) - .join('td') - .on("mouseover", (event) => { + .join('td'); + + tableSelect.on("mouseover", (event) => { d3.select(event.target).select(function(){ return this.closest("td"); }).style("background-color", "lightgray"); @@ -80,17 +128,14 @@ export default function drawSummaryTable(selection: divBaseType, visualObj: Visu .on("mouseout", (event) => { d3.select(event.target).select(function(){ return this.closest("td"); - }).style("background-color", "white"); + }).style("background-color", "inherit"); }) .style("border", "1px black solid") .style("padding", "5px") - .style("font-size", "12px") - .style("overflow", "hidden") - .style("text-overflow", "ellipsis") - .style("max-width", `${maxWidth}px`); const nhsIconSettings = visualObj.viewModel.inputSettings.settings.nhs_icons; const draw_icons: boolean = nhsIconSettings.show_variation_icons || nhsIconSettings.show_assurance_icons; + const showGrouped: boolean = visualObj.viewModel.showGrouped; const thisSelDims = (tableSelect.node() as SVGGElement).getBoundingClientRect() const scaling = visualObj.viewModel.inputSettings.settings.nhs_icons.variation_icons_scaling @@ -98,7 +143,7 @@ export default function drawSummaryTable(selection: divBaseType, visualObj: Visu const icon_y: number = (thisSelDims.height * 0.8) / 0.08 / 2 - 189; tableSelect.each(function(d) { - if (draw_icons && (d.column === "variation" || d.column === "assurance")) { + if (showGrouped && draw_icons && (d.column === "variation" || d.column === "assurance")) { d3.select(this) .append("svg") .attr("width", thisSelDims.width * 0.8) diff --git a/src/Functions/extractInputData.ts b/src/Functions/extractInputData.ts index a0a9373..e412c81 100644 --- a/src/Functions/extractInputData.ts +++ b/src/Functions/extractInputData.ts @@ -67,7 +67,7 @@ export default function extractInputData(inputView: DataViewCategorical, .map(d => d.show_specification ? d.specification_upper : null); const spcSettings: defaultSettingsType["spc"][] = extractConditionalFormatting(inputView, "spc", inputSettings)?.values const inputValidStatus: ValidationT = validateInputData(keys, numerators, denominators, xbar_sds, groupings, derivedSettings.chart_type_props, first_idx, last_idx); - console.log(inputValidStatus) + if (inputValidStatus.status !== 0) { return invalidInputData(inputValidStatus); } diff --git a/src/defaultSettings.ts b/src/defaultSettings.ts index 64a4ba8..46c05e8 100644 --- a/src/defaultSettings.ts +++ b/src/defaultSettings.ts @@ -155,7 +155,24 @@ const defaultSettings = { date_format_locale: { default: "en-GB", valid: ["en-GB", "en-US"]} }, summary_table: { - show_table: { default: false } + show_table: { default: false }, + table_header_font: textOptions.font, + table_header_font_weight: textOptions.weight, + table_header_text_transform: textOptions.text_transform, + table_header_text_align: textOptions.text_align, + table_header_size: textOptions.size, + table_header_colour: colourOptions.standard, + table_header_bg_colour: { default: "#D3D3D3" }, + table_body_font: textOptions.font, + table_body_font_weight: textOptions.weight, + table_body_text_transform: textOptions.text_transform, + table_body_text_align: textOptions.text_align, + table_body_size: textOptions.size, + table_body_colour: colourOptions.standard, + table_body_bg_colour: { default: "#FFFFFF" }, + table_text_overflow: textOptions.text_overflow, + table_opacity: { default: 1, valid: { numberRange: { min: 0, max: 1 } } }, + table_opacity_unselected: { default: 0.2, valid: { numberRange: { min: 0, max: 1 } } } }, download_options: { show_button: { default: false } @@ -197,6 +214,11 @@ export const settingsPaneGroupings = { "Axis": ["ylimit_colour", "limit_multiplier", "ylimit_sig_figs", "ylimit_l", "ylimit_u"], "Ticks": ["ylimit_ticks", "ylimit_tick_count", "ylimit_tick_font", "ylimit_tick_size", "ylimit_tick_colour", "ylimit_tick_rotation"], "Label": ["ylimit_label", "ylimit_label_font", "ylimit_label_size", "ylimit_label_colour"] + }, + summary_table: { + "General": ["show_table", "table_text_overflow", "table_opacity", "table_opacity_unselected"], + "Header": ["table_header_font", "table_header_size", "table_header_text_align", "table_header_font_weight", "table_header_text_transform", "table_header_colour", "table_header_bg_colour"], + "Body": ["table_body_font", "table_body_size", "table_body_text_align", "table_body_font_weight", "table_body_text_transform", "table_body_colour", "table_body_bg_colour"] } } diff --git a/src/validSettingValues.ts b/src/validSettingValues.ts index 36bd86c..9f98894 100644 --- a/src/validSettingValues.ts +++ b/src/validSettingValues.ts @@ -35,6 +35,22 @@ const textOptions = { default: 10, valid: { numberRange: { min: 0, max: 100 } } }, + weight: { + default: "normal", + valid: ["normal", "bold", "bolder", "lighter"] + }, + text_transform: { + default: "uppercase", + valid: ["uppercase", "lowercase", "capitalize", "none"] + }, + text_overflow: { + default: "ellipsis", + valid: ["ellipsis", "clip", "none"] + }, + text_align: { + default: "center", + valid: ["center", "left", "right"] + } } const lineOptions = { diff --git a/src/visual.ts b/src/visual.ts index c1b88ca..e5fa770 100644 --- a/src/visual.ts +++ b/src/visual.ts @@ -23,8 +23,8 @@ export class Visual implements powerbi.extensibility.IVisual { selectionManager: powerbi.extensibility.ISelectionManager; constructor(options: powerbi.extensibility.visual.VisualConstructorOptions) { - this.tableDiv = d3.select(options.element).append("div"); - this.tableDiv.style("overflow", "auto"); + this.tableDiv = d3.select(options.element).append("div") + .style("overflow", "auto"); this.svg = d3.select(options.element).append("svg"); this.host = options.host; @@ -40,8 +40,6 @@ export class Visual implements powerbi.extensibility.IVisual { public update(options: VisualUpdateOptions): void { try { - console.log(options) - console.log(options.dataViews[0].categorical.values.grouped()) this.host.eventService.renderingStarted(options); // Remove printed error if refreshing after a previous error run this.svg.select(".errormessage").remove(); @@ -62,7 +60,6 @@ export class Visual implements powerbi.extensibility.IVisual { } this.viewModel.update(options, this.host); - console.log(this.viewModel) if (this.viewModel.showGrouped) { if (this.viewModel.inputDataGrouped.map(d => d.validationStatus.status).some(d => d !== 0)) { @@ -135,9 +132,6 @@ export class Visual implements powerbi.extensibility.IVisual { const anyHighlights: boolean = this.viewModel.inputData ? this.viewModel.inputData.anyHighlights : false; const anyHighlightsGrouped: boolean = this.viewModel.inputDataGrouped ? this.viewModel.inputDataGrouped.some(d => d.anyHighlights) : false; const allSelectionIDs: ISelectionId[] = this.selectionManager.getSelectionIds() as ISelectionId[]; - console.log("ids", allSelectionIDs) - console.log("anyHighlights", anyHighlights) - console.log("anyHighlightsGrouped", anyHighlightsGrouped) const opacityFull: number = this.viewModel.inputSettings.settings.scatter.opacity; const opacityReduced: number = this.viewModel.inputSettings.settings.scatter.opacity_unselected; @@ -162,7 +156,6 @@ export class Visual implements powerbi.extensibility.IVisual { const currentDotNode = dotsNodes?.[i]; const currentTableNode = tableNodes?.[i]; let dot: plotData = d3.select(currentDotNode ?? currentTableNode).datum() as plotData; - console.log("dot: ", dot) const currentPointSelected: boolean = allSelectionIDs.some((currentSelectionId: ISelectionId) => { return currentSelectionId.includes(dot.identity); }); From 0409b539304f8b59b6879d514ff3965440f79f55 Mon Sep 17 00:00:00 2001 From: Andrew Johnson Date: Tue, 6 Aug 2024 23:01:32 +0300 Subject: [PATCH 16/18] Update menu name --- capabilities.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/capabilities.json b/capabilities.json index 55eff6e..3164935 100644 --- a/capabilities.json +++ b/capabilities.json @@ -117,7 +117,7 @@ } }, "outliers" : { - "displayName": "Outlier Highlighting", + "displayName": "Outlier Detection", "properties": { "process_flag_type": { "displayName": "Type of Change to Flag", From f49c7e593aef9d3432969e9387e92361aa109c21 Mon Sep 17 00:00:00 2001 From: Andrew Johnson Date: Tue, 6 Aug 2024 23:03:24 +0300 Subject: [PATCH 17/18] Reduce padding --- src/D3 Plotting Functions/drawSummaryTable.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/D3 Plotting Functions/drawSummaryTable.ts b/src/D3 Plotting Functions/drawSummaryTable.ts index 15f8bfa..928a678 100644 --- a/src/D3 Plotting Functions/drawSummaryTable.ts +++ b/src/D3 Plotting Functions/drawSummaryTable.ts @@ -39,7 +39,7 @@ export default function drawSummaryTable(selection: divBaseType, visualObj: Visu .data(cols) .join("th") .style("border", "1px black solid") - .style("padding", "5px") + .style("padding", "1px") .style("background-color", tableSettings.table_header_bg_colour) .style("font-weight", tableSettings.table_header_font_weight) .style("text-transform", tableSettings.table_header_text_transform) @@ -131,7 +131,7 @@ export default function drawSummaryTable(selection: divBaseType, visualObj: Visu }).style("background-color", "inherit"); }) .style("border", "1px black solid") - .style("padding", "5px") + .style("padding", "1px") const nhsIconSettings = visualObj.viewModel.inputSettings.settings.nhs_icons; const draw_icons: boolean = nhsIconSettings.show_variation_icons || nhsIconSettings.show_assurance_icons; From 0e8d378c51506cc6ffe308a389b375b9df8b8c79 Mon Sep 17 00:00:00 2001 From: Andrew Johnson Date: Tue, 6 Aug 2024 23:09:39 +0300 Subject: [PATCH 18/18] Fix lints --- src/Classes/viewModelClass.ts | 22 +++++++----------- src/D3 Plotting Functions/drawSummaryTable.ts | 23 +++++++++++-------- src/Functions/validateInputData.ts | 2 +- src/visual.ts | 2 +- 4 files changed, 23 insertions(+), 26 deletions(-) diff --git a/src/Classes/viewModelClass.ts b/src/Classes/viewModelClass.ts index f123abf..a6938cf 100644 --- a/src/Classes/viewModelClass.ts +++ b/src/Classes/viewModelClass.ts @@ -179,8 +179,8 @@ export default class viewModelClass { options.dataViews[0].categorical.values.grouped().forEach((d, idx) => { (d).categories = options.dataViews[0].categorical.categories; - let first_idx: number = d.values[0].values.findIndex(d_in => !isNullOrUndefined(d_in)); - let last_idx: number = d.values[0].values.map(d_in => !isNullOrUndefined(d_in)).lastIndexOf(true); + const first_idx: number = d.values[0].values.findIndex(d_in => !isNullOrUndefined(d_in)); + const last_idx: number = d.values[0].values.map(d_in => !isNullOrUndefined(d_in)).lastIndexOf(true); const inpData: dataObject = extractInputData(d, this.inputSettings.settingsGrouped[idx], this.inputSettings.derivedSettingsGrouped[idx], @@ -392,12 +392,10 @@ export default class viewModelClass { { name: "ul99", label: "UL 99%" }); } if (this.inputSettings.settings.lines.show_95) { - this.tableColumns.push({ name: "ll95", label: "LL 95%" }, - { name: "ul95", label: "UL 95%" }); + this.tableColumns.push({ name: "ll95", label: "LL 95%" }, { name: "ul95", label: "UL 95%" }); } if (this.inputSettings.settings.lines.show_68) { - this.tableColumns.push({ name: "ll68", label: "LL 68%" }, - { name: "ul68", label: "UL 68%" }); + this.tableColumns.push({ name: "ll68", label: "LL 68%" }, { name: "ul68", label: "UL 68%" }); } } @@ -433,12 +431,6 @@ export default class viewModelClass { aesthetics.colour = getAesthetic(this.outliers.astpoint[i], "outliers", "ast_colour", this.inputSettings.settings) as string; } - - let identity: ISelectionId = host.createSelectionIdBuilder() - .withCategory(this.inputData.categories, - this.inputData.limitInputArgs.keys[i].id) - .createSelectionId(); - const table_row: summaryTableRowData = { date: this.controlLimits.keys[i].label, numerator: this.controlLimits.numerators?.[i], @@ -465,7 +457,9 @@ export default class viewModelClass { value: this.controlLimits.values[i], aesthetics: aesthetics, table_row: table_row, - identity: identity, + identity: host.createSelectionIdBuilder() + .withCategory(this.inputData.categories, this.inputData.limitInputArgs.keys[i].id) + .createSelectionId(), highlighted: !isNullOrUndefined(this.inputData.highlights?.[index]), tooltip: buildTooltip(table_row, this.inputData?.tooltips?.[index], this.inputSettings.settings, this.inputSettings.derivedSettings) @@ -583,7 +577,7 @@ export default class viewModelClass { const shift_n: number = inputSettings.settings.outliers.shift_n; const ast_specification: boolean = inputSettings.settings.outliers.astronomical_limit === "Specification"; const two_in_three_specification: boolean = inputSettings.settings.outliers.two_in_three_limit === "Specification"; - let outliers = { + const outliers = { astpoint: rep("none", controlLimits.values.length), two_in_three: rep("none", controlLimits.values.length), trend: rep("none", controlLimits.values.length), diff --git a/src/D3 Plotting Functions/drawSummaryTable.ts b/src/D3 Plotting Functions/drawSummaryTable.ts index 928a678..e756211 100644 --- a/src/D3 Plotting Functions/drawSummaryTable.ts +++ b/src/D3 Plotting Functions/drawSummaryTable.ts @@ -4,6 +4,8 @@ import initialiseIconSVG from "./initialiseIconSVG"; import * as nhsIcons from "./NHS Icons" import * as d3 from "./D3 Modules"; +// ESLint errors due to number of lines in function, but would reduce readability to separate further +/* eslint-disable */ export default function drawSummaryTable(selection: divBaseType, visualObj: Visual) { selection.style("height", "100%").style("width", "100%"); selection.selectAll(".iconrow").remove(); @@ -31,8 +33,8 @@ export default function drawSummaryTable(selection: divBaseType, visualObj: Visu cols = visualObj.viewModel.tableColumns; } - let maxWidth: number = visualObj.viewModel.svgWidth / cols.length; - let tableSettings = visualObj.viewModel.inputSettings.settings.summary_table; + const maxWidth: number = visualObj.viewModel.svgWidth / cols.length; + const tableSettings = visualObj.viewModel.inputSettings.settings.summary_table; const tableHeaders = selection.select(".table-header") .selectAll("th") @@ -77,31 +79,31 @@ export default function drawSummaryTable(selection: divBaseType, visualObj: Visu } }) .style("background-color", (d) => { - let groupBGColour: string = d.aesthetics?.["table_body_bg_colour"] ?? tableSettings.table_body_bg_colour; + const groupBGColour: string = d.aesthetics?.["table_body_bg_colour"] ?? tableSettings.table_body_bg_colour; return groupBGColour; }) .style("font-weight", (d) => { - let groupWeight: string = d.aesthetics?.["table_body_font_weight"] ?? tableSettings.table_body_font_weight; + const groupWeight: string = d.aesthetics?.["table_body_font_weight"] ?? tableSettings.table_body_font_weight; return groupWeight; }) .style("text-transform", (d) => { - let groupTransform: string = d.aesthetics?.["table_body_text_transform"] ?? tableSettings.table_body_text_transform; + const groupTransform: string = d.aesthetics?.["table_body_text_transform"] ?? tableSettings.table_body_text_transform; return groupTransform; }) .style("text-align", (d) => { - let groupAlign: string = d.aesthetics?.["table_body_text_align"] ?? tableSettings.table_body_text_align; + const groupAlign: string = d.aesthetics?.["table_body_text_align"] ?? tableSettings.table_body_text_align; return groupAlign; }) .style("font-size", (d) => { - let groupSize: number = d.aesthetics?.["table_body_size"] ?? tableSettings.table_body_size; + const groupSize: number = d.aesthetics?.["table_body_size"] ?? tableSettings.table_body_size; return `${groupSize}px`; }) .style("font-family", (d) => { - let groupFont: string = d.aesthetics?.["table_body_font"] ?? tableSettings.table_body_font; + const groupFont: string = d.aesthetics?.["table_body_font"] ?? tableSettings.table_body_font; return groupFont; }) .style("color", (d) => { - let groupColour: string = d.aesthetics?.["table_body_colour"] ?? tableSettings.table_body_colour; + const groupColour: string = d.aesthetics?.["table_body_colour"] ?? tableSettings.table_body_colour; return groupColour; }); @@ -156,7 +158,7 @@ export default function drawSummaryTable(selection: divBaseType, visualObj: Visu .attr("transform", `scale(${0.08 * scaling}) translate(${icon_x}, ${icon_y})`) .call(nhsIcons[d.value]); } else { - let value: string = typeof d.value === "number" + const value: string = typeof d.value === "number" ? d.value.toFixed(visualObj.viewModel.inputSettings.settings.spc.sig_figs) : d.value; @@ -169,3 +171,4 @@ export default function drawSummaryTable(selection: divBaseType, visualObj: Visu visualObj.updateHighlighting(); }); } +/* eslint-enable */ diff --git a/src/Functions/validateInputData.ts b/src/Functions/validateInputData.ts index 2ab0f4a..9d5ffd1 100644 --- a/src/Functions/validateInputData.ts +++ b/src/Functions/validateInputData.ts @@ -20,7 +20,7 @@ function validateInputDataImpl(key: string, numerator: number, denominator: numb xbar_sd: number, grouping: string, chart_type_props: derivedSettingsClass["chart_type_props"]): { message: string, type: ValidationFailTypes } { - let rtn = { message: "", type: ValidationFailTypes.Valid }; + const rtn = { message: "", type: ValidationFailTypes.Valid }; if (isNullOrUndefined(grouping)) { //rtn.message = "Grouping missing"; //rtn.type = ValidationFailTypes.GroupingMissing; diff --git a/src/visual.ts b/src/visual.ts index e5fa770..4460d97 100644 --- a/src/visual.ts +++ b/src/visual.ts @@ -155,7 +155,7 @@ export class Visual implements powerbi.extensibility.IVisual { for (let i = 0; i < maxNodes; i++) { const currentDotNode = dotsNodes?.[i]; const currentTableNode = tableNodes?.[i]; - let dot: plotData = d3.select(currentDotNode ?? currentTableNode).datum() as plotData; + const dot: plotData = d3.select(currentDotNode ?? currentTableNode).datum() as plotData; const currentPointSelected: boolean = allSelectionIDs.some((currentSelectionId: ISelectionId) => { return currentSelectionId.includes(dot.identity); });