Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Rework dataview mapping to fix input aggregation of conditional formatting #316

Merged
merged 1 commit into from
Aug 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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