diff --git a/capabilities.json b/capabilities.json index d98c3d6..843cd82 100644 --- a/capabilities.json +++ b/capabilities.json @@ -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": { @@ -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" } } - ] - } - } - } }] } diff --git a/pbiviz.json b/pbiviz.json index 055c250..a1bb80f 100644 --- a/pbiviz.json +++ b/pbiviz.json @@ -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" diff --git a/src/Classes/settingsClass.ts b/src/Classes/settingsClass.ts index 308e62d..40ebb30 100644 --- a/src/Classes/settingsClass.ts +++ b/src/Classes/settingsClass.ts @@ -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(), 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(); + const is_grouped: boolean = inputView.categorical.categories.some(d => d.source.roles.indicator); this.settingsGrouped = new Array(); 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 = extractConditionalFormatting(inputView?.categorical, settingGroup, this.settings); @@ -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"] }) } @@ -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"]] })) diff --git a/src/Classes/viewModelClass.ts b/src/Classes/viewModelClass.ts index cbc818c..8c558f4 100644 --- a/src/Classes/viewModelClass.ts +++ b/src/Classes/viewModelClass.ts @@ -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, @@ -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(); this.inputDataGrouped = new Array(); this.groupStartEndIndexesGrouped = new Array(); this.controlLimitsGrouped = new Array(); this.outliersGrouped = new Array(); this.identitiesGrouped = new Array(); - options.dataViews[0].categorical.values.grouped().forEach((d, idx) => { - (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(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() : this.getGroupingIndexes(inpData); const limits: controlLimitsObject = invalidData ? null : this.calculateLimits(inpData, groupStartEndIndexes, this.inputSettings.settingsGrouped[idx]); @@ -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(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); @@ -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); diff --git a/src/D3 Plotting Functions/drawDots.ts b/src/D3 Plotting Functions/drawDots.ts index fa7a704..487241a 100644 --- a/src/D3 Plotting Functions/drawDots.ts +++ b/src/D3 Plotting Functions/drawDots.ts @@ -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(); } @@ -74,6 +77,7 @@ export default function drawDots(selection: svgBaseType, visualObj: Visual) { }); selection.on('click', () => { + console.log("b") visualObj.selectionManager.clear(); visualObj.updateHighlighting(); }); diff --git a/src/D3 Plotting Functions/drawSummaryTable.ts b/src/D3 Plotting Functions/drawSummaryTable.ts index 3789d5c..987a5df 100644 --- a/src/D3 Plotting Functions/drawSummaryTable.ts +++ b/src/D3 Plotting Functions/drawSummaryTable.ts @@ -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(); } @@ -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"); diff --git a/src/Functions/extractInputData.ts b/src/Functions/extractInputData.ts index e412c81..f65dd9f 100644 --- a/src/Functions/extractInputData.ts +++ b/src/Functions/extractInputData.ts @@ -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(inputView, "numerators", inputSettings); const denominators: number[] = extractDataColumn(inputView, "denominators", inputSettings); const xbar_sds: number[] = extractDataColumn(inputView, "xbar_sds", inputSettings); @@ -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(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); } @@ -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; @@ -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(); @@ -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, diff --git a/src/Functions/validateInputData.ts b/src/Functions/validateInputData.ts index 9d5ffd1..33cc359 100644 --- a/src/Functions/validateInputData.ts +++ b/src/Functions/validateInputData.ts @@ -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(); let all_status: ValidationFailTypes[] = new Array(); - 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); diff --git a/src/visual.ts b/src/visual.ts index ed90f66..c3eedab 100644 --- a/src/visual.ts +++ b/src/visual.ts @@ -39,12 +39,30 @@ export class Visual implements powerbi.extensibility.IVisual { } public update(options: VisualUpdateOptions): void { + const idx_per_indicator = new Array(); + const indicator_names = new Array(); + const indicator_idx = options.dataViews[0].categorical.categories?.findIndex(d => d.source.roles.indicator); + + if (indicator_idx === -1) { + idx_per_indicator.push(options.dataViews[0].categorical.categories[0].values.map((_, i) => i)); + } else { + const indicator_vals = options.dataViews[0].categorical.categories?.[indicator_idx]?.values; + for (let i = 0; i < indicator_vals.length; i++) { + const indicator_name = indicator_vals[i].toString(); + if (indicator_names.includes(indicator_name)) { + idx_per_indicator[indicator_names.indexOf(indicator_name)].push(i); + } else { + indicator_names.push(indicator_name); + idx_per_indicator.push([i]); + } + } + } try { this.host.eventService.renderingStarted(options); // Remove printed error if refreshing after a previous error run this.svg.select(".errormessage").remove(); - this.viewModel.inputSettings.update(options.dataViews[0]); + this.viewModel.inputSettings.update(options.dataViews[0], idx_per_indicator); if (this.viewModel.inputSettings.validationStatus.error !== "") { this.processVisualError(options, this.viewModel.inputSettings.validationStatus.error, @@ -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, @@ -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); - } + }) } }