Skip to content

Commit

Permalink
Begin consolidating and simplifying grouped data processing (#318)
Browse files Browse the repository at this point in the history
* Begin consolidating and simplifying grouped data processing

* Lints
  • Loading branch information
andrjohns authored Aug 19, 2024
1 parent 0d01176 commit 40291aa
Show file tree
Hide file tree
Showing 8 changed files with 116 additions and 96 deletions.
2 changes: 1 addition & 1 deletion pbiviz.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
2 changes: 1 addition & 1 deletion src/Classes/index.ts
Original file line number Diff line number Diff line change
@@ -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"
2 changes: 1 addition & 1 deletion src/Classes/settingsClass.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<defaultSettingsType>();
if (is_grouped) {
groupIdxs.forEach(() => {
Expand Down
69 changes: 67 additions & 2 deletions src/Classes/viewModelClass.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<number[]>();
const indicator_names = new Array<string>();
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 = {
Expand Down
4 changes: 3 additions & 1 deletion src/D3 Plotting Functions/addContextMenu.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
2 changes: 1 addition & 1 deletion src/Functions/extractInputData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<defaultSettingsType["spc"]>(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);
}
Expand Down
41 changes: 21 additions & 20 deletions src/Functions/validateInputData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)) {
Expand All @@ -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;
Expand Down Expand Up @@ -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<string>();
let all_status: ValidationFailTypes[] = new Array<ValidationFailTypes>();
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);
}
Expand All @@ -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;
}
Expand Down
90 changes: 21 additions & 69 deletions src/visual.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<SVGSVGElement, unknown, null, undefined>;
export type divBaseType = d3.Selection<HTMLDivElement, unknown, null, undefined>;
Expand Down Expand Up @@ -39,69 +38,39 @@ export class Visual implements powerbi.extensibility.IVisual {
}

public update(options: VisualUpdateOptions): void {
const idx_per_indicator = new Array<number[]>();
const indicator_names = new Array<string>();
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)
Expand All @@ -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) {
Expand All @@ -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;
Expand Down

0 comments on commit 40291aa

Please sign in to comment.