Skip to content

Commit

Permalink
Fix input aggregation of conditional formatting
Browse files Browse the repository at this point in the history
  • Loading branch information
andrjohns committed Aug 18, 2024
1 parent 803956e commit 1752020
Show file tree
Hide file tree
Showing 9 changed files with 77 additions and 82 deletions.
34 changes: 5 additions & 29 deletions capabilities.json
Original file line number Diff line number Diff line change
Expand Up @@ -984,12 +984,14 @@
"key": { "max": 5 },
"numerators": { "max": 1 },
"denominators": { "max": 1 },
"xbar_sds": { "max": 1 },
"indicator": { "max": 0 }
"xbar_sds": { "max": 1 }
}],
"categorical": {
"categories": {
"for": { "in": "key" },
"select": [
{ "for": { "in": "key" } },
{ "for": { "in": "indicator" } }
],
"dataReductionAlgorithm": { "window": { "count": 30000 } }
},
"values": {
Expand All @@ -1002,31 +1004,5 @@
]
}
}
},{
"conditions": [{
"key": { "max": 5 },
"numerators": { "max": 1 },
"denominators": { "max": 1 },
"xbar_sds": { "max": 1 },
"indicator": { "min": 1 }
}],
"categorical": {
"categories": {
"for": { "in": "key" },
"dataReductionAlgorithm": { "window": { "count": 30000 } }
},
"values": {
"group": {
"by": "indicator",
"select": [
{ "for": { "in": "numerators" } },
{ "for": { "in": "denominators" } },
{ "for": { "in": "groupings" } },
{ "for": { "in": "xbar_sds" } },
{ "for": { "in": "tooltips" } }
]
}
}
}
}]
}
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.1",
"version":"1.4.4.2",
"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
18 changes: 7 additions & 11 deletions src/Classes/settingsClass.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,28 +46,24 @@ export default class settingsClass {
*
* @param inputObjects
*/
update(inputView: DataView): void {
update(inputView: DataView, groupIdxs: number[][]): void {
this.validationStatus
= JSON.parse(JSON.stringify({ status: 0, messages: new Array<string[]>(), error: "" }));
// Get the names of all classes in settingsObject which have values to be updated
const allSettingGroups: string[] = Object.keys(this.settings);

const is_grouped: boolean = inputView.categorical.values?.source?.roles?.indicator;
let group_idxs: number[] = new Array<number>();
const is_grouped: boolean = inputView.categorical.categories.some(d => d.source.roles.indicator);
this.settingsGrouped = new Array<defaultSettingsType>();
if (is_grouped) {
group_idxs = inputView.categorical.values.grouped().map(d => {
groupIdxs.forEach(() => {
this.settingsGrouped.push(Object.fromEntries(Object.keys(defaultSettings).map((settingGroupName) => {
return [settingGroupName, Object.fromEntries(Object.keys(defaultSettings[settingGroupName]).map((settingName) => {
return [settingName, defaultSettings[settingGroupName][settingName]];
}))];
})) as settingsValueTypes);

return d.values[0].values.findIndex(d_in => !isNullOrUndefined(d_in));
});
})
}


allSettingGroups.forEach((settingGroup: defaultSettingsKey) => {
const condFormatting: ConditionalReturnT<defaultSettingsType[defaultSettingsKey]>
= extractConditionalFormatting(inputView?.categorical, settingGroup, this.settings);
Expand Down Expand Up @@ -97,10 +93,10 @@ export default class settingsClass {
: defaultSettings[settingGroup][settingName]["default"]

if (is_grouped) {
group_idxs.forEach((idx, idx_idx) => {
groupIdxs.forEach((idx, idx_idx) => {
this.settingsGrouped[idx_idx][settingGroup][settingName]
= condFormatting?.values
? condFormatting?.values[idx][settingName]
? condFormatting?.values[idx[0]][settingName]
: defaultSettings[settingGroup][settingName]["default"]
})
}
Expand Down Expand Up @@ -205,7 +201,7 @@ export default class settingsClass {
objectName: settingGroupName,
properties: props,
propertyInstanceKind: Object.fromEntries(propertyKinds),
selector: { data: [{ roles: ["key"] }] },
selector: { data: [{ dataViewWildcard: { matchingOption: 0 } }] },
validValues: Object.fromEntries(Object.keys(defaultSettings[settingGroupName]).map((settingName: defaultSettingsNestedKey) => {
return [settingName, defaultSettings[settingGroupName][settingName]?.["valid"]]
}))
Expand Down
26 changes: 14 additions & 12 deletions src/Classes/viewModelClass.ts
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,8 @@ export default class viewModelClass {
this.colourPalette = null;
}

update(options: VisualUpdateOptions, host: IVisualHost) {
update(options: VisualUpdateOptions, host: IVisualHost, groupIdxs: number[][], groupNames: string[]): void {
this.groupNames = groupNames;
if (isNullOrUndefined(this.colourPalette)) {
this.colourPalette = {
isHighContrast: host.colorPalette.isHighContrast,
Expand All @@ -168,24 +169,20 @@ export default class viewModelClass {

// Only re-construct data and re-calculate limits if they have changed
if (options.type === 2 || this.firstRun) {
if (options.dataViews[0].categorical.values?.source?.roles?.indicator) {
if (options.dataViews[0].categorical.categories.some(d => d.source.roles.indicator)) {
this.showGrouped = true;
this.groupNames = new Array<string>();
this.inputDataGrouped = new Array<dataObject>();
this.groupStartEndIndexesGrouped = new Array<number[][]>();
this.controlLimitsGrouped = new Array<controlLimitsObject>();
this.outliersGrouped = new Array<outliersObject>();
this.identitiesGrouped = new Array<ISelectionId>();

options.dataViews[0].categorical.values.grouped().forEach((d, idx) => {
(<powerbi.DataViewCategorical>d).categories = options.dataViews[0].categorical.categories;
const first_idx: number = d.values[0].values.findIndex(d_in => !isNullOrUndefined(d_in));
const last_idx: number = d.values[0].values.map(d_in => !isNullOrUndefined(d_in)).lastIndexOf(true);
const inpData: dataObject = extractInputData(<powerbi.DataViewCategorical>d,
groupIdxs.forEach((group_idxs, idx) => {
const inpData: dataObject = extractInputData(options.dataViews[0].categorical,
this.inputSettings.settingsGrouped[idx],
this.inputSettings.derivedSettingsGrouped[idx],
this.inputSettings.validationStatus.messages,
first_idx, last_idx);
group_idxs);
const invalidData: boolean = inpData.validationStatus.status !== 0;
const groupStartEndIndexes: number[][] = invalidData ? new Array<number[]>() : this.getGroupingIndexes(inpData);
const limits: controlLimitsObject = invalidData ? null : this.calculateLimits(inpData, groupStartEndIndexes, this.inputSettings.settingsGrouped[idx]);
Expand All @@ -197,8 +194,13 @@ export default class viewModelClass {
this.scaleAndTruncateLimits(limits, this.inputSettings.settingsGrouped[idx],
this.inputSettings.derivedSettingsGrouped[idx]);
}
this.identitiesGrouped.push(host.createSelectionIdBuilder().withSeries(options.dataViews[0].categorical.values, d).createSelectionId());
this.groupNames.push(<string>d.name);
const idBuilder = host.createSelectionIdBuilder();
group_idxs.forEach(i => {
options.dataViews[0].categorical.categories.forEach(d => {
idBuilder.withCategory(d, i);
})
})
this.identitiesGrouped.push(idBuilder.createSelectionId());
this.inputDataGrouped.push(inpData);
this.groupStartEndIndexesGrouped.push(groupStartEndIndexes);
this.controlLimitsGrouped.push(limits);
Expand All @@ -218,7 +220,7 @@ export default class viewModelClass {
this.inputSettings.settings,
this.inputSettings.derivedSettings,
this.inputSettings.validationStatus.messages,
0, options.dataViews[0].categorical.values[0].values.length - 1);
groupIdxs[0]);

if (this.inputData.validationStatus.status === 0) {
this.groupStartEndIndexes = this.getGroupingIndexes(this.inputData, this.splitIndexes);
Expand Down
6 changes: 5 additions & 1 deletion src/D3 Plotting Functions/drawDots.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,10 @@ export default function drawDots(selection: svgBaseType, visualObj: Visual) {
// PowerBI based on all selected dots
.select(d.identity, (event.ctrlKey || event.metaKey))
// Change opacity of non-selected dots
.then(() => { visualObj.updateHighlighting(); });
.then(() => {
console.log("a")
visualObj.updateHighlighting();
});
}
event.stopPropagation();
}
Expand Down Expand Up @@ -74,6 +77,7 @@ export default function drawDots(selection: svgBaseType, visualObj: Visual) {
});

selection.on('click', () => {
console.log("b")
visualObj.selectionManager.clear();
visualObj.updateHighlighting();
});
Expand Down
7 changes: 4 additions & 3 deletions src/D3 Plotting Functions/drawSummaryTable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,8 +105,8 @@ export default function drawSummaryTable(selection: divBaseType, visualObj: Visu
if (visualObj.host.hostCapabilities.allowInteractions) {
visualObj.selectionManager
.select(d.identity, event.ctrlKey || event.metaKey)
.then(() => {
visualObj.updateHighlighting();
.then(() =>{
visualObj.updateHighlighting()
});
event.stopPropagation();
}
Expand Down Expand Up @@ -174,7 +174,8 @@ export default function drawSummaryTable(selection: divBaseType, visualObj: Visu
.style("border-width", `${tableAesthetics.table_body_border_width}px`)
.style("border-style", tableAesthetics.table_body_border_style)
.style("border-color", tableAesthetics.table_body_border_colour)
.style("padding", `${tableAesthetics.table_body_text_padding}px`);
.style("padding", `${tableAesthetics.table_body_text_padding}px`)
.style("opacity", "inherit");

if (idx === 0) {
currNode.style("border-left", "inherit");
Expand Down
15 changes: 7 additions & 8 deletions src/Functions/extractInputData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ export default function extractInputData(inputView: DataViewCategorical,
inputSettings: defaultSettingsType,
derivedSettings: derivedSettingsClass,
validationMessages: string[][],
first_idx: number, last_idx: number): dataObject {
idxs: number[]): dataObject {
const numerators: number[] = extractDataColumn<number[]>(inputView, "numerators", inputSettings);
const denominators: number[] = extractDataColumn<number[]>(inputView, "denominators", inputSettings);
const xbar_sds: number[] = extractDataColumn<number[]>(inputView, "xbar_sds", inputSettings);
Expand All @@ -66,8 +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, first_idx, last_idx);

const inputValidStatus: ValidationT = validateInputData(keys, numerators, denominators, xbar_sds, groupings, derivedSettings.chart_type_props, idxs);
if (inputValidStatus.status !== 0) {
return invalidInputData(inputValidStatus);
}
Expand All @@ -78,8 +77,8 @@ export default function extractInputData(inputView: DataViewCategorical,
const groupVarName: string = inputView.categories[0].source.displayName;
const settingsMessages = validationMessages;
let valid_x: number = 0;
for (let i: number = first_idx; i <= last_idx; i++) {
if (inputValidStatus.messages[i - first_idx] === "") {
idxs.forEach((i, idx) => {
if (inputValidStatus.messages[idx] === "") {
valid_ids.push(i);
valid_keys.push({ x: valid_x, id: i, label: keys[i] })
valid_x += 1;
Expand All @@ -92,9 +91,9 @@ export default function extractInputData(inputView: DataViewCategorical,
);
}
} else {
removalMessages.push(`${groupVarName} ${keys[i]} removed due to: ${inputValidStatus.messages[i - first_idx]}.`)
removalMessages.push(`${groupVarName} ${keys[i]} removed due to: ${inputValidStatus.messages[idx]}.`)
}
}
})

const valid_groupings: string[] = extractValues(groupings, valid_ids);
const groupingIndexes: number[] = new Array<number>();
Expand Down Expand Up @@ -132,7 +131,7 @@ export default function extractInputData(inputView: DataViewCategorical,
xbar_sds: extractValues(xbar_sds, valid_ids),
outliers_in_limits: false,
},
spcSettings: spcSettings[first_idx],
spcSettings: spcSettings[idxs[0]],
tooltips: extractValues(tooltips, valid_ids),
highlights: curr_highlights,
anyHighlights: curr_highlights.filter(d => !isNullOrUndefined(d)).length > 0,
Expand Down
4 changes: 2 additions & 2 deletions src/Functions/validateInputData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,11 +64,11 @@ export default function validateInputData(keys: string[],
xbar_sds: number[],
groupings: string[],
chart_type_props: derivedSettingsClass["chart_type_props"],
first_idx: number, last_idx: number): { status: number, messages: string[], error?: string } {
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>();
for (let i = first_idx; i <= last_idx; i++) {
for (const i of idxs) {
const validation = validateInputDataImpl(keys[i], numerators?.[i], denominators?.[i], xbar_sds?.[i], groupings?.[i], chart_type_props);
messages.push(validation.message);
all_status.push(validation.type);
Expand Down
47 changes: 32 additions & 15 deletions src/visual.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,12 +39,30 @@ 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]);
this.viewModel.inputSettings.update(options.dataViews[0], idx_per_indicator);
if (this.viewModel.inputSettings.validationStatus.error !== "") {
this.processVisualError(options,
this.viewModel.inputSettings.validationStatus.error,
Expand All @@ -59,8 +77,7 @@ export class Visual implements powerbi.extensibility.IVisual {
return;
}

this.viewModel.update(options, this.host);

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,
Expand Down Expand Up @@ -146,25 +163,25 @@ export class Visual implements powerbi.extensibility.IVisual {
dotsSelection.style("fill-opacity", defaultOpacity);
tableSelection.style("opacity", defaultOpacity);
if (anyHighlights || (allSelectionIDs.length > 0) || anyHighlightsGrouped) {
const dotsNodes = dotsSelection.nodes();
const tableNodes = tableSelection.nodes();
// If either the table or dots haven't been initialised
// there will be no nodes to update styling for or iterate over
const maxNodes = Math.max(dotsNodes.length, tableNodes.length);

for (let i = 0; i < maxNodes; i++) {
const currentDotNode = dotsNodes?.[i];
const currentTableNode = tableNodes?.[i];
const dot: plotData = d3.select(currentDotNode ?? currentTableNode).datum() as plotData;
dotsSelection.nodes().forEach(currentDotNode => {
const dot: plotData = d3.select(currentDotNode).datum() as plotData;
const currentPointSelected: boolean = allSelectionIDs.some((currentSelectionId: ISelectionId) => {
return currentSelectionId.includes(dot.identity);
});
const currentPointHighlighted: boolean = dot.highlighted;
const newDotOpacity: number = (currentPointSelected || currentPointHighlighted) ? dot.aesthetics.opacity : dot.aesthetics.opacity_unselected;
const newTableOpacity: number = (currentPointSelected || currentPointHighlighted) ? dot.aesthetics["table_opacity"] : dot.aesthetics["table_opacity_unselected"];
d3.select(currentDotNode).style("fill-opacity", newDotOpacity);
})

tableSelection.nodes().forEach(currentTableNode => {
const dot: plotData = d3.select(currentTableNode).datum() as plotData;
const currentPointSelected: boolean = allSelectionIDs.some((currentSelectionId: ISelectionId) => {
return currentSelectionId.includes(dot.identity);
});
const currentPointHighlighted: boolean = dot.highlighted;
const newTableOpacity: number = (currentPointSelected || currentPointHighlighted) ? dot.aesthetics["table_opacity"] : dot.aesthetics["table_opacity_unselected"];
d3.select(currentTableNode).style("opacity", newTableOpacity);
}
})
}
}

Expand Down

0 comments on commit 1752020

Please sign in to comment.