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

Add limit truncation options, toggle lines in tooltips #142

Merged
merged 2 commits into from
Feb 4, 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
8 changes: 8 additions & 0 deletions capabilities.json
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,14 @@
{ "displayName" : "Square-Root", "value" : "sqrt" }
]
}
},
"ll_truncate": {
"displayName": "Truncate Lower Limits at:",
"type": { "numeric": true }
},
"ul_truncate": {
"displayName": "Truncate Upper Limits at:",
"type": { "numeric": true }
}
}
},
Expand Down
58 changes: 29 additions & 29 deletions src/Classes/viewModelClass.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ type IVisualHost = powerbi.extensibility.visual.IVisualHost;
type VisualTooltipDataItem = powerbi.extensibility.VisualTooltipDataItem;
type ISelectionId = powerbi.visuals.ISelectionId;
import { chartClass, settingsClass, type limitData, plotPropertiesClass, type defaultSettingsType } from "../Classes"
import { extractInputData, buildTooltip, type dataObject, checkFlagDirection } from "../Functions";
import { extractInputData, buildTooltip, type dataObject, checkFlagDirection, truncate, truncateInputs, multiply } from "../Functions";
import * as chartObjects from "../Chart Types"
import getTransformation from "../Funnel Calculations/getTransformation";
import two_sigma from "../Outlier Flagging/two_sigma"
Expand Down Expand Up @@ -60,6 +60,7 @@ export default class viewModelClass {

this.chartBase = new chartObjects[chart_type](this.inputData, this.inputSettings);
this.calculatedLimits = this.chartBase.getLimits();
this.scaleAndTruncateLimits();

this.initialisePlotData(host);
this.initialiseGroupedLines();
Expand All @@ -79,9 +80,7 @@ export default class viewModelClass {
this.plotPoints = new Array<plotData>();
const transform_text: string = this.inputSettings.settings.funnel.transformation;
const transform: (x: number) => number = getTransformation(transform_text);
const target: number = this.chartBase.getTarget({ transformed: false });
const multiplier: number = this.inputSettings.derivedSettings.multiplier;
const data_type: string = this.inputSettings.settings.funnel.chart_type;
const flag_two_sigma: boolean = this.inputSettings.settings.outliers.two_sigma;
const flag_three_sigma: boolean = this.inputSettings.settings.outliers.three_sigma;

Expand All @@ -90,8 +89,7 @@ export default class viewModelClass {
const numerator: number = this.inputData.numerators[i];
const denominator: number = this.inputData.denominators[i];
const ratio: number = (numerator / denominator);
const limits_impl: limitData[] = this.calculatedLimits.filter(d => d.denominators === denominator && d.ll99 !== null && d.ul99 !== null);
const limits: limitData = limits_impl.length > 0 ? limits_impl[0] : this.calculatedLimits.filter(d => d.denominators === denominator)[0];
const limits: limitData = this.calculatedLimits.filter(d => d.denominators === denominator && d.ll99 !== null && d.ul99 !== null)[0];
const aesthetics: defaultSettingsType["scatter"] = this.inputData.scatter_formatting[i]
const two_sigma_outlier: string = flag_two_sigma ? two_sigma(ratio, limits) : "none";
const three_sigma_outlier: string = flag_three_sigma ? three_sigma(ratio, limits) : "none";
Expand Down Expand Up @@ -123,32 +121,19 @@ export default class viewModelClass {
.withCategory(this.inputData.categories, original_index)
.createSelectionId(),
highlighted: this.inputData.highlights?.[i] != null,
tooltip: buildTooltip({
group: category,
numerator: numerator,
denominator: denominator,
target: target,
transform_text: transform_text,
transform: transform,
limits: limits,
data_type: data_type,
multiplier: multiplier,
two_sigma_outlier: two_sigma_outlier !== "none",
three_sigma_outlier: three_sigma_outlier !== "none",
sig_figs: this.inputSettings.settings.funnel.sig_figs,
userTooltips: this.inputData.tooltips[i]
})
tooltip: buildTooltip(
i,
this.calculatedLimits,
{ two_sigma: two_sigma_outlier !== "none", three_sigma: three_sigma_outlier !== "none" },
this.inputData,
this.inputSettings.settings,
this.inputSettings.derivedSettings
)
})
}
}

initialiseGroupedLines(): void {
const multiplier: number = this.inputSettings.derivedSettings.multiplier;
const transform: (x: number) => number = getTransformation(this.inputSettings.settings.funnel.transformation);

const target: number = this.chartBase.getTarget({ transformed: false });
const alt_target: number = this.inputSettings.settings.lines.alt_target;

const labels: string[] = new Array<string>();
if (this.inputSettings.settings.lines.show_target) {
labels.push("target");
Expand All @@ -168,16 +153,31 @@ export default class viewModelClass {

const formattedLines: lineData[] = new Array<lineData>();
this.calculatedLimits.forEach(limits => {
limits.target = target;
limits.alt_target = alt_target;
labels.forEach(label => {
formattedLines.push({
x: limits.denominators,
line_value: limits[label] ? transform(limits[label] * multiplier) : null,
line_value: limits?.[label],
group: label
})
})
})
this.groupedLines = d3.groups(formattedLines, d => d.group);
}

scaleAndTruncateLimits(): void {
// Scale limits using provided multiplier
const multiplier: number = this.inputSettings.derivedSettings.multiplier;
const transform: (x: number) => number = getTransformation(this.inputSettings.settings.funnel.transformation);

const limits: truncateInputs = {
lower: this.inputSettings.settings.funnel.ll_truncate,
upper: this.inputSettings.settings.funnel.ul_truncate
};
this.calculatedLimits.forEach(limit => {
["target", "ll99", "ll95", "ll68", "ul68", "ul95", "ul99"].forEach(type => {
limit[type] = truncate(transform(multiply(limit[type], multiplier)), limits)
})
})
}
}

124 changes: 71 additions & 53 deletions src/Functions/buildTooltip.ts
Original file line number Diff line number Diff line change
@@ -1,33 +1,31 @@
import type powerbi from "powerbi-visuals-api";
type VisualTooltipDataItem = powerbi.extensibility.VisualTooltipDataItem;
import type { limitData } from "../Classes";
import type { limitData, defaultSettingsType, derivedSettingsClass } from "../Classes";
import { type dataObject } from "../Functions";
import getTransformation from "../Funnel Calculations/getTransformation";

type tooltipArgs = {
group: string,
numerator: number,
denominator: number,
target: number,
transform_text: string,
transform: (x: number) => number,
limits: limitData,
data_type: string,
multiplier: number,
two_sigma_outlier: boolean,
three_sigma_outlier: boolean,
sig_figs: number,
userTooltips: VisualTooltipDataItem[]
}
export default function buildTooltip(index: number,
calculatedLimits: limitData[],
outliers: { two_sigma: boolean, three_sigma: boolean },
inputData: dataObject,
inputSettings: defaultSettingsType,
derivedSettings: derivedSettingsClass): VisualTooltipDataItem[] {
const data_type: string = inputSettings.funnel.chart_type;
const multiplier: number = derivedSettings.multiplier;
const transform_text: string = inputSettings.funnel.transformation;
const transform: (x: number) => number = getTransformation(transform_text);

const group: string = inputData.keys[index].label;
const numerator: number = inputData.numerators[index];
const denominator: number = inputData.denominators[index];

export default function buildTooltip(args: tooltipArgs): VisualTooltipDataItem[] {
const numerator: number = args.numerator;
const denominator: number = args.denominator;
const multiplier: number = args.multiplier;
const ratio: number = args.transform((numerator / denominator) * multiplier);
const ul99: number = args.transform(args.limits.ul99 * multiplier);
const ll99: number = args.transform(args.limits.ll99 * multiplier);
const target: number = args.transform(args.target * multiplier);
const limits: limitData = calculatedLimits.filter(d => d.denominators === denominator && d.ll99 !== null && d.ul99 !== null)[0];

const prop_labels: boolean = (args.data_type === "PR" && args.multiplier === 100);
const ratio: number = transform((numerator / denominator) * multiplier);
const suffix: string = derivedSettings.percentLabels ? "%" : "";

const prop_labels: boolean = derivedSettings.percentLabels;
const sig_figs: number = inputSettings.funnel.sig_figs;
const valueLabel: Record<string, string> = {
"PR" : "Proportion",
"SR" : "Standardised Ratio",
Expand All @@ -37,45 +35,65 @@ export default function buildTooltip(args: tooltipArgs): VisualTooltipDataItem[]
const tooltip: VisualTooltipDataItem[] = new Array<VisualTooltipDataItem>();
tooltip.push({
displayName: "Group",
value: args.group
value: group
});
tooltip.push({
displayName: valueLabel[args.data_type],
value: prop_labels ? ratio.toFixed(args.sig_figs) + "%" : ratio.toFixed(args.sig_figs)
})
tooltip.push({
displayName: "Numerator",
value: (args.numerator).toFixed(args.sig_figs)
})
tooltip.push({
displayName: "Denominator",
value: (args.denominator).toFixed(args.sig_figs)
displayName: valueLabel[data_type],
value: ratio.toFixed(sig_figs) + suffix
})
tooltip.push({
displayName: "Upper 99% Limit",
value: prop_labels ? ul99.toFixed(args.sig_figs) + "%" : ul99.toFixed(args.sig_figs)
})
tooltip.push({
displayName: "Centerline",
value: prop_labels ? target.toFixed(args.sig_figs) + "%" : target.toFixed(args.sig_figs)
if(numerator || !(numerator === null || numerator === undefined)) {
tooltip.push({
displayName: "Numerator",
value: (numerator).toFixed(prop_labels ? 0 : sig_figs)
})
}
if(denominator || !(denominator === null || denominator === undefined)) {
tooltip.push({
displayName: "Denominator",
value: (denominator).toFixed(prop_labels ? 0 : sig_figs)
})
}
["68", "95", "99"].forEach(limit => {
if (inputSettings.lines[`ttip_show_${limit}`] && inputSettings.lines[`show_${limit}`]) {
tooltip.push({
displayName: `Upper ${inputSettings.lines[`ttip_label_${limit}`]}`,
value: (limits[`ul${limit}`]).toFixed(sig_figs) + suffix
})
}
})
tooltip.push({
displayName: "Lower 99% Limit",
value: prop_labels ? ll99.toFixed(args.sig_figs) + "%" : ll99.toFixed(args.sig_figs)
if (inputSettings.lines.show_target && inputSettings.lines.ttip_show_target) {
tooltip.push({
displayName: inputSettings.lines.ttip_label_target,
value: (limits.target).toFixed(sig_figs) + suffix
})
}
if (inputSettings.lines.show_alt_target && inputSettings.lines.ttip_show_alt_target && !(limits.alt_target === null || limits.alt_target === undefined)) {
tooltip.push({
displayName: inputSettings.lines.ttip_label_alt_target,
value: (limits.alt_target).toFixed(sig_figs) + suffix
})
}
["68", "95", "99"].forEach(limit => {
if (inputSettings.lines[`ttip_show_${limit}`] && inputSettings.lines[`show_${limit}`]) {
tooltip.push({
displayName: `Lower ${inputSettings.lines[`ttip_label_${limit}`]}`,
value: (limits[`ll${limit}`]).toFixed(sig_figs) + suffix
})
}
})

if (args.transform_text !== "none") {
if (transform_text !== "none") {
tooltip.push({
displayName: "Plot Scaling",
value: args.transform_text
value: transform_text
})
}
if (args.two_sigma_outlier || args.three_sigma_outlier) {
if (outliers.two_sigma || outliers.three_sigma) {
const patterns: string[] = new Array<string>();
if (args.three_sigma_outlier) {
if (outliers.three_sigma) {
patterns.push("Three Sigma Outlier")
}
if (args.two_sigma_outlier) {
if (outliers.two_sigma) {
patterns.push("Two Sigma Outlier")
}
tooltip.push({
Expand All @@ -84,8 +102,8 @@ export default function buildTooltip(args: tooltipArgs): VisualTooltipDataItem[]
})
}

if (args?.userTooltips?.length > 0) {
args.userTooltips.forEach(customTooltip => tooltip.push(customTooltip));
if (inputData.tooltips.length > 0) {
inputData.tooltips[index].forEach(customTooltip => tooltip.push(customTooltip));
}

return tooltip;
Expand Down
1 change: 1 addition & 0 deletions src/Functions/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,4 @@ export { default as validateDataView } from "./validateDataView"
export { default as validateInputData } from "./validateInputData"
export { default as formatPrimitiveValue } from "./formatPrimitiveValue"
export { default as checkFlagDirection } from "./checkFlagDirection"
export { default as truncate, truncateInputs } from "./truncate"
24 changes: 24 additions & 0 deletions src/Functions/truncate.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { broadcast_binary } from "../Functions"

export type truncateInputs = { lower?: number, upper?: number };

/**
* Truncates a number or array of numbers within specified limits.
* @param val The number or array of numbers to be truncated.
* @param limits The limits for truncation.
* @returns The truncated number or array of numbers.
*/
const truncate = broadcast_binary(
(val: number, limits: truncateInputs): number => {
let rtn: number = val;
if (limits.lower || limits.lower == 0) {
rtn = (rtn < limits.lower ? limits.lower : rtn)
}
if (limits.upper) {
rtn = (rtn > limits.upper ? limits.upper : rtn);
}
return rtn;
}
)

export default truncate;
4 changes: 3 additions & 1 deletion src/defaultSettings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@ const defaultSettings = {
od_adjust: "no",
multiplier: 1,
sig_figs: 2,
transformation: "none"
transformation: "none",
ll_truncate: <number>null,
ul_truncate: <number>null,
},
scatter: {
use_group_text: false,
Expand Down