Skip to content

Commit

Permalink
Add line markers for labels, support conditional formatting (#358)
Browse files Browse the repository at this point in the history
  • Loading branch information
andrjohns authored Dec 11, 2024
1 parent 5b02edc commit ca2705d
Show file tree
Hide file tree
Showing 7 changed files with 99 additions and 53 deletions.
22 changes: 21 additions & 1 deletion capabilities.json
Original file line number Diff line number Diff line change
Expand Up @@ -1328,7 +1328,7 @@
}
}
},
"label_options" : {
"labels" : {
"displayName" : "Value Label Options",
"properties" : {
"show_labels": {
Expand Down Expand Up @@ -1389,6 +1389,26 @@
"label_line_max_length": {
"displayName": "Max Connecting Line Length (px)",
"type": { "numeric": true }
},
"label_marker_show": {
"displayName": "Show Line Markers",
"type" : { "bool" : true }
},
"label_marker_offset": {
"displayName": "Marker Offset from Value (px)",
"type": { "numeric": true }
},
"label_marker_size": {
"displayName": "Marker Size",
"type": { "numeric": true }
},
"label_marker_colour":{
"displayName": "Marker Fill Colour",
"type": { "fill": { "solid": { "color": true } } }
},
"label_marker_outline_colour":{
"displayName": "Marker Outline Colour",
"type": { "fill": { "solid": { "color": true } } }
}
}
}
Expand Down
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.19",
"version":"1.4.4.20",
"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: 2 additions & 0 deletions src/Classes/viewModelClass.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ export type plotData = {
tooltip: VisualTooltipDataItem[];
label: {
text_value: string,
aesthetics: defaultSettingsType["labels"],
x: number,
y: number
};
Expand Down Expand Up @@ -599,6 +600,7 @@ export default class viewModelClass {
this.inputSettings.settings, this.inputSettings.derivedSettings),
label: {
text_value: this.inputData.labels?.[index],
aesthetics: this.inputData.label_formatting[index],
x: null,
y: null
}
Expand Down
2 changes: 1 addition & 1 deletion src/D3 Plotting Functions/D3 Modules/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
export { select, selectAll, type Selection, type BaseType } from "d3-selection";
export { groups, leastIndex } from "d3-array";
export { line } from "d3-shape"
export { line, symbol, symbolTriangle } from "d3-shape"
export { type Axis, axisBottom, axisLeft } from "d3-axis"
export { type ScaleLinear, scaleLinear, type NumberValue } from "d3-scale"
export { drag } from "d3-drag"
111 changes: 63 additions & 48 deletions src/D3 Plotting Functions/drawValueLabels.ts
Original file line number Diff line number Diff line change
@@ -1,68 +1,82 @@
import * as d3 from "./D3 Modules";
import type { svgBaseType, Visual } from "../visual";
import { plotData } from "../Classes";
import type { plotData } from "../Classes";

const degToRad: number = Math.PI / 180;

const labelFormatting = function(selection: d3.Selection<d3.BaseType, plotData, d3.BaseType, unknown>, visualObj: Visual) {
let label_options = visualObj.viewModel.inputSettings.settings.label_options;
const calcCoord = function(axis: "x" | "y", d: plotData, visualObj: Visual) {
const plotHeight: number = visualObj.viewModel.svgHeight;
const xAxisHeight: number = plotHeight - visualObj.viewModel.plotProperties.yAxis.start_padding;
const label_position: string = visualObj.viewModel.inputSettings.settings.label_options.label_position;
let y_offset: number = visualObj.viewModel.inputSettings.settings.label_options.label_y_offset;
let line_offset: number = visualObj.viewModel.inputSettings.settings.label_options.label_line_offset;
line_offset = label_position === "top" ? line_offset : -(line_offset + visualObj.viewModel.inputSettings.settings.label_options.label_size / 2);
const label_position: string = d.label.aesthetics.label_position;
let y_offset: number = d.label.aesthetics.label_y_offset;
const label_height: number = label_position === "top" ? (0 + y_offset) : (xAxisHeight - y_offset);
const theta: number = visualObj.viewModel.inputSettings.settings.label_options.label_angle_offset;
const max_line_length: number = visualObj.viewModel.inputSettings.settings.label_options.label_line_max_length;

const y: number = visualObj.viewModel.plotProperties.yScale(d.value);

let side_length: number = label_position === "top" ? (y - label_height) : (label_height - y);
side_length = Math.min(side_length, d.label.aesthetics.label_line_max_length);
if (axis === "x") {
const theta: number = d.label.aesthetics.label_angle_offset;
const x: number = visualObj.viewModel.plotProperties.xScale(d.x);
// When angle offset provided, calculate adjusted x coordinate using law of sines
const theta_2: number = 180 - (90 + theta);

return d.label.x ?? Math.sin(theta * degToRad) * side_length / Math.sin(theta_2 * degToRad) + x;
} else {
return d.label.y ?? y + (label_position === "top" ? -side_length : side_length);
}

}

const labelFormatting = function(selection: d3.Selection<d3.BaseType, plotData, d3.BaseType, unknown>, visualObj: Visual) {
selection.select("text")
.text(d => d.label.text_value)
.attr("x", d => {
const y: number = visualObj.viewModel.plotProperties.yScale(d.value);
let side_length: number = label_position === "top" ? (y - label_height) : (label_height - y);
side_length = Math.min(side_length, max_line_length);
const x: number = visualObj.viewModel.plotProperties.xScale(d.x);
// When angle offset provided, calculate adjusted x coordinate using law of sines
const theta_2: number = 180 - (90 + theta);
return d.label.x ?? Math.sin(theta * degToRad) * side_length / Math.sin(theta_2 * degToRad) + x;
})
.attr("y", d => {
const y: number = visualObj.viewModel.plotProperties.yScale(d.value);
let side_length: number = label_position === "top" ? (y - label_height) : (label_height - y);
side_length = Math.min(side_length, max_line_length);
return d.label.y ?? y + (label_position === "top" ? -side_length : side_length);
})
.attr("x", d => calcCoord("x", d, visualObj))
.attr("y", d => calcCoord("y", d, visualObj))
.style("text-anchor", "middle")
.style("font-size", `${label_options.label_size}px`)
.style("font-family", label_options.label_font)
.style("fill", label_options.label_colour);
.style("font-size", d => `${d.label.aesthetics.label_size}px`)
.style("font-family", d => d.label.aesthetics.label_font)
.style("fill", d => d.label.aesthetics.label_colour);

selection.select("line")
.attr("x1", d => {
const y: number = visualObj.viewModel.plotProperties.yScale(d.value);
let side_length: number = label_position === "top" ? (y - label_height) : (label_height - y);
side_length = Math.min(side_length, max_line_length);
const x: number = visualObj.viewModel.plotProperties.xScale(d.x);
// When angle offset provided, calculate adjusted x coordinate using law of sines
const theta_2: number = 180 - (90 + theta);
return d.label.x ?? Math.sin(theta * degToRad) * side_length / Math.sin(theta_2 * degToRad) + x;
})
.attr("x1", d => calcCoord("x", d, visualObj))
.attr("y1", d => {
const y: number = visualObj.viewModel.plotProperties.yScale(d.value);
let side_length: number = label_position === "top" ? (y - label_height) : (label_height - y);
side_length = Math.min(side_length, max_line_length);
return (d.label.y ?? y + (label_position === "top" ? -side_length : side_length)) + line_offset;
const label_position: string = d.label.aesthetics.label_position;
let line_offset: number = d.label.aesthetics.label_line_offset;
line_offset = label_position === "top" ? line_offset : -(line_offset + d.label.aesthetics.label_size / 2);
return calcCoord("y", d, visualObj) + line_offset
})
.attr("x2", d => visualObj.viewModel.plotProperties.xScale(d.x))
.attr("y2", d => visualObj.viewModel.plotProperties.yScale(d.value))
.style("stroke", visualObj.viewModel.inputSettings.settings.label_options.label_line_colour)
.style("stroke-width", d => (d.label.text_value ?? "") === "" ? 0 : visualObj.viewModel.inputSettings.settings.label_options.label_line_width)
.style("stroke-dasharray", visualObj.viewModel.inputSettings.settings.label_options.label_line_type);
.attr("y2", d => {
const label_position: string = d.label.aesthetics.label_position;
let marker_offset: number = d.label.aesthetics.label_marker_offset + d.label.aesthetics.label_size / 2;
marker_offset = label_position === "top" ? -marker_offset : marker_offset;
return visualObj.viewModel.plotProperties.yScale(d.value) + marker_offset
})
.style("stroke", visualObj.viewModel.inputSettings.settings.labels.label_line_colour)
.style("stroke-width", d => (d.label.text_value ?? "") === "" ? 0 : visualObj.viewModel.inputSettings.settings.labels.label_line_width)
.style("stroke-dasharray", visualObj.viewModel.inputSettings.settings.labels.label_line_type);

selection.select("path")
.attr("d", d => {
const show_marker: boolean = d.label.aesthetics.label_marker_show && (d.label.text_value ?? "") !== "";
const marker_size: number = show_marker ? Math.pow(d.label.aesthetics.label_marker_size, 2) : 0;
return d3.symbol().type(d3.symbolTriangle).size(marker_size)()
})
.attr("transform", d => {
const label_position: string = d.label.aesthetics.label_position;
const x: number = visualObj.viewModel.plotProperties.xScale(d.x);
const y: number = visualObj.viewModel.plotProperties.yScale(d.value);
let marker_offset: number = d.label.aesthetics.label_marker_offset + d.label.aesthetics.label_size / 2;
marker_offset = label_position === "top" ? -marker_offset : marker_offset;
return `translate(${x},${y + marker_offset}) rotate(${label_position === "top" ? 180 : 0})`;
})
.style("fill", d => d.label.aesthetics.label_marker_colour)
.style("stroke", d => d.label.aesthetics.label_marker_outline_colour)
}

export default function drawLabels(selection: svgBaseType, visualObj: Visual) {
if (!visualObj.viewModel.inputSettings.settings.label_options.show_labels) {
if (!visualObj.viewModel.inputSettings.settings.labels.show_labels) {
selection.select(".text-labels").remove();
return;
}
Expand All @@ -71,9 +85,9 @@ export default function drawLabels(selection: svgBaseType, visualObj: Visual) {
selection.append("g").classed("text-labels", true);
}

const label_position: string = visualObj.viewModel.inputSettings.settings.label_options.label_position;
let line_offset: number = visualObj.viewModel.inputSettings.settings.label_options.label_line_offset;
line_offset = label_position === "top" ? line_offset : -(line_offset + visualObj.viewModel.inputSettings.settings.label_options.label_size / 2);
const label_position: string = visualObj.viewModel.inputSettings.settings.labels.label_position;
let line_offset: number = visualObj.viewModel.inputSettings.settings.labels.label_line_offset;
line_offset = label_position === "top" ? line_offset : -(line_offset + visualObj.viewModel.inputSettings.settings.labels.label_size / 2);

const dragFun = d3.drag().on("drag", function(e) {
e.subject.label.x = e.sourceEvent.x;
Expand All @@ -97,6 +111,7 @@ export default function drawLabels(selection: svgBaseType, visualObj: Visual) {
let grp = enter.append("g").classed("text-group-inner", true)
grp.append("text");
grp.append("line");
grp.append("path")

grp.call(labelFormatting, visualObj)
.call(dragFun);
Expand Down
4 changes: 4 additions & 0 deletions src/Functions/extractInputData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export type dataObject = {
groupings: string[];
groupingIndexes: number[];
scatter_formatting: defaultSettingsType["scatter"][];
label_formatting: defaultSettingsType["labels"][];
tooltips: VisualTooltipDataItem[][];
labels: string[];
warningMessage: string;
Expand All @@ -35,6 +36,7 @@ function invalidInputData(inputValidStatus: ValidationT): dataObject {
groupings: null,
groupingIndexes: null,
scatter_formatting: null,
label_formatting: null,
tooltips: null,
labels: null,
warningMessage: inputValidStatus.error,
Expand All @@ -59,6 +61,7 @@ export default function extractInputData(inputView: DataViewCategorical,
const labels: string[] = extractDataColumn<string[]>(inputView, "labels", inputSettings, idxs);
const highlights: powerbi.PrimitiveValue[] = idxs.map(d => inputView?.values?.[0]?.highlights?.[d]);
let scatter_cond = extractConditionalFormatting<defaultSettingsType["scatter"]>(inputView, "scatter", inputSettings, idxs)?.values;
let labels_cond = extractConditionalFormatting<defaultSettingsType["labels"]>(inputView, "labels", inputSettings, idxs)?.values;
let alt_targets: number[] = extractConditionalFormatting<defaultSettingsType["lines"]>(inputView, "lines", inputSettings, idxs)
?.values
.map(d => inputSettings.lines.show_alt_target ? d.alt_target : null);
Expand Down Expand Up @@ -153,6 +156,7 @@ export default function extractInputData(inputView: DataViewCategorical,
groupings: valid_groupings,
groupingIndexes: groupingIndexes,
scatter_formatting: extractValues(scatter_cond, valid_ids),
label_formatting: extractValues(labels_cond, valid_ids),
warningMessage: removalMessages.length > 0 ? removalMessages.join("\n") : "",
alt_targets: valid_alt_targets,
speclimits_lower: extractValues(speclimits_lower, valid_ids),
Expand Down
9 changes: 7 additions & 2 deletions src/defaultSettings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -264,7 +264,7 @@ const defaultSettings = {
download_options: {
show_button: { default: false }
},
label_options: {
labels: {
show_labels: { default: true },
label_position: { default: "top", valid: ["top", "bottom"] },
label_y_offset: { default: 20 },
Expand All @@ -276,7 +276,12 @@ const defaultSettings = {
label_line_colour: colourOptions.standard,
label_line_width: { default: 1, valid: lineOptions.width.valid },
label_line_type: { default: "10 0", valid: lineOptions.type.valid },
label_line_max_length: { default: 1000, valid: { numberRange: { min: 0, max: 10000 }}}
label_line_max_length: { default: 1000, valid: { numberRange: { min: 0, max: 10000 }}},
label_marker_show: { default: true },
label_marker_offset: { default: 5 },
label_marker_size: { default: 3, valid: { numberRange: { min: 0, max: 100 }}},
label_marker_colour: colourOptions.standard,
label_marker_outline_colour: colourOptions.standard
}
};

Expand Down

0 comments on commit ca2705d

Please sign in to comment.