From 7ccc8e42150ea8e744227d86f9f796d1e530f3d4 Mon Sep 17 00:00:00 2001 From: Andrew Johnson Date: Fri, 28 Jul 2023 10:20:09 +0300 Subject: [PATCH 01/10] viewModel stricter null checking --- package.json | 13 +++--- src/Classes/controlLimitsClass.ts | 50 +++++++++++----------- src/Classes/dataClass.ts | 68 +++++++++++++++--------------- src/Classes/plotPropertiesClass.ts | 5 ++- src/Classes/viewModelClass.ts | 49 ++++++++++----------- 5 files changed, 92 insertions(+), 93 deletions(-) diff --git a/package.json b/package.json index b00f0ce..aedd772 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,7 @@ "d3-selection": "^3.0.0", "d3-shape": "^3.2.0", "powerbi-visuals-api": "~3.6.0", - "powerbi-visuals-utils-dataviewutils": "^2.2.1" + "powerbi-visuals-utils-dataviewutils": "^6.0.1" }, "devDependencies": { "@types/d3-array": "^3.0.5", @@ -22,13 +22,12 @@ "@types/d3-scale": "^4.0.3", "@types/d3-selection": "^3.0.5", "@types/d3-shape": "^3.1.1", - "@typescript-eslint/eslint-plugin": "^5.59.8", - "@typescript-eslint/parser": "^5.59.8", - "eslint": "^8.41.0", + "@typescript-eslint/eslint-plugin": "^6.2.0", + "@typescript-eslint/parser": "^6.2.0", + "eslint": "^8.45.0", "eslint-plugin-powerbi-visuals": "^0.8.1", - "process": "^0.11.10", - "ts-loader": "9.4.2", - "typescript": "3.6.3" + "ts-loader": "9.4.4", + "typescript": "5.1.6" }, "override": { "powerbi-visuals-api": "$powerbi-visuals-api" diff --git a/src/Classes/controlLimitsClass.ts b/src/Classes/controlLimitsClass.ts index a040ad0..ed48fea 100644 --- a/src/Classes/controlLimitsClass.ts +++ b/src/Classes/controlLimitsClass.ts @@ -86,30 +86,32 @@ export default class controlLimitsClass { } } - constructor(args: controlLimitsArgs) { - this.keys = args.keys; - this.values = args.values; - if (args.numerators || !(args.numerators === null || args.numerators === undefined)) { - this.numerators = args.numerators; - } - if (args.denominators || !(args.denominators === null || args.denominators === undefined)) { - this.denominators = args.denominators; - } - this.targets = args.targets; - this.alt_targets = rep(args.inputSettings.spc.alt_target, args.values.length) - this.ll99 = args.ll99; - this.ll95 = args.ll95; - this.ul95 = args.ul95; - this.ul99 = args.ul99; - this.astpoint = rep("none", args.values.length); - this.trend = rep("none", args.values.length); - this.two_in_three = rep("none", args.values.length); - this.shift = rep("none", args.values.length); - if (args.count || !(args.count === null || args.count === undefined)) { - this.count = args.count; - } + constructor(args?: controlLimitsArgs) { + if (args) { + this.keys = args.keys; + this.values = args.values; + if (args.numerators || !(args.numerators === null || args.numerators === undefined)) { + this.numerators = args.numerators; + } + if (args.denominators || !(args.denominators === null || args.denominators === undefined)) { + this.denominators = args.denominators; + } + this.targets = args.targets; + this.alt_targets = rep(args.inputSettings.spc.alt_target, args.values.length) + this.ll99 = args.ll99; + this.ll95 = args.ll95; + this.ul95 = args.ul95; + this.ul99 = args.ul99; + this.astpoint = rep("none", args.values.length); + this.trend = rep("none", args.values.length); + this.two_in_three = rep("none", args.values.length); + this.shift = rep("none", args.values.length); + if (args.count || !(args.count === null || args.count === undefined)) { + this.count = args.count; + } - this.scaleAndTruncateLimits(args.inputSettings) - this.flagOutliers(args.inputSettings) + this.scaleAndTruncateLimits(args.inputSettings) + this.flagOutliers(args.inputSettings) + } } } diff --git a/src/Classes/dataClass.ts b/src/Classes/dataClass.ts index 1cd7ae1..5de5784 100644 --- a/src/Classes/dataClass.ts +++ b/src/Classes/dataClass.ts @@ -22,44 +22,46 @@ export default class dataClass { scatter_formatting: defaultSettingsType["scatter"][]; tooltips: VisualTooltipDataItem[][]; - constructor(inputView: DataViewCategorical, inputSettings: settingsClass) { - const numerators: number[] = extractDataColumn(inputView, "numerators", inputSettings); - const denominators: number[] = extractDataColumn(inputView, "denominators", inputSettings); - const xbar_sds: number[] = extractDataColumn(inputView, "xbar_sds", inputSettings); - const keys: string[] = extractDataColumn(inputView, "key", inputSettings); - const scatter_cond = extractConditionalFormatting(inputView, "scatter", inputSettings) as defaultSettingsType["scatter"][]; - const tooltips = extractDataColumn(inputView, "tooltips", inputSettings); + constructor(inputView?: DataViewCategorical, inputSettings?: settingsClass) { + if (inputView && inputSettings) { + const numerators: number[] = extractDataColumn(inputView, "numerators", inputSettings); + const denominators: number[] = extractDataColumn(inputView, "denominators", inputSettings); + const xbar_sds: number[] = extractDataColumn(inputView, "xbar_sds", inputSettings); + const keys: string[] = extractDataColumn(inputView, "key", inputSettings); + const scatter_cond = extractConditionalFormatting(inputView, "scatter", inputSettings) as defaultSettingsType["scatter"][]; + const tooltips = extractDataColumn(inputView, "tooltips", inputSettings); - const valid_ids: number[] = new Array(); - const valid_keys: { x: number, id: number, label: string }[] = new Array<{ x: number, id: number, label: string }>(); + const valid_ids: number[] = new Array(); + const valid_keys: { x: number, id: number, label: string }[] = new Array<{ x: number, id: number, label: string }>(); - for (let i: number = 0; i < numerators.length; i++) { - if (checkValidInput(numerators[i], - denominators ? denominators[i] : null, - xbar_sds ? xbar_sds[i] : null, inputSettings.spc.chart_type)) { - valid_ids.push(i); - valid_keys.push({ x: null, id: i, label: keys[i] }) + for (let i: number = 0; i < numerators.length; i++) { + if (checkValidInput(numerators[i], + denominators ? denominators[i] : null, + xbar_sds ? xbar_sds[i] : null, inputSettings.spc.chart_type)) { + valid_ids.push(i); + valid_keys.push({ x: null, id: i, label: keys[i] }) + } } - } - valid_keys.forEach((d, idx) => { d.x = idx }); + valid_keys.forEach((d, idx) => { d.x = idx }); - let percent_labels: boolean; - if (inputSettings.spc.perc_labels === "Automatic") { - percent_labels = ["p", "pp"].includes(inputSettings.spc.chart_type) && (inputSettings.spc.multiplier === 1 || inputSettings.spc.multiplier === 100); - } else { - percent_labels = inputSettings.spc.perc_labels === "Yes"; - } + let percent_labels: boolean; + if (inputSettings.spc.perc_labels === "Automatic") { + percent_labels = ["p", "pp"].includes(inputSettings.spc.chart_type) && (inputSettings.spc.multiplier === 1 || inputSettings.spc.multiplier === 100); + } else { + percent_labels = inputSettings.spc.perc_labels === "Yes"; + } - this.keys = valid_keys; - this.numerators = extractValues(numerators, valid_ids); - this.denominators = extractValues(denominators, valid_ids); - this.xbar_sds = extractValues(xbar_sds, valid_ids); - this.tooltips = extractValues(tooltips, valid_ids); - this.highlights = inputView.values[0].highlights ? extractValues(inputView.values[0].highlights, valid_ids) : inputView.values[0].highlights; - this.anyHighlights = this.highlights ? true : false - this.categories = inputView.categories[0]; - this.percentLabels = percent_labels; - this.scatter_formatting = extractValues(scatter_cond, valid_ids); + this.keys = valid_keys; + this.numerators = extractValues(numerators, valid_ids); + this.denominators = extractValues(denominators, valid_ids); + this.xbar_sds = extractValues(xbar_sds, valid_ids); + this.tooltips = extractValues(tooltips, valid_ids); + this.highlights = inputView.values[0].highlights ? extractValues(inputView.values[0].highlights, valid_ids) : inputView.values[0].highlights; + this.anyHighlights = this.highlights ? true : false + this.categories = inputView.categories[0]; + this.percentLabels = percent_labels; + this.scatter_formatting = extractValues(scatter_cond, valid_ids); + } } } diff --git a/src/Classes/plotPropertiesClass.ts b/src/Classes/plotPropertiesClass.ts index 0409205..85805c5 100644 --- a/src/Classes/plotPropertiesClass.ts +++ b/src/Classes/plotPropertiesClass.ts @@ -52,7 +52,8 @@ export default class plotPropertiesClass { plotPoints: plotData[], controlLimits: controlLimitsClass, inputData: dataClass, - inputSettings: settingsClass }): void { + inputSettings: settingsClass, + invalidDataView: boolean }): void { // Get the width and height of plotting space this.width = args.options.viewport.width; @@ -68,7 +69,7 @@ export default class plotPropertiesClass { let yUpperLimit: number = args.inputSettings.y_axis.ylimit_u; // Only update data-/settings-dependent plot aesthetics if they have changed - if (args.inputData && args.controlLimits) { + if (!args.invalidDataView) { xUpperLimit = xUpperLimit !== null ? xUpperLimit : d3.max(args.controlLimits.keys.map(d => d.x)) const limitMultiplier: number = args.inputSettings.y_axis.limit_multiplier; diff --git a/src/Classes/viewModelClass.ts b/src/Classes/viewModelClass.ts index 4ea024f..7615692 100644 --- a/src/Classes/viewModelClass.ts +++ b/src/Classes/viewModelClass.ts @@ -18,7 +18,7 @@ import * as limitFunctions from "../Limit Calculations" export class lineData { x: number; - line_value: number; + line_value: number | null; group: string; } @@ -46,23 +46,33 @@ export default class viewModelClass { firstRun: boolean; limitFunction: (inputData: dataClass, inputSettings: settingsClass) => controlLimitsClass; + constructor() { + this.inputData = new dataClass(); + this.inputSettings = new settingsClass(); + this.limitFunction = null; + this.controlLimits = null; + this.plotPoints = new Array(); + this.groupedLines = new Array<[string, lineData[]]>(); + this.plotProperties = new plotPropertiesClass(); + this.plotProperties.firstRun = true; + this.firstRun = true + this.splitIndexes = new Array(); + } + update(options: VisualUpdateOptions, host: IVisualHost) { - if (this.firstRun) { - this.inputSettings = new settingsClass(); - } const dv: powerbi.DataView[] = options.dataViews; this.inputSettings.update(dv[0]); - const split_indexes_storage: DataViewObject = dv[0].metadata.objects ? dv[0].metadata.objects.split_indexes_storage : null; - const split_indexes: DataViewPropertyValue = split_indexes_storage ? split_indexes_storage.split_indexes : null; + const split_indexes_storage: DataViewObject | null = dv[0].metadata.objects ? dv[0].metadata.objects.split_indexes_storage : null; + const split_indexes: DataViewPropertyValue | null = split_indexes_storage ? split_indexes_storage.split_indexes : null; this.splitIndexes = split_indexes ? JSON.parse((split_indexes)) : new Array(); + let invalidDataView: boolean = checkInvalidDataView(dv); // Make sure that the construction returns early with null members so // that the visual does not crash when trying to process invalid data - if (checkInvalidDataView(dv)) { - this.inputData = null; - this.limitFunction = null; - this.controlLimits = null; + if (invalidDataView) { + this.inputData = new dataClass(); + this.controlLimits = new controlLimitsClass(); this.plotPoints = new Array(); this.groupedLines = new Array<[string, lineData[]]>(); this.splitIndexes = new Array(); @@ -85,16 +95,13 @@ export default class viewModelClass { this.initialiseGroupedLines(); } } - if (this.firstRun) { - this.plotProperties = new plotPropertiesClass(); - this.plotProperties.firstRun = true; - } this.plotProperties.update({ options: options, plotPoints: this.plotPoints, controlLimits: this.controlLimits, inputData: this.inputData, - inputSettings: this.inputSettings + inputSettings: this.inputSettings, + invalidDataView: invalidDataView }) this.firstRun = false; } @@ -200,16 +207,4 @@ export default class viewModelClass { } this.groupedLines = d3.groups(formattedLines, d => d.group); } - - constructor() { - this.inputData = null; - this.inputSettings = null; - this.limitFunction = null; - this.controlLimits = null; - this.plotPoints = new Array(); - this.groupedLines = new Array<[string, lineData[]]>(); - this.plotProperties = null; - this.firstRun = true - this.splitIndexes = new Array(); - } } From b0726a648b08d7979d05b66c6b8cc2bc5145f839 Mon Sep 17 00:00:00 2001 From: Andrew Johnson Date: Fri, 28 Jul 2023 11:03:50 +0300 Subject: [PATCH 02/10] plotProperties null robust --- src/Classes/controlLimitsClass.ts | 2 +- src/Classes/dataClass.ts | 14 ++++++++------ src/Classes/plotPropertiesClass.ts | 19 +++++++++++++------ src/Classes/viewModelClass.ts | 4 +--- src/D3 Plotting Functions/D3 Modules/index.ts | 12 +++++++++++- src/Functions/checkValidInput.ts | 4 ++-- src/Functions/isNullOrUndefined.ts | 3 +++ tsconfig.json | 1 + 8 files changed, 40 insertions(+), 19 deletions(-) create mode 100644 src/Functions/isNullOrUndefined.ts diff --git a/src/Classes/controlLimitsClass.ts b/src/Classes/controlLimitsClass.ts index ed48fea..971b2a9 100644 --- a/src/Classes/controlLimitsClass.ts +++ b/src/Classes/controlLimitsClass.ts @@ -30,7 +30,7 @@ export default class controlLimitsClass { numerators?: number[]; denominators?: number[]; targets: number[]; - alt_targets?: number[]; + alt_targets: number[]; ll99: number[]; ll95: number[]; ul95: number[]; diff --git a/src/Classes/dataClass.ts b/src/Classes/dataClass.ts index 5de5784..bd055a9 100644 --- a/src/Classes/dataClass.ts +++ b/src/Classes/dataClass.ts @@ -34,17 +34,17 @@ export default class dataClass { const valid_ids: number[] = new Array(); const valid_keys: { x: number, id: number, label: string }[] = new Array<{ x: number, id: number, label: string }>(); + let validCount: number = 0 for (let i: number = 0; i < numerators.length; i++) { if (checkValidInput(numerators[i], denominators ? denominators[i] : null, xbar_sds ? xbar_sds[i] : null, inputSettings.spc.chart_type)) { valid_ids.push(i); - valid_keys.push({ x: null, id: i, label: keys[i] }) + valid_keys.push({ x: validCount, id: i, label: keys[i] }) + validCount += 1; } } - valid_keys.forEach((d, idx) => { d.x = idx }); - let percent_labels: boolean; if (inputSettings.spc.perc_labels === "Automatic") { percent_labels = ["p", "pp"].includes(inputSettings.spc.chart_type) && (inputSettings.spc.multiplier === 1 || inputSettings.spc.multiplier === 100); @@ -52,14 +52,16 @@ export default class dataClass { percent_labels = inputSettings.spc.perc_labels === "Yes"; } + const inputValues = (inputView.values as powerbi.DataViewValueColumns)[0]; + this.keys = valid_keys; this.numerators = extractValues(numerators, valid_ids); this.denominators = extractValues(denominators, valid_ids); this.xbar_sds = extractValues(xbar_sds, valid_ids); this.tooltips = extractValues(tooltips, valid_ids); - this.highlights = inputView.values[0].highlights ? extractValues(inputView.values[0].highlights, valid_ids) : inputView.values[0].highlights; - this.anyHighlights = this.highlights ? true : false - this.categories = inputView.categories[0]; + this.highlights = inputValues.highlights ? extractValues(inputValues.highlights, valid_ids) : new Array; + this.anyHighlights = (this.highlights.length > 0) + this.categories = (inputView.categories as powerbi.DataViewCategoryColumn[])[0]; this.percentLabels = percent_labels; this.scatter_formatting = extractValues(scatter_cond, valid_ids); } diff --git a/src/Classes/plotPropertiesClass.ts b/src/Classes/plotPropertiesClass.ts index 85805c5..daacef7 100644 --- a/src/Classes/plotPropertiesClass.ts +++ b/src/Classes/plotPropertiesClass.ts @@ -6,6 +6,7 @@ import { plotData } from "./viewModelClass" import settingsClass from "./settingsClass"; import dataClass from "./dataClass"; import controlLimitsClass from "./controlLimitsClass"; +import isNullOrUndefined from "../Functions/isNullOrUndefined"; export type axisProperties = { lower: number, @@ -61,7 +62,7 @@ export default class plotPropertiesClass { this.displayPlot = args.plotPoints ? args.plotPoints.length > 1 - : null; + : false; const xLowerLimit: number = args.inputSettings.x_axis.xlimit_l; let xUpperLimit: number = args.inputSettings.x_axis.xlimit_u; @@ -70,7 +71,9 @@ export default class plotPropertiesClass { // Only update data-/settings-dependent plot aesthetics if they have changed if (!args.invalidDataView) { - xUpperLimit = xUpperLimit !== null ? xUpperLimit : d3.max(args.controlLimits.keys.map(d => d.x)) + xUpperLimit = isNullOrUndefined(xUpperLimit) + ? d3.max(args.controlLimits.keys.map(d => d.x)) + : xUpperLimit; const limitMultiplier: number = args.inputSettings.y_axis.limit_multiplier; const chart_type: string = args.inputSettings.spc.chart_type; @@ -78,10 +81,14 @@ export default class plotPropertiesClass { const ul99: number[] = args.controlLimits.ul99; const ll99: number[] = args.controlLimits.ll99; const alt_targets: number[] = args.controlLimits.alt_targets; - const maxValueOrLimit: number = d3.max(values.concat(ul99).concat(alt_targets)); - const minValueOrLimit: number = d3.min(values.concat(ll99).concat(alt_targets)); - const maxTarget: number = d3.max(args.controlLimits.targets); - const minTarget: number = d3.min(args.controlLimits.targets); + const maxValueOrLimit: number = d3.max((values.concat(ul99).concat(alt_targets))); + const minValueOrLimit: number = d3.min((values.concat(ll99).concat(alt_targets))); + const maxTarget: number = d3.max((args.controlLimits.targets)); + const minTarget: number = d3.min((args.controlLimits.targets)); + + console.log((values.concat(ll99).concat(alt_targets))) + console.log([minValueOrLimit, maxValueOrLimit]) + console.log([minTarget, maxTarget]) const upperLimitRaw: number = maxTarget + (maxValueOrLimit - maxTarget) * limitMultiplier; const lowerLimitRaw: number = minTarget - (minTarget - minValueOrLimit) * limitMultiplier; diff --git a/src/Classes/viewModelClass.ts b/src/Classes/viewModelClass.ts index 7615692..c72060d 100644 --- a/src/Classes/viewModelClass.ts +++ b/src/Classes/viewModelClass.ts @@ -49,8 +49,6 @@ export default class viewModelClass { constructor() { this.inputData = new dataClass(); this.inputSettings = new settingsClass(); - this.limitFunction = null; - this.controlLimits = null; this.plotPoints = new Array(); this.groupedLines = new Array<[string, lineData[]]>(); this.plotProperties = new plotPropertiesClass(); @@ -174,7 +172,7 @@ export default class viewModelClass { .withCategory(this.inputData.categories, this.inputData.keys[i].id) .createSelectionId(), - highlighted: this.inputData.highlights ? (this.inputData.highlights[index] ? true : false) : false, + highlighted: this.inputData.anyHighlights ? (this.inputData.highlights[index] ? true : false) : false, tooltip: buildTooltip(i, this.controlLimits, this.inputData, this.inputSettings) }) this.tickLabels.push({x: index, label: this.controlLimits.keys[i].label}); diff --git a/src/D3 Plotting Functions/D3 Modules/index.ts b/src/D3 Plotting Functions/D3 Modules/index.ts index 047f8bf..97f4e14 100644 --- a/src/D3 Plotting Functions/D3 Modules/index.ts +++ b/src/D3 Plotting Functions/D3 Modules/index.ts @@ -1,5 +1,15 @@ +import { min as d3min, max as d3max } from "d3-array"; + +/** + * D3 Array reduction functions have return type number | undefined, + * redefine these to remove the 'undefined' as that is handled separately + */ +const min = (x: number[]): number => d3min(x) as number; +const max = (x: number[]): number => d3max(x) as number; + export { select, selectAll, Selection, BaseType } from "d3-selection"; -export { groups, leastIndex, mean, median, sum, max, min } from "d3-array"; +export { groups, leastIndex, mean, median, sum } from "d3-array"; +export { min, max }; export { line } from "d3-shape" export { Axis, axisBottom, axisLeft } from "d3-axis" export { ScaleLinear, scaleLinear, NumberValue } from "d3-scale" diff --git a/src/Functions/checkValidInput.ts b/src/Functions/checkValidInput.ts index 6499f65..3141333 100644 --- a/src/Functions/checkValidInput.ts +++ b/src/Functions/checkValidInput.ts @@ -1,4 +1,4 @@ -export default function checkValidInput(numerator: number, denominator: number, xbar_sd: number, data_type: string): boolean { +export default function checkValidInput(numerator: number, denominator: number | null, xbar_sd: number | null, data_type: string): boolean { const denominatorConstraintRequired: string[] = ["p", "pprime", "u", "uprime"]; const denominatorRequired: string[] = ["p", "pprime", "u", "uprime", "xbar", "s"]; const denominatorConstraintForRunIChart: string[] = ["i", "run"]; @@ -7,7 +7,7 @@ export default function checkValidInput(numerator: number, denominator: number, ? denominator !== null && denominator !== undefined && denominator > 0 : true; const proportionDenominatorValid: boolean = denominatorConstraintRequired.includes(data_type) - ? (numerator <= denominator) + ? (numerator <= (denominator as number)) : true; const runIChartDenominatorValid: boolean = (denominatorConstraintForRunIChart.includes(data_type) && !(denominator === null)) diff --git a/src/Functions/isNullOrUndefined.ts b/src/Functions/isNullOrUndefined.ts new file mode 100644 index 0000000..60ccd1a --- /dev/null +++ b/src/Functions/isNullOrUndefined.ts @@ -0,0 +1,3 @@ +export default function isNullOrUndefined(x: T): boolean { + return (x === null) || (x === undefined); +} diff --git a/tsconfig.json b/tsconfig.json index b68a445..91c2119 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -13,6 +13,7 @@ "emitDecoratorMetadata": true, "experimentalDecorators": true, "allowSyntheticDefaultImports": true, + "strictNullChecks": true, "target": "es6", "sourceMap": true, "outDir": "./.tmp/build/", From e0caf72bd5a83df11651034a580ee7623fe8c4e4 Mon Sep 17 00:00:00 2001 From: Andrew Johnson Date: Fri, 28 Jul 2023 11:13:31 +0300 Subject: [PATCH 03/10] settingsClass null robust --- src/Classes/plotPropertiesClass.ts | 12 ++++-------- src/Classes/settingsClass.ts | 11 ++++++++--- src/Functions/extractConditionalFormatting.ts | 6 ------ src/Functions/isNotNullOrUndefined.ts | 3 +++ src/Functions/isNullOrUndefined.ts | 3 --- tsconfig.json | 1 - 6 files changed, 15 insertions(+), 21 deletions(-) create mode 100644 src/Functions/isNotNullOrUndefined.ts delete mode 100644 src/Functions/isNullOrUndefined.ts diff --git a/src/Classes/plotPropertiesClass.ts b/src/Classes/plotPropertiesClass.ts index daacef7..2b7bacb 100644 --- a/src/Classes/plotPropertiesClass.ts +++ b/src/Classes/plotPropertiesClass.ts @@ -6,7 +6,7 @@ import { plotData } from "./viewModelClass" import settingsClass from "./settingsClass"; import dataClass from "./dataClass"; import controlLimitsClass from "./controlLimitsClass"; -import isNullOrUndefined from "../Functions/isNullOrUndefined"; +import isNotNullOrUndefined from "../Functions/isNotNullOrUndefined"; export type axisProperties = { lower: number, @@ -71,9 +71,9 @@ export default class plotPropertiesClass { // Only update data-/settings-dependent plot aesthetics if they have changed if (!args.invalidDataView) { - xUpperLimit = isNullOrUndefined(xUpperLimit) - ? d3.max(args.controlLimits.keys.map(d => d.x)) - : xUpperLimit; + xUpperLimit = isNotNullOrUndefined(xUpperLimit) + ? xUpperLimit + : d3.max(args.controlLimits.keys.map(d => d.x)); const limitMultiplier: number = args.inputSettings.y_axis.limit_multiplier; const chart_type: string = args.inputSettings.spc.chart_type; @@ -86,10 +86,6 @@ export default class plotPropertiesClass { const maxTarget: number = d3.max((args.controlLimits.targets)); const minTarget: number = d3.min((args.controlLimits.targets)); - console.log((values.concat(ll99).concat(alt_targets))) - console.log([minValueOrLimit, maxValueOrLimit]) - console.log([minTarget, maxTarget]) - const upperLimitRaw: number = maxTarget + (maxValueOrLimit - maxTarget) * limitMultiplier; const lowerLimitRaw: number = minTarget - (minTarget - minValueOrLimit) * limitMultiplier; const multiplier: number = args.inputSettings.spc.multiplier; diff --git a/src/Classes/settingsClass.ts b/src/Classes/settingsClass.ts index 625ea6a..0ed8c7b 100644 --- a/src/Classes/settingsClass.ts +++ b/src/Classes/settingsClass.ts @@ -10,6 +10,7 @@ import { dataViewWildcard } from "powerbi-visuals-utils-dataviewutils"; import extractConditionalFormatting from "../Functions/extractConditionalFormatting"; import defaultSettings from "../defaultSettings" import { defaultSettingsType, defaultSettingsKey, settingsPaneGroupings } from "../defaultSettings"; +import isNotNullOrUndefined from "../Functions/isNotNullOrUndefined"; /** * This is the core class which controls the initialisation and @@ -40,13 +41,17 @@ export default class settingsClass implements defaultSettingsType { const allSettingGroups: string[] = Object.getOwnPropertyNames(this); allSettingGroups.forEach(settingGroup => { - const categoricalView: DataViewCategorical = inputView.categorical ? inputView.categorical : null; - const condFormatting: defaultSettingsType[defaultSettingsKey] = extractConditionalFormatting(categoricalView, settingGroup, this)[0]; + let condFormatting: defaultSettingsType[defaultSettingsKey] | undefined; + if (isNotNullOrUndefined(inputView.categorical)) { + condFormatting = extractConditionalFormatting(inputView.categorical as DataViewCategorical, settingGroup, this)[0]; + } // Get the names of all settings in a given class and // use those to extract and update the relevant values const settingNames: string[] = Object.getOwnPropertyNames(this[settingGroup]); settingNames.forEach(settingName => { - this[settingGroup][settingName] = condFormatting ? condFormatting[settingName] : defaultSettings[settingGroup][settingName] + this[settingGroup][settingName] = isNotNullOrUndefined(condFormatting) + ? (condFormatting as defaultSettingsType[defaultSettingsKey])[settingName] + : defaultSettings[settingGroup][settingName] }) }) } diff --git a/src/Functions/extractConditionalFormatting.ts b/src/Functions/extractConditionalFormatting.ts index 3fbf3d7..daa37e8 100644 --- a/src/Functions/extractConditionalFormatting.ts +++ b/src/Functions/extractConditionalFormatting.ts @@ -8,12 +8,6 @@ import defaultSettings, { defaultSettingsType, defaultSettingsKey } from "../def type SettingsTypes = defaultSettingsType[defaultSettingsKey]; export default function extractConditionalFormatting(categoricalView: DataViewCategorical, name: string, inputSettings: settingsClass): SettingsTypes[] { - if (categoricalView === null) { - return [null]; - } - if ((categoricalView.categories === null) || (categoricalView.categories === undefined)) { - return [null]; - } const inputCategories: DataViewCategoryColumn = (categoricalView.categories as DataViewCategoryColumn[])[0]; const settingNames = Object.getOwnPropertyNames(inputSettings[name]); diff --git a/src/Functions/isNotNullOrUndefined.ts b/src/Functions/isNotNullOrUndefined.ts new file mode 100644 index 0000000..2b0b042 --- /dev/null +++ b/src/Functions/isNotNullOrUndefined.ts @@ -0,0 +1,3 @@ +export default function isNotNullOrUndefined(x: T): boolean { + return (x !== null) && (x !== undefined); +} diff --git a/src/Functions/isNullOrUndefined.ts b/src/Functions/isNullOrUndefined.ts deleted file mode 100644 index 60ccd1a..0000000 --- a/src/Functions/isNullOrUndefined.ts +++ /dev/null @@ -1,3 +0,0 @@ -export default function isNullOrUndefined(x: T): boolean { - return (x === null) || (x === undefined); -} diff --git a/tsconfig.json b/tsconfig.json index 91c2119..b68a445 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -13,7 +13,6 @@ "emitDecoratorMetadata": true, "experimentalDecorators": true, "allowSyntheticDefaultImports": true, - "strictNullChecks": true, "target": "es6", "sourceMap": true, "outDir": "./.tmp/build/", From d4e8a572a12b3ab3409e1a4c82ed9c86324a0d24 Mon Sep 17 00:00:00 2001 From: Andrew Johnson Date: Fri, 28 Jul 2023 11:23:55 +0300 Subject: [PATCH 04/10] D3 plotting null robust --- src/D3 Plotting Functions/Assurance Icons/consistentFail.ts | 4 ++-- src/D3 Plotting Functions/Assurance Icons/consistentPass.ts | 4 ++-- src/D3 Plotting Functions/Assurance Icons/inconsistent.ts | 5 ++--- src/D3 Plotting Functions/Variation Icons/commonCause.ts | 4 ++-- src/D3 Plotting Functions/Variation Icons/concernHigh.ts | 4 ++-- src/D3 Plotting Functions/Variation Icons/concernLow.ts | 4 ++-- src/D3 Plotting Functions/Variation Icons/improvementHigh.ts | 4 ++-- src/D3 Plotting Functions/Variation Icons/improvementLow.ts | 4 ++-- src/D3 Plotting Functions/Variation Icons/neutralHigh.ts | 4 ++-- src/D3 Plotting Functions/Variation Icons/neutralLow.ts | 4 ++-- src/D3 Plotting Functions/drawDots.ts | 2 +- src/D3 Plotting Functions/drawIcons.ts | 4 ++++ 12 files changed, 25 insertions(+), 22 deletions(-) diff --git a/src/D3 Plotting Functions/Assurance Icons/consistentFail.ts b/src/D3 Plotting Functions/Assurance Icons/consistentFail.ts index fde0013..1d327ff 100644 --- a/src/D3 Plotting Functions/Assurance Icons/consistentFail.ts +++ b/src/D3 Plotting Functions/Assurance Icons/consistentFail.ts @@ -1,4 +1,4 @@ -import { svgBaseType } from "../../visual" +import { iconSelection } from "../drawIcons" /** * Inline function to be called by D3 for rendering the Assurance - Fail icon. @@ -9,7 +9,7 @@ import { svgBaseType } from "../../visual" * * @param selection The D3 parent object to which the icon's SVG code will be added */ -export default function consistentFail(selection: svgBaseType): void { +export default function consistentFail(selection: iconSelection): void { selection.append("g") .attr("clip-path","url(#clip2)") .append("g") diff --git a/src/D3 Plotting Functions/Assurance Icons/consistentPass.ts b/src/D3 Plotting Functions/Assurance Icons/consistentPass.ts index ae6e6b3..22ac4b5 100644 --- a/src/D3 Plotting Functions/Assurance Icons/consistentPass.ts +++ b/src/D3 Plotting Functions/Assurance Icons/consistentPass.ts @@ -1,4 +1,4 @@ -import { svgBaseType } from "../../visual" +import { iconSelection } from "../drawIcons" /** * Inline function to be called by D3 for rendering the Assurance - Pass icon. @@ -9,7 +9,7 @@ import { svgBaseType } from "../../visual" * * @param selection The D3 parent object to which the icon's SVG code will be added */ -export default function consistentPass(selection: svgBaseType): void { +export default function consistentPass(selection: iconSelection): void { selection.append("g") .attr("clip-path","url(#clip2)") .append("g") diff --git a/src/D3 Plotting Functions/Assurance Icons/inconsistent.ts b/src/D3 Plotting Functions/Assurance Icons/inconsistent.ts index 3b888b9..c7eb556 100644 --- a/src/D3 Plotting Functions/Assurance Icons/inconsistent.ts +++ b/src/D3 Plotting Functions/Assurance Icons/inconsistent.ts @@ -1,5 +1,4 @@ -import { svgBaseType } from "../../visual" - +import { iconSelection } from "../drawIcons" /** * Inline function to be called by D3 for rendering the Assurance - Iconsistent icon. * The code below is a translation from HTML to D3 syntax of the SVG file: @@ -9,7 +8,7 @@ import { svgBaseType } from "../../visual" * * @param selection The D3 parent object to which the icon's SVG code will be added */ -export default function inconsistent(selection: svgBaseType): void { +export default function inconsistent(selection: iconSelection): void { selection.append("g") .attr("clip-path","url(#clip2)") .append("g") diff --git a/src/D3 Plotting Functions/Variation Icons/commonCause.ts b/src/D3 Plotting Functions/Variation Icons/commonCause.ts index bf6b054..2441b82 100644 --- a/src/D3 Plotting Functions/Variation Icons/commonCause.ts +++ b/src/D3 Plotting Functions/Variation Icons/commonCause.ts @@ -1,4 +1,4 @@ -import { svgBaseType } from "../../visual"; +import { iconSelection } from "../drawIcons" /** * Inline function to be called by D3 for rendering the Variation - Common Cause icon. @@ -9,7 +9,7 @@ import { svgBaseType } from "../../visual"; * * @param selection The D3 parent object to which the icon's SVG code will be added */ -export default function commonCause(selection: svgBaseType): void { +export default function commonCause(selection: iconSelection): void { selection.append("g") .attr("clip-path","url(#clip2)") .append("g") diff --git a/src/D3 Plotting Functions/Variation Icons/concernHigh.ts b/src/D3 Plotting Functions/Variation Icons/concernHigh.ts index 6c38a28..6d69593 100644 --- a/src/D3 Plotting Functions/Variation Icons/concernHigh.ts +++ b/src/D3 Plotting Functions/Variation Icons/concernHigh.ts @@ -1,4 +1,4 @@ -import { svgBaseType } from "../../visual"; +import { iconSelection } from "../drawIcons" /** * Inline function to be called by D3 for rendering the Variation - Concern High icon. @@ -12,7 +12,7 @@ import { svgBaseType } from "../../visual"; // ESLint errors due to number of lines in function, but would reduce readability to separate further /* eslint-disable */ -export default function concernHigh(selection: svgBaseType): void { +export default function concernHigh(selection: iconSelection): void { selection.append("g") .attr("clip-path","url(#clip2)") .append("g") diff --git a/src/D3 Plotting Functions/Variation Icons/concernLow.ts b/src/D3 Plotting Functions/Variation Icons/concernLow.ts index bdece3c..f410c09 100644 --- a/src/D3 Plotting Functions/Variation Icons/concernLow.ts +++ b/src/D3 Plotting Functions/Variation Icons/concernLow.ts @@ -1,4 +1,4 @@ -import { svgBaseType } from "../../visual" +import { iconSelection } from "../drawIcons" /** * Inline function to be called by D3 for rendering the Variation - Concern Low icon. @@ -9,7 +9,7 @@ import { svgBaseType } from "../../visual" * * @param selection The D3 parent object to which the icon's SVG code will be added */ -export default function concernLow(selection: svgBaseType): void { +export default function concernLow(selection: iconSelection): void { selection.append("g") .attr("clip-path","url(#clip2)") .append("g") diff --git a/src/D3 Plotting Functions/Variation Icons/improvementHigh.ts b/src/D3 Plotting Functions/Variation Icons/improvementHigh.ts index 2daa0c7..88945ca 100644 --- a/src/D3 Plotting Functions/Variation Icons/improvementHigh.ts +++ b/src/D3 Plotting Functions/Variation Icons/improvementHigh.ts @@ -1,4 +1,4 @@ -import { svgBaseType } from "../../visual" +import { iconSelection } from "../drawIcons" /** * Inline function to be called by D3 for rendering the Variation - Improvement High icon. @@ -12,7 +12,7 @@ import { svgBaseType } from "../../visual" // ESLint errors due to number of lines in function, but would reduce readability to separate further /* eslint-disable */ -export default function improvementHigh(selection: svgBaseType): void { +export default function improvementHigh(selection: iconSelection): void { selection.append("g") .attr("clip-path","url(#clip2)") .append("g") diff --git a/src/D3 Plotting Functions/Variation Icons/improvementLow.ts b/src/D3 Plotting Functions/Variation Icons/improvementLow.ts index 87e288e..6888df9 100644 --- a/src/D3 Plotting Functions/Variation Icons/improvementLow.ts +++ b/src/D3 Plotting Functions/Variation Icons/improvementLow.ts @@ -1,4 +1,4 @@ -import { svgBaseType } from "../../visual" +import { iconSelection } from "../drawIcons" /** * Inline function to be called by D3 for rendering the Variation - Improvement Low icon. @@ -9,7 +9,7 @@ import { svgBaseType } from "../../visual" * * @param selection The D3 parent object to which the icon's SVG code will be added */ -export default function improvementLow(selection: svgBaseType): void { +export default function improvementLow(selection: iconSelection): void { selection.append("g") .attr("clip-path","url(#clip2)") .append("g") diff --git a/src/D3 Plotting Functions/Variation Icons/neutralHigh.ts b/src/D3 Plotting Functions/Variation Icons/neutralHigh.ts index 1ea151e..3ffe560 100644 --- a/src/D3 Plotting Functions/Variation Icons/neutralHigh.ts +++ b/src/D3 Plotting Functions/Variation Icons/neutralHigh.ts @@ -1,4 +1,4 @@ -import { svgBaseType } from "../../visual" +import { iconSelection } from "../drawIcons" /** * Inline function to be called by D3 for rendering the Variation - Neutral High icon. @@ -9,7 +9,7 @@ import { svgBaseType } from "../../visual" * * @param selection The D3 parent object to which the icon's SVG code will be added */ -export default function neutralHigh(selection: svgBaseType): void { +export default function neutralHigh(selection: iconSelection): void { selection.append("g") .attr("clip-path","url(#clip2)") .append("g") diff --git a/src/D3 Plotting Functions/Variation Icons/neutralLow.ts b/src/D3 Plotting Functions/Variation Icons/neutralLow.ts index dba1f5c..ab6ed06 100644 --- a/src/D3 Plotting Functions/Variation Icons/neutralLow.ts +++ b/src/D3 Plotting Functions/Variation Icons/neutralLow.ts @@ -1,4 +1,4 @@ -import { svgBaseType } from "../../visual" +import { iconSelection } from "../drawIcons" /** * Inline function to be called by D3 for rendering the Variation - Neutral Low icon. @@ -9,7 +9,7 @@ import { svgBaseType } from "../../visual" * * @param selection The D3 parent object to which the icon's SVG code will be added */ -export default function neutralLow(selection: svgBaseType): void { +export default function neutralLow(selection: iconSelection): void { selection.append("g") .attr("clip-path","url(#clip2)") .append("g") diff --git a/src/D3 Plotting Functions/drawDots.ts b/src/D3 Plotting Functions/drawDots.ts index 049b7b5..15fd3c3 100644 --- a/src/D3 Plotting Functions/drawDots.ts +++ b/src/D3 Plotting Functions/drawDots.ts @@ -33,7 +33,7 @@ export default function drawDots(selection: svgBaseType, visualObj: Visual) { visualObj.host.persistProperties({ replace: [{ objectName: "split_indexes_storage", - selector: undefined, + selector: {}, properties: { split_indexes: JSON.stringify(visualObj.viewModel.splitIndexes) } }] }); diff --git a/src/D3 Plotting Functions/drawIcons.ts b/src/D3 Plotting Functions/drawIcons.ts index 62aba68..6d77a9c 100644 --- a/src/D3 Plotting Functions/drawIcons.ts +++ b/src/D3 Plotting Functions/drawIcons.ts @@ -1,10 +1,13 @@ import * as variationIcon from "./Variation Icons" import * as assuranceIcon from "./Assurance Icons" +import * as d3 from "./D3 Modules" import variationIconsToDraw from "../Functions/variationIconsToDraw"; import initialiseIconSVG from "./initialiseIconSVG"; import { svgBaseType, Visual } from "../visual"; import assuranceIconToDraw from "../Functions/assuranceIconToDraw"; +export type iconSelection = d3.Selection; + export default function drawIcons(selection: svgBaseType, visualObj: Visual): void { selection.selectAll(".icongroup").remove() if (!(visualObj.viewModel.plotProperties.displayPlot)) { @@ -33,6 +36,7 @@ export default function drawIcons(selection: svgBaseType, visualObj: Visual): vo const assurance_location: string = visualObj.viewModel.inputSettings.nhs_icons.assurance_icons_locations; const assurance_scaling: number = visualObj.viewModel.inputSettings.nhs_icons.assurance_icons_scaling; const assuranceIconPresent: string = assuranceIconToDraw(visualObj.viewModel); + if (assuranceIconPresent === "none") { return; } From 82bdbc97f929cc5d3571385d85624e4ef6d6f20d Mon Sep 17 00:00:00 2001 From: Andrew Johnson Date: Fri, 28 Jul 2023 11:26:05 +0300 Subject: [PATCH 05/10] Missed d3 --- src/D3 Plotting Functions/drawLines.ts | 2 +- src/D3 Plotting Functions/drawTooltipLine.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/D3 Plotting Functions/drawLines.ts b/src/D3 Plotting Functions/drawLines.ts index a833405..3d1a6cf 100644 --- a/src/D3 Plotting Functions/drawLines.ts +++ b/src/D3 Plotting Functions/drawLines.ts @@ -15,7 +15,7 @@ export default function drawLines(selection: svgBaseType, visualObj: Visual) { const upper: number = visualObj.viewModel.plotProperties.yAxis.upper; return d3.line() .x(d => visualObj.viewModel.plotProperties.xScale(d.x)) - .y(d => visualObj.viewModel.plotProperties.yScale(d.line_value)) + .y(d => visualObj.viewModel.plotProperties.yScale(d.line_value as number)) .defined(d => d.line_value !== null && between(d.line_value, lower, upper))(d[1]) }) .attr("fill", "none") diff --git a/src/D3 Plotting Functions/drawTooltipLine.ts b/src/D3 Plotting Functions/drawTooltipLine.ts index e19cc8f..e0d4dc5 100644 --- a/src/D3 Plotting Functions/drawTooltipLine.ts +++ b/src/D3 Plotting Functions/drawTooltipLine.ts @@ -32,7 +32,7 @@ export default function drawTooltipLine(selection: svgBaseType, visualObj: Visua const xValue: number = plotProperties.xScale.invert(event.pageX); const xRange: number[] = plotPoints.map(d => d.x).map(d => Math.abs(d - xValue)); - const nearestDenominator: number = d3.leastIndex(xRange,(a,b) => a-b); + const nearestDenominator: number = d3.leastIndex(xRange,(a,b) => a-b) as number; const x_coord: number = plotProperties.xScale(plotPoints[nearestDenominator].x) const y_coord: number = plotProperties.yScale(plotPoints[nearestDenominator].value) From 086637d67ddf3cb9bbd2788bd959508acd00c2fd Mon Sep 17 00:00:00 2001 From: Andrew Johnson Date: Fri, 28 Jul 2023 11:49:43 +0300 Subject: [PATCH 06/10] More functions null robust --- src/Classes/settingsClass.ts | 2 +- src/Functions/BinaryFunctions.ts | 2 +- src/Functions/buildTooltip.ts | 8 ++++---- src/Functions/checkInvalidDataView.ts | 20 ++++++++------------ 4 files changed, 14 insertions(+), 18 deletions(-) diff --git a/src/Classes/settingsClass.ts b/src/Classes/settingsClass.ts index 0ed8c7b..b074408 100644 --- a/src/Classes/settingsClass.ts +++ b/src/Classes/settingsClass.ts @@ -42,7 +42,7 @@ export default class settingsClass implements defaultSettingsType { allSettingGroups.forEach(settingGroup => { let condFormatting: defaultSettingsType[defaultSettingsKey] | undefined; - if (isNotNullOrUndefined(inputView.categorical)) { + if (isNotNullOrUndefined(inputView.categorical) && isNotNullOrUndefined(inputView.categorical.categories)) { condFormatting = extractConditionalFormatting(inputView.categorical as DataViewCategorical, settingGroup, this)[0]; } // Get the names of all settings in a given class and diff --git a/src/Functions/BinaryFunctions.ts b/src/Functions/BinaryFunctions.ts index 01a597b..2c96eb0 100644 --- a/src/Functions/BinaryFunctions.ts +++ b/src/Functions/BinaryFunctions.ts @@ -27,6 +27,6 @@ export const add = broadcast_binary((x: number, y: number): number => x + y); export const subtract = broadcast_binary((x: number, y: number): number => x - y); export const divide = broadcast_binary((x: number, y: number): number => x / y); export const multiply = broadcast_binary( - (x: number, y: number): number => { + (x: number, y: number): number | null => { return (x === null || y === null) ? null : (x * y); }); diff --git a/src/Functions/buildTooltip.ts b/src/Functions/buildTooltip.ts index 84136d9..fb23420 100644 --- a/src/Functions/buildTooltip.ts +++ b/src/Functions/buildTooltip.ts @@ -26,8 +26,8 @@ export default function buildTooltip(index: number, controlLimits: controlLimits const date: string = controlLimits.keys[index].label; const value: number = controlLimits.values[index]; - const numerator: number = controlLimits.numerators ? controlLimits.numerators[index] : null; - const denominator: number = controlLimits.denominators ? controlLimits.denominators[index] : null; + const numerator: number | null = controlLimits.numerators ? controlLimits.numerators[index] : null; + const denominator: number | null = controlLimits.denominators ? controlLimits.denominators[index] : null; const target: number = controlLimits.targets[index]; const limits = { ll99: controlLimits.ll99 ? controlLimits.ll99[index] : null, @@ -71,7 +71,7 @@ export default function buildTooltip(index: number, controlLimits: controlLimits if (chart_type !== "run") { tooltip.push({ displayName: "Upper 99% Limit", - value: (limits.ul99 * multiplier).toFixed(sig_figs) + suffix + value: (limits.ul99 !== null) ? (limits.ul99 * multiplier).toFixed(sig_figs) + suffix : "" }) } tooltip.push({ @@ -81,7 +81,7 @@ export default function buildTooltip(index: number, controlLimits: controlLimits if (chart_type !== "run") { tooltip.push({ displayName: "Lower 99% Limit", - value: (limits.ll99 * multiplier).toFixed(sig_figs) + suffix + value: (limits.ll99 !== null) ? (limits.ll99 * multiplier).toFixed(sig_figs) + suffix : "" }) } diff --git a/src/Functions/checkInvalidDataView.ts b/src/Functions/checkInvalidDataView.ts index 6469d21..4f0bc82 100644 --- a/src/Functions/checkInvalidDataView.ts +++ b/src/Functions/checkInvalidDataView.ts @@ -12,21 +12,17 @@ export default function checkInvalidDataView(inputDV: powerbi.DataView[]): boole return flag1; } - const flag2: boolean = - !inputDV[0].categorical.categories[0].source + const inputView: powerbi.DataViewCategorical = inputDV[0].categorical as powerbi.DataViewCategorical; + const inputCategories: powerbi.DataViewCategoryColumn[] = inputView.categories as powerbi.DataViewCategoryColumn[]; + const inputValues: powerbi.DataViewValueColumns = inputView.values as powerbi.DataViewValueColumns; - if (flag2) { - return flag2; + if (!inputCategories[0].source) { + return true; } - const flag3: boolean = - !inputDV[0].categorical.values[0].source.roles.numerators - if (flag3) { - return flag3; + if (!(inputValues[0].source.roles as Record).numerators) { + return true; } - const flag4: boolean = - inputDV[0].categorical.values.some(d => d.values.length < 1) - || inputDV[0].categorical.categories.some(d => d.values.length < 1); - return flag4; + return inputValues.some(d => d.values.length < 1) || inputCategories.some(d => d.values.length < 1); } From c4866cdc64d2d365a3e99bddc9fd7f5459183c28 Mon Sep 17 00:00:00 2001 From: Andrew Johnson Date: Fri, 28 Jul 2023 12:55:36 +0300 Subject: [PATCH 07/10] More strict null handling --- src/Classes/controlLimitsClass.ts | 5 ++- src/Classes/settingsClass.ts | 7 ++-- src/D3 Plotting Functions/D3 Modules/index.ts | 8 ++-- src/Functions/BinaryFunctions.ts | 2 +- src/Functions/Constants.ts | 41 ++++++++++--------- src/Functions/UnaryFunctions.ts | 2 +- src/Functions/dateToFormattedString.ts | 2 +- src/Functions/diff.ts | 6 +-- src/Functions/extractDataColumn.ts | 8 ++-- src/Functions/isNotNullOrUndefined.ts | 2 +- src/Limit Calculations/mr.ts | 4 +- src/Limit Calculations/run.ts | 12 +++--- src/Limit Calculations/t.ts | 2 +- 13 files changed, 51 insertions(+), 50 deletions(-) diff --git a/src/Classes/controlLimitsClass.ts b/src/Classes/controlLimitsClass.ts index 971b2a9..d8e8097 100644 --- a/src/Classes/controlLimitsClass.ts +++ b/src/Classes/controlLimitsClass.ts @@ -8,6 +8,7 @@ import checkFlagDirection from "../Functions/checkFlagDirection" import truncate from "../Functions/truncate"; import { truncateInputs } from "../Functions/truncate"; import { multiply } from "../Functions/BinaryFunctions"; +import isNotNullOrUndefined from "../Functions/isNotNullOrUndefined"; type controlLimitsArgs = { inputSettings: settingsClass, @@ -90,10 +91,10 @@ export default class controlLimitsClass { if (args) { this.keys = args.keys; this.values = args.values; - if (args.numerators || !(args.numerators === null || args.numerators === undefined)) { + if (isNotNullOrUndefined(args.numerators)) { this.numerators = args.numerators; } - if (args.denominators || !(args.denominators === null || args.denominators === undefined)) { + if (isNotNullOrUndefined(args.denominators)) { this.denominators = args.denominators; } this.targets = args.targets; diff --git a/src/Classes/settingsClass.ts b/src/Classes/settingsClass.ts index b074408..31ffa42 100644 --- a/src/Classes/settingsClass.ts +++ b/src/Classes/settingsClass.ts @@ -1,6 +1,5 @@ import powerbi from "powerbi-visuals-api"; import DataView = powerbi.DataView; -import DataViewCategorical = powerbi.DataViewCategorical; import DataViewPropertyValue = powerbi.DataViewPropertyValue import VisualObjectInstanceEnumerationObject = powerbi.VisualObjectInstanceEnumerationObject; import VisualEnumerationInstanceKinds = powerbi.VisualEnumerationInstanceKinds; @@ -42,8 +41,10 @@ export default class settingsClass implements defaultSettingsType { allSettingGroups.forEach(settingGroup => { let condFormatting: defaultSettingsType[defaultSettingsKey] | undefined; - if (isNotNullOrUndefined(inputView.categorical) && isNotNullOrUndefined(inputView.categorical.categories)) { - condFormatting = extractConditionalFormatting(inputView.categorical as DataViewCategorical, settingGroup, this)[0]; + if (isNotNullOrUndefined(inputView.categorical)) { + if (isNotNullOrUndefined(inputView.categorical.categories)) { + condFormatting = extractConditionalFormatting(inputView.categorical, settingGroup, this)[0]; + } } // Get the names of all settings in a given class and // use those to extract and update the relevant values diff --git a/src/D3 Plotting Functions/D3 Modules/index.ts b/src/D3 Plotting Functions/D3 Modules/index.ts index 97f4e14..1273deb 100644 --- a/src/D3 Plotting Functions/D3 Modules/index.ts +++ b/src/D3 Plotting Functions/D3 Modules/index.ts @@ -1,4 +1,4 @@ -import { min as d3min, max as d3max } from "d3-array"; +import { min as d3min, max as d3max, mean as d3mean, median as d3median } from "d3-array"; /** * D3 Array reduction functions have return type number | undefined, @@ -6,10 +6,12 @@ import { min as d3min, max as d3max } from "d3-array"; */ const min = (x: number[]): number => d3min(x) as number; const max = (x: number[]): number => d3max(x) as number; +const mean = (x: number[]): number => d3mean(x) as number; +const median = (x: number[]): number => d3median(x) as number; export { select, selectAll, Selection, BaseType } from "d3-selection"; -export { groups, leastIndex, mean, median, sum } from "d3-array"; -export { min, max }; +export { groups, leastIndex, sum } from "d3-array"; +export { min, max, mean, median }; export { line } from "d3-shape" export { Axis, axisBottom, axisLeft } from "d3-axis" export { ScaleLinear, scaleLinear, NumberValue } from "d3-scale" diff --git a/src/Functions/BinaryFunctions.ts b/src/Functions/BinaryFunctions.ts index 2c96eb0..3e0fa76 100644 --- a/src/Functions/BinaryFunctions.ts +++ b/src/Functions/BinaryFunctions.ts @@ -1,4 +1,4 @@ -type ReturnT = Input1T extends Array ? BaseT[] : Input2T extends Array ? BaseT[] : BaseT; +type ReturnT = Input1T extends Array ? NonNullable[] : Input2T extends Array ? NonNullable[] : NonNullable; export default function broadcast_binary(fun: (x: ScalarInput1T, y: ScalarInput2T) => ScalarReturnT) { return function { - if ((sampleSize <= 1) || (sampleSize === null)) { - return null; - } - const Nminus1: number = sampleSize - 1; +function c4(sampleSize: number): number { + const Nminus1: number = sampleSize - 1; - return sqrt(2.0 / Nminus1) - * exp(lgamma(sampleSize / 2.0) - lgamma(Nminus1 / 2.0)); - } -); + return sqrt(2.0 / Nminus1) + * exp(lgamma(sampleSize / 2.0) - lgamma(Nminus1 / 2.0)); +}; -export const c5 = broadcast_unary( - (sampleSize: number): number => { - return sqrt(1 - square(c4(sampleSize))); - } -); +function c5(sampleSize: number): number { + return sqrt(1 - square(c4(sampleSize) as number)); +}; export const a3 = broadcast_unary( - (sampleSize: number): number => { - const filt_samp: number = sampleSize <= 1 ? null : sampleSize; - return 3.0 / (c4(filt_samp) * sqrt(filt_samp)); + (sampleSize: number): number | null => { + if ((sampleSize === null) || (sampleSize <= 1)) { + return null; + } + return 3.0 / (c4(sampleSize) as number * sqrt(sampleSize)); } ); @@ -35,13 +30,19 @@ const b_helper = broadcast_binary( ) export const b3 = broadcast_binary( - (sampleSize: number, use95: boolean): number => { + (sampleSize: number, use95: boolean): number | null => { + if ((sampleSize === null) || (sampleSize <= 1)) { + return null; + } return 1 - b_helper(sampleSize, use95); } ); export const b4 = broadcast_binary( - (sampleSize: number, use95: boolean): number => { + (sampleSize: number, use95: boolean): number | null => { + if ((sampleSize === null) || (sampleSize <= 1)) { + return null; + } return 1 + b_helper(sampleSize, use95); } ); diff --git a/src/Functions/UnaryFunctions.ts b/src/Functions/UnaryFunctions.ts index 34552d1..d6d2cd2 100644 --- a/src/Functions/UnaryFunctions.ts +++ b/src/Functions/UnaryFunctions.ts @@ -1,6 +1,6 @@ import gammaln from "@stdlib/math-base-special-gammaln"; -type ReturnT = InputT extends Array ? BaseT[] : BaseT; +type ReturnT = InputT extends Array ? NonNullable[] : NonNullable; export default function broadcast_unary(fun: (x: ScalarInputT) => ScalarReturnT) { return function(y: T): ReturnT { diff --git a/src/Functions/dateToFormattedString.ts b/src/Functions/dateToFormattedString.ts index b58d626..43ce182 100644 --- a/src/Functions/dateToFormattedString.ts +++ b/src/Functions/dateToFormattedString.ts @@ -49,7 +49,7 @@ const dateToFormattedString = broadcast_binary( const formatString: string = `{ ${localeDateMap[inpLocale]}, "options": { "day" : "2-digit", ${monthDateMap[inpMonth]}, ${yearDateMap[inpYear]} }, ${delimDateMap[inpDelim]} }`; const date_format: dateFormat = JSON.parse(formatString); const formattedString: string = input_date.toLocaleDateString(date_format.locale, date_format.options) - .replace(/(\/|(\s|,\s))/gi, date_format.delimiter); + .replace(/(\/|(\s|,\s))/gi, date_format.delimiter as string); if (inpDay !== "DD") { const weekday: string = input_date.toLocaleDateString(date_format.locale, {weekday : weekdayDateMap[inpDay]}) return weekday + " " + formattedString; diff --git a/src/Functions/diff.ts b/src/Functions/diff.ts index 73e6a07..cb5b0b1 100644 --- a/src/Functions/diff.ts +++ b/src/Functions/diff.ts @@ -1,7 +1,3 @@ export default function diff(x: number[]): number[] { - const consec_diff: number[] = new Array(x.length - 1); - for (let i = 1; i < x.length; i++) { - consec_diff[(i-1)] = (x[i] - x[(i-1)]); - } - return [null].concat(consec_diff); + return x.map((d, idx, arr) => idx > 0 ? d - arr[idx - 1] : null) as number[] } diff --git a/src/Functions/extractDataColumn.ts b/src/Functions/extractDataColumn.ts index c9193f4..a22c306 100644 --- a/src/Functions/extractDataColumn.ts +++ b/src/Functions/extractDataColumn.ts @@ -29,22 +29,22 @@ export default function extractDataColumn(inputView: DataView } else { columnRaw = columnRawTmp[0]; } - if (columnRaw.source.type.dateTime) { + if (columnRaw.source.type?.dateTime) { return dateToFormattedString(columnRaw.values, inputSettings.dates) as Extract; } else { return columnRaw.values as Extract; } } else if (name === "tooltips") { let rtn = new Array(); - const tooltipColumns = inputView.values.filter(viewColumn => viewColumn.source.roles.tooltips); + const tooltipColumns = inputView.values!.filter(viewColumn => viewColumn.source.roles?.tooltips); if (tooltipColumns.length > 0) { rtn = tooltipColumns[0].values.map((_, idx) => { return tooltipColumns.map(viewColumn => { return { displayName: viewColumn.source.displayName, - value: viewColumn.source.type.numeric + value: viewColumn.source.type?.numeric ? ((viewColumn.values[idx])).toString() - : viewColumn.source.type.dateTime + : viewColumn.source.type?.dateTime ? dateToFormattedString((viewColumn.values[idx]), inputSettings.dates) : (viewColumn.values[idx]) } diff --git a/src/Functions/isNotNullOrUndefined.ts b/src/Functions/isNotNullOrUndefined.ts index 2b0b042..9e18bee 100644 --- a/src/Functions/isNotNullOrUndefined.ts +++ b/src/Functions/isNotNullOrUndefined.ts @@ -1,3 +1,3 @@ -export default function isNotNullOrUndefined(x: T): boolean { +export default function isNotNullOrUndefined(x: T | null | undefined): x is T { return (x !== null) && (x !== undefined); } diff --git a/src/Limit Calculations/mr.ts b/src/Limit Calculations/mr.ts index 1759bd9..8e990c9 100644 --- a/src/Limit Calculations/mr.ts +++ b/src/Limit Calculations/mr.ts @@ -20,8 +20,8 @@ export default function mrLimits(inputData: dataClass, inputSettings: settingsCl inputSettings: inputSettings, keys: inputData.keys, values: consec_diff, - numerators: useRatio ? inputData.numerators : null, - denominators: useRatio ? inputData.denominators : null, + numerators: useRatio ? inputData.numerators : undefined, + denominators: useRatio ? inputData.denominators : undefined, targets: rep(cl, inputData.keys.length), ll99: rep(0, inputData.keys.length), ll95: rep(0, inputData.keys.length), diff --git a/src/Limit Calculations/run.ts b/src/Limit Calculations/run.ts index 2479152..d93c4b1 100644 --- a/src/Limit Calculations/run.ts +++ b/src/Limit Calculations/run.ts @@ -16,12 +16,12 @@ export default function runLimits(inputData: dataClass, inputSettings: settingsC inputSettings: inputSettings, keys: inputData.keys, values: ratio.map(d => isNaN(d) ? 0 : d), - numerators: useRatio ? inputData.numerators : null, - denominators: useRatio ? inputData.denominators : null, + numerators: useRatio ? inputData.numerators : new Array(), + denominators: useRatio ? inputData.denominators : new Array(), targets: rep(cl, inputData.keys.length), - ll99: null, - ll95: null, - ul95: null, - ul99: null + ll99: new Array(), + ll95: new Array(), + ul95: new Array(), + ul99: new Array() }); } diff --git a/src/Limit Calculations/t.ts b/src/Limit Calculations/t.ts index 4037f45..4b1af49 100644 --- a/src/Limit Calculations/t.ts +++ b/src/Limit Calculations/t.ts @@ -9,7 +9,7 @@ export default function tLimits(inputData: dataClass, inputSettings: settingsCla const val: number[] = pow(inputData.numerators, 1 / 3.6); const inputDataCopy: dataClass = JSON.parse(JSON.stringify(inputData)); inputDataCopy.numerators = val; - inputDataCopy.denominators = null; + inputDataCopy.denominators = new Array(); const limits: controlLimitsClass = iLimits(inputDataCopy, inputSettings); limits.targets = pow(limits.targets, 3.6); limits.values = pow(limits.values, 3.6); From ec88f8152771eea5ee3d5d739e8fd519d8ca1037 Mon Sep 17 00:00:00 2001 From: Andrew Johnson Date: Fri, 28 Jul 2023 13:00:22 +0300 Subject: [PATCH 08/10] Remaining null strict --- src/defaultSettings.ts | 24 ++++++++++++------------ tsconfig.json | 1 + 2 files changed, 13 insertions(+), 12 deletions(-) diff --git a/src/defaultSettings.ts b/src/defaultSettings.ts index b2823eb..1892747 100644 --- a/src/defaultSettings.ts +++ b/src/defaultSettings.ts @@ -12,9 +12,9 @@ const defaultSettings = { sig_figs: 2, perc_labels: "Automatic", split_on_click: false, - ll_truncate: (null), - ul_truncate: (null), - alt_target: (null) + ll_truncate: (null), + ul_truncate: (null), + alt_target: (null) }, outliers: { process_flag_type: "both", @@ -81,13 +81,13 @@ const defaultSettings = { xlimit_tick_size: 10, xlimit_tick_colour: "#000000", xlimit_tick_rotation: -35, - xlimit_tick_count: (null), - xlimit_label:(null), + xlimit_tick_count: (null), + xlimit_label:"", xlimit_label_font: "'Arial', sans-serif", xlimit_label_size: 10, xlimit_label_colour: "#000000", - xlimit_l: (null), - xlimit_u: (null) + xlimit_l: (null), + xlimit_u: (null) }, y_axis: { ylimit_colour: "#000000", @@ -96,15 +96,15 @@ const defaultSettings = { ylimit_tick_size: 10, ylimit_tick_colour: "#000000", ylimit_tick_rotation: -35, - ylimit_tick_count: (null), - ylimit_label:(null), + ylimit_tick_count: (null), + ylimit_label:"", ylimit_label_font: "'Arial', sans-serif", ylimit_label_size: 10, ylimit_label_colour: "#000000", - ylimit_l: (null), - ylimit_u: (null), + ylimit_l: (null), + ylimit_u: (null), limit_multiplier: 1.5, - ylimit_sig_figs: (null) + ylimit_sig_figs: (null) }, dates: { date_format_day: "DD", diff --git a/tsconfig.json b/tsconfig.json index b68a445..ced9fdf 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -13,6 +13,7 @@ "emitDecoratorMetadata": true, "experimentalDecorators": true, "allowSyntheticDefaultImports": true, + "strictNullChecks": false, "target": "es6", "sourceMap": true, "outDir": "./.tmp/build/", From ce072d65ae69d08dcf9d1dae0a4c65c8e0f2f457 Mon Sep 17 00:00:00 2001 From: Andrew Johnson Date: Fri, 28 Jul 2023 14:38:45 +0300 Subject: [PATCH 09/10] Tidying --- src/Classes/controlLimitsClass.ts | 4 ++-- src/D3 Plotting Functions/drawYAxis.ts | 6 +++--- src/D3 Plotting Functions/updateHighlighting.ts | 6 +++++- src/Limit Calculations/run.ts | 4 ++-- tsconfig.json | 2 +- 5 files changed, 13 insertions(+), 9 deletions(-) diff --git a/src/Classes/controlLimitsClass.ts b/src/Classes/controlLimitsClass.ts index d8e8097..b5c349e 100644 --- a/src/Classes/controlLimitsClass.ts +++ b/src/Classes/controlLimitsClass.ts @@ -14,8 +14,8 @@ type controlLimitsArgs = { inputSettings: settingsClass, keys: { x: number, id: number, label: string }[]; values: number[]; - numerators?: number[]; - denominators?: number[]; + numerators?: number[] | undefined; + denominators?: number[] | undefined; targets: number[]; ll99: number[]; ll95: number[]; diff --git a/src/D3 Plotting Functions/drawYAxis.ts b/src/D3 Plotting Functions/drawYAxis.ts index 56a8ce1..ffb14bf 100644 --- a/src/D3 Plotting Functions/drawYAxis.ts +++ b/src/D3 Plotting Functions/drawYAxis.ts @@ -18,10 +18,10 @@ export default function drawYAxis(selection: svgBaseType, visualObj: Visual, ref } if (visualObj.viewModel.inputData) { yAxis.tickFormat( - (d: number) => { + (d: d3.NumberValue) => { return visualObj.viewModel.inputData.percentLabels - ? (d * (multiplier === 100 ? 1 : (multiplier === 1 ? 100 : multiplier))).toFixed(sig_figs) + "%" - : d.toFixed(sig_figs); + ? (d * (multiplier === 100 ? 1 : (multiplier === 1 ? 100 : multiplier))).toFixed(sig_figs) + "%" + : (d).toFixed(sig_figs); } ); } diff --git a/src/D3 Plotting Functions/updateHighlighting.ts b/src/D3 Plotting Functions/updateHighlighting.ts index 5a9bf47..19d4efc 100644 --- a/src/D3 Plotting Functions/updateHighlighting.ts +++ b/src/D3 Plotting Functions/updateHighlighting.ts @@ -1,8 +1,11 @@ import powerbi from "powerbi-visuals-api"; +import * as d3 from "./D3 Modules" import { plotData } from "../Classes/viewModelClass"; import ISelectionId = powerbi.visuals.ISelectionId; import { svgBaseType, Visual } from "../visual"; +type dotSelection = d3.Selection + export default function updateHighlighting(selection: svgBaseType, visualObj: Visual) { const anyHighlights: boolean = visualObj.viewModel.inputData ? visualObj.viewModel.inputData.anyHighlights : false; const allSelectionIDs: ISelectionId[] = visualObj.selectionManager.getSelectionIds() as ISelectionId[]; @@ -16,7 +19,8 @@ export default function updateHighlighting(selection: svgBaseType, visualObj: Vi selection.selectAll(".dotsgroup").selectChildren().style("fill-opacity", defaultOpacity); selection.selectAll(".linesgroup").style("stroke-opacity", defaultOpacity); if (anyHighlights || (allSelectionIDs.length > 0)) { - selection.selectAll(".dotsgroup").selectChildren().style("fill-opacity", (dot: plotData) => { + const dotSelection: dotSelection = selection.selectAll(".dotsgroup").selectChildren(); + dotSelection.style("fill-opacity", (dot: any) => { const currentPointSelected: boolean = allSelectionIDs.some((currentSelectionId: ISelectionId) => { return currentSelectionId.includes(dot.identity); }); diff --git a/src/Limit Calculations/run.ts b/src/Limit Calculations/run.ts index d93c4b1..1731570 100644 --- a/src/Limit Calculations/run.ts +++ b/src/Limit Calculations/run.ts @@ -16,8 +16,8 @@ export default function runLimits(inputData: dataClass, inputSettings: settingsC inputSettings: inputSettings, keys: inputData.keys, values: ratio.map(d => isNaN(d) ? 0 : d), - numerators: useRatio ? inputData.numerators : new Array(), - denominators: useRatio ? inputData.denominators : new Array(), + numerators: useRatio ? inputData.numerators : undefined, + denominators: useRatio ? inputData.denominators : undefined, targets: rep(cl, inputData.keys.length), ll99: new Array(), ll95: new Array(), diff --git a/tsconfig.json b/tsconfig.json index ced9fdf..e5f062b 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -9,11 +9,11 @@ "noImplicitThis": true, "noUnusedLocals": true, "noUnusedParameters": true, + "noImplicitOverride": true, "strictBindCallApply": true, "emitDecoratorMetadata": true, "experimentalDecorators": true, "allowSyntheticDefaultImports": true, - "strictNullChecks": false, "target": "es6", "sourceMap": true, "outDir": "./.tmp/build/", From 7e13fc2640307f170880f27b4d7b3139ae2082ab Mon Sep 17 00:00:00 2001 From: Andrew Johnson Date: Fri, 28 Jul 2023 14:42:01 +0300 Subject: [PATCH 10/10] eslint --- src/Classes/viewModelClass.ts | 2 +- src/Functions/Constants.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Classes/viewModelClass.ts b/src/Classes/viewModelClass.ts index c72060d..ef65dbb 100644 --- a/src/Classes/viewModelClass.ts +++ b/src/Classes/viewModelClass.ts @@ -65,7 +65,7 @@ export default class viewModelClass { const split_indexes: DataViewPropertyValue | null = split_indexes_storage ? split_indexes_storage.split_indexes : null; this.splitIndexes = split_indexes ? JSON.parse((split_indexes)) : new Array(); - let invalidDataView: boolean = checkInvalidDataView(dv); + const invalidDataView: boolean = checkInvalidDataView(dv); // Make sure that the construction returns early with null members so // that the visual does not crash when trying to process invalid data if (invalidDataView) { diff --git a/src/Functions/Constants.ts b/src/Functions/Constants.ts index b9d8b5b..8bee7e2 100644 --- a/src/Functions/Constants.ts +++ b/src/Functions/Constants.ts @@ -7,11 +7,11 @@ function c4(sampleSize: number): number { return sqrt(2.0 / Nminus1) * exp(lgamma(sampleSize / 2.0) - lgamma(Nminus1 / 2.0)); -}; +} function c5(sampleSize: number): number { return sqrt(1 - square(c4(sampleSize) as number)); -}; +} export const a3 = broadcast_unary( (sampleSize: number): number | null => {