diff --git a/pbiviz.json b/pbiviz.json index a1bb80f..6bbaac2 100644 --- a/pbiviz.json +++ b/pbiviz.json @@ -4,7 +4,7 @@ "displayName":"SPC Charts", "guid":"PBISPC", "visualClassName":"Visual", - "version":"1.4.4.2", + "version":"1.4.4.3", "description":"A PowerBI custom visual for SPC charts", "supportUrl":"https://github.com/AUS-DOH-Safety-and-Quality/PowerBI-SPC", "gitHubUrl":"https://github.com/AUS-DOH-Safety-and-Quality/PowerBI-SPC" diff --git a/src/Classes/index.ts b/src/Classes/index.ts index 64f2e7b..4f45b64 100644 --- a/src/Classes/index.ts +++ b/src/Classes/index.ts @@ -1,4 +1,4 @@ export { default as plotPropertiesClass, type axisProperties } from "./plotPropertiesClass" export { default as settingsClass, type defaultSettingsType, type defaultSettingsKey, type settingsScalarTypes } from "./settingsClass" -export { default as viewModelClass, type plotData, type lineData, type controlLimitsObject, type controlLimitsArgs, type outliersObject } from "./viewModelClass" +export { default as viewModelClass, type plotData, type lineData, type controlLimitsObject, type controlLimitsArgs, type outliersObject, type viewModelValidationT } from "./viewModelClass" export { default as derivedSettingsClass } from "./derivedSettingsClass" diff --git a/src/Classes/settingsClass.ts b/src/Classes/settingsClass.ts index 40ebb30..0466b78 100644 --- a/src/Classes/settingsClass.ts +++ b/src/Classes/settingsClass.ts @@ -52,7 +52,7 @@ 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.categories.some(d => d.source.roles.indicator); + const is_grouped: boolean = inputView.categorical?.categories?.some(d => d.source.roles.indicator); this.settingsGrouped = new Array(); if (is_grouped) { groupIdxs.forEach(() => { diff --git a/src/Classes/viewModelClass.ts b/src/Classes/viewModelClass.ts index 8c558f4..1f1b372 100644 --- a/src/Classes/viewModelClass.ts +++ b/src/Classes/viewModelClass.ts @@ -6,9 +6,16 @@ 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, type derivedSettingsClass } from "../Classes"; -import { buildTooltip, getAesthetic, checkFlagDirection, truncate, type truncateInputs, multiply, rep, type dataObject, extractInputData, isNullOrUndefined, variationIconsToDraw, assuranceIconToDraw } from "../Functions" +import { buildTooltip, getAesthetic, checkFlagDirection, truncate, type truncateInputs, multiply, rep, type dataObject, extractInputData, isNullOrUndefined, variationIconsToDraw, assuranceIconToDraw, validateDataView } from "../Functions" import { astronomical, trend, twoInThree, shift } from "../Outlier Flagging" +export type viewModelValidationT = { + status: boolean, + error?: string, + warning?: string, + type?: string +} + export type lineData = { x: number; line_value: number; @@ -152,7 +159,65 @@ export default class viewModelClass { this.colourPalette = null; } - update(options: VisualUpdateOptions, host: IVisualHost, groupIdxs: number[][], groupNames: string[]): void { + update(options: VisualUpdateOptions, host: IVisualHost): viewModelValidationT { + const res: viewModelValidationT = { status: true }; + const idx_per_indicator = new Array(); + const indicator_names = new Array(); + const indicator_idx = options.dataViews[0]?.categorical?.categories?.findIndex(d => d.source.roles.indicator); + + if ((indicator_idx === -1)) { + idx_per_indicator.push(options.dataViews[0].categorical.categories[0].values.map((_, i) => i)); + } else if (!isNullOrUndefined(indicator_idx)) { + const indicator_vals = options.dataViews[0].categorical.categories?.[indicator_idx]?.values; + for (let i = 0; i < indicator_vals.length; i++) { + const indicator_name = indicator_vals[i].toString(); + if (indicator_names.includes(indicator_name)) { + idx_per_indicator[indicator_names.indexOf(indicator_name)].push(i); + } else { + indicator_names.push(indicator_name); + idx_per_indicator.push([i]); + } + } + } + + this.inputSettings.update(options.dataViews[0], idx_per_indicator); + if (this.inputSettings.validationStatus.error !== "") { + res.status = false; + res.error = this.inputSettings.validationStatus.error; + res.type = "settings"; + return res; + } + const checkDV: string = validateDataView(options.dataViews, this.inputSettings); + if (checkDV !== "valid") { + res.status = false; + res.error = checkDV; + return res; + } + this.update_impl(options, host, idx_per_indicator, indicator_names); + if (this.showGrouped) { + if (this.inputDataGrouped.map(d => d.validationStatus.status).some(d => d !== 0)) { + res.status = false; + res.error = this.inputDataGrouped.map(d => d.validationStatus.error).join("\n"); + return res; + } + if (this.inputDataGrouped.some(d => d.warningMessage !== "")) { + res.warning = this.inputDataGrouped.map(d => d.warningMessage).join("\n"); + } + } else { + if (this.inputData.validationStatus.status !== 0) { + res.status = false; + res.error = this.inputData.validationStatus.error; + return res; + } + if (this.inputData.warningMessage !== "") { + res.warning = this.inputData.warningMessage; + } + } + + return res; + } + + update_impl(options: VisualUpdateOptions, host: IVisualHost, groupIdxs: number[][], groupNames: string[]): void { this.groupNames = groupNames; if (isNullOrUndefined(this.colourPalette)) { this.colourPalette = { diff --git a/src/D3 Plotting Functions/addContextMenu.ts b/src/D3 Plotting Functions/addContextMenu.ts index 884b332..d264a97 100644 --- a/src/D3 Plotting Functions/addContextMenu.ts +++ b/src/D3 Plotting Functions/addContextMenu.ts @@ -3,7 +3,9 @@ import type { plotData } from "../Classes"; import type { divBaseType, svgBaseType, Visual } from "../visual"; export default function addContextMenu(selection: svgBaseType | divBaseType, visualObj: Visual) { - if (!(visualObj.viewModel.plotProperties.displayPlot)) { + if (!(visualObj.viewModel.plotProperties.displayPlot + || visualObj.viewModel.inputSettings.settings.summary_table.show_table + || visualObj.viewModel.showGrouped)) { selection.on("contextmenu", () => { return; }); return; } diff --git a/src/Functions/extractInputData.ts b/src/Functions/extractInputData.ts index f65dd9f..6c02934 100644 --- a/src/Functions/extractInputData.ts +++ b/src/Functions/extractInputData.ts @@ -66,7 +66,7 @@ export default function extractInputData(inputView: DataViewCategorical, ?.values .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, idxs); + const inputValidStatus: ValidationT = validateInputData(keys, numerators, denominators, xbar_sds, derivedSettings.chart_type_props, idxs); if (inputValidStatus.status !== 0) { return invalidInputData(inputValidStatus); } diff --git a/src/Functions/validateInputData.ts b/src/Functions/validateInputData.ts index 33cc359..8919518 100644 --- a/src/Functions/validateInputData.ts +++ b/src/Functions/validateInputData.ts @@ -16,15 +16,15 @@ const enum ValidationFailTypes { 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 } { +function validateInputDataImpl(key: string, + numerator: number, + denominator: number, + xbar_sd: number, + chart_type_props: derivedSettingsClass["chart_type_props"], + check_denom: boolean): { message: string, type: ValidationFailTypes } { const rtn = { message: "", type: ValidationFailTypes.Valid }; - if (isNullOrUndefined(grouping)) { - //rtn.message = "Grouping missing"; - //rtn.type = ValidationFailTypes.GroupingMissing; - } else if (isNullOrUndefined(key)) { + if (isNullOrUndefined(key)) { rtn.message = "Date missing"; rtn.type = ValidationFailTypes.DateMissing; } else if (isNullOrUndefined(numerator)) { @@ -33,7 +33,7 @@ function validateInputDataImpl(key: string, numerator: number, denominator: numb } 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) { + } else if (check_denom) { if (isNullOrUndefined(denominator)) { rtn.message = "Denominator missing"; rtn.type = ValidationFailTypes.DenominatorMissing; @@ -62,14 +62,15 @@ export default function validateInputData(keys: string[], numerators: number[], denominators: number[], xbar_sds: number[], - groupings: string[], chart_type_props: derivedSettingsClass["chart_type_props"], idxs: number[]): { status: number, messages: string[], error?: string } { let allSameType: boolean = false; let messages: string[] = new Array(); let all_status: ValidationFailTypes[] = new Array(); + const check_denom = chart_type_props.needs_denominator + || (chart_type_props.denominator_optional && !isNullOrUndefined(denominators)); for (const i of idxs) { - const validation = validateInputDataImpl(keys[i], numerators?.[i], denominators?.[i], xbar_sds?.[i], groupings?.[i], chart_type_props); + const validation = validateInputDataImpl(keys[i], numerators?.[i], denominators?.[i], xbar_sds?.[i], chart_type_props, check_denom); messages.push(validation.message); all_status.push(validation.type); } @@ -79,45 +80,45 @@ export default function validateInputData(keys: string[], let commonType = Array.from(allSameTypeSet)[0]; let validationRtn: ValidationT = { - status: (allSameType && commonType === ValidationFailTypes.Valid) ? 0 : 1, + status: (allSameType && commonType !== ValidationFailTypes.Valid) ? 1 : 0, messages: messages }; if (allSameType && commonType !== ValidationFailTypes.Valid) { switch(commonType) { - case 1: { + case ValidationFailTypes.GroupingMissing: { validationRtn.error = "Grouping missing" break; } - case 2: { + case ValidationFailTypes.DateMissing: { validationRtn.error = "All dates/IDs are missing or null!" break; } - case 3: { + case ValidationFailTypes.NumeratorMissing: { validationRtn.error = "All numerators are missing or null!" break; } - case 4: { + case ValidationFailTypes.NumeratorNegative: { validationRtn.error = "All numerators are negative!" break; } - case 5: { + case ValidationFailTypes.DenominatorMissing: { validationRtn.error = "All denominators missing or null!" break; } - case 6: { + case ValidationFailTypes.DenominatorNegative: { validationRtn.error = "All denominators are negative!" break; } - case 7: { + case ValidationFailTypes.DenominatorLessThanNumerator: { validationRtn.error = "All denominators are smaller than numerators!"; break; } - case 8: { + case ValidationFailTypes.SDMissing: { validationRtn.error = "All SDs missing or null!"; break; } - case 9: { + case ValidationFailTypes.SDNegative: { validationRtn.error = "All SDs are negative!"; break; } diff --git a/src/visual.ts b/src/visual.ts index c3eedab..551adb9 100644 --- a/src/visual.ts +++ b/src/visual.ts @@ -9,8 +9,7 @@ import * as d3 from "./D3 Plotting Functions/D3 Modules"; import { drawXAxis, drawYAxis, drawTooltipLine, drawLines, drawDots, drawIcons, addContextMenu, drawErrors, initialiseSVG, drawSummaryTable, drawDownloadButton } from "./D3 Plotting Functions" -import { defaultSettingsKey, viewModelClass, type plotData } from "./Classes" -import { validateDataView } from "./Functions"; +import { defaultSettingsKey, viewModelClass, type plotData, type viewModelValidationT } from "./Classes" export type svgBaseType = d3.Selection; export type divBaseType = d3.Selection; @@ -39,69 +38,39 @@ export class Visual implements powerbi.extensibility.IVisual { } public update(options: VisualUpdateOptions): void { - const idx_per_indicator = new Array(); - const indicator_names = new Array(); - const indicator_idx = options.dataViews[0].categorical.categories?.findIndex(d => d.source.roles.indicator); - - if (indicator_idx === -1) { - idx_per_indicator.push(options.dataViews[0].categorical.categories[0].values.map((_, i) => i)); - } else { - const indicator_vals = options.dataViews[0].categorical.categories?.[indicator_idx]?.values; - for (let i = 0; i < indicator_vals.length; i++) { - const indicator_name = indicator_vals[i].toString(); - if (indicator_names.includes(indicator_name)) { - idx_per_indicator[indicator_names.indexOf(indicator_name)].push(i); - } else { - indicator_names.push(indicator_name); - idx_per_indicator.push([i]); - } - } - } try { this.host.eventService.renderingStarted(options); // Remove printed error if refreshing after a previous error run this.svg.select(".errormessage").remove(); - this.viewModel.inputSettings.update(options.dataViews[0], idx_per_indicator); - if (this.viewModel.inputSettings.validationStatus.error !== "") { - this.processVisualError(options, - this.viewModel.inputSettings.validationStatus.error, - "settings"); - return; - } + // This step handles the updating of both the input data and settings + // If there are any errors or failures, the update exits early sets the + // update status to false + const update_status: viewModelValidationT = this.viewModel.update(options, this.host); - const checkDV: string = validateDataView(options.dataViews, - this.viewModel.inputSettings); - if (checkDV !== "valid") { - this.processVisualError(options, checkDV); - return; - } - - this.viewModel.update(options, this.host, idx_per_indicator, indicator_names); - 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; + if (!update_status.status) { + this.tableDiv.style("width", "0%").style("height", "0%"); + this.svg.attr("width", options.viewport.width) + .attr("height", options.viewport.height) + if (this.viewModel?.inputSettings?.settings?.canvas?.show_errors ?? true) { + this.svg.call(drawErrors, options, update_status?.error, update_status?.type); + } else { + this.svg.call(initialiseSVG, true); } - 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.host.eventService.renderingFailed(options); return; } - if (this.viewModel.inputSettings.settings.summary_table.show_table) { + if (update_status.warning) { + this.host.displayWarningIcon("Invalid inputs or settings ignored.\n", + update_status.warning); + } + + if (this.viewModel.showGrouped || this.viewModel.inputSettings.settings.summary_table.show_table) { this.svg.attr("width", 0).attr("height", 0); this.tableDiv.call(drawSummaryTable, this) - .call(addContextMenu, this); + .call(addContextMenu, this); } else { this.tableDiv.style("width", "0%").style("height", "0%"); this.svg.attr("width", options.viewport.width) @@ -116,11 +85,6 @@ export class Visual implements powerbi.extensibility.IVisual { .call(drawDownloadButton, this) } - 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) { @@ -133,18 +97,6 @@ export class Visual implements powerbi.extensibility.IVisual { } } - processVisualError(options: VisualUpdateOptions, message: string, type: string = null): void { - this.tableDiv.style("width", "0%").style("height", "0%"); - this.svg.attr("width", options.viewport.width) - .attr("height", options.viewport.height) - if (this.viewModel.inputSettings.settings.canvas.show_errors) { - this.svg.call(drawErrors, options, message, type); - } else { - this.svg.call(initialiseSVG, true); - } - this.host.eventService.renderingFinished(options); - } - 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;