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

Rotate offset & markers for value label lines around point #366

Merged
merged 3 commits into from
Jan 6, 2025
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
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.20",
"version":"1.4.4.21",
"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
13 changes: 9 additions & 4 deletions src/Classes/viewModelClass.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,8 +74,10 @@ export type plotData = {
label: {
text_value: string,
aesthetics: defaultSettingsType["labels"],
x: number,
y: number
angle: number,
distance: number,
line_offset: number,
marker_offset: number
};
}

Expand Down Expand Up @@ -587,6 +589,7 @@ export default class viewModelClass {
two_in_three: this.outliers.two_in_three[i]
}


this.plotPoints.push({
x: index,
value: this.controlLimits.values[i],
Expand All @@ -601,8 +604,10 @@ export default class viewModelClass {
label: {
text_value: this.inputData.labels?.[index],
aesthetics: this.inputData.label_formatting[index],
x: null,
y: null
angle: null,
distance: null,
line_offset: null,
marker_offset: null
}
})
this.tickLabels.push({x: index, label: this.controlLimits.keys[i].label});
Expand Down
129 changes: 76 additions & 53 deletions src/D3 Plotting Functions/drawValueLabels.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,56 +2,60 @@ import * as d3 from "./D3 Modules";
import type { svgBaseType, Visual } from "../visual";
import type { plotData } from "../Classes";

const degToRad: number = Math.PI / 180;

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 = 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 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) {
// -90 degrees for vertically above, 90 degrees for vertically below
const allData = selection.data();
const initialLabelXY: {x: number, y: number, theta: number, line_offset: number, marker_offset: number}[] = allData.map(d => {
const label_direction_mult: number = d.label.aesthetics.label_position === "top" ? -1 : 1;
const plotHeight: number = visualObj.viewModel.svgHeight;
const xAxisHeight: number = plotHeight - visualObj.viewModel.plotProperties.yAxis.start_padding;
const label_position: string = d.label.aesthetics.label_position;
let y_offset: number = d.label.aesthetics.label_y_offset;
const label_initial: number = label_position === "top" ? (0 + y_offset) : (xAxisHeight - y_offset);
const y: number = visualObj.viewModel.plotProperties.yScale(d.value);
let side_length: number = label_position === "top" ? (y - label_initial) : (label_initial - y);
const x_val = visualObj.viewModel.plotProperties.xScale(d.x);
const y_val = visualObj.viewModel.plotProperties.yScale(d.value);

const theta: number = d.label.angle ?? (d.label.aesthetics.label_angle_offset + label_direction_mult * 90);
side_length = d.label.distance ?? (Math.min(side_length, d.label.aesthetics.label_line_max_length));

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);

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 {x: x_val + side_length * Math.cos(theta * Math.PI / 180),
y: y_val + side_length * Math.sin(theta * Math.PI / 180),
theta: theta,
line_offset: line_offset,
marker_offset: marker_offset
};

})

selection.select("text")
.text(d => d.label.text_value)
.attr("x", d => calcCoord("x", d, visualObj))
.attr("y", d => calcCoord("y", d, visualObj))
.attr("x", (_, i) => initialLabelXY[i].x)
.attr("y", (_, i) => initialLabelXY[i].y)
.style("text-anchor", "middle")
.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 => calcCoord("x", d, visualObj))
.attr("y1", d => {
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("x1", (_, i) => initialLabelXY[i].x)
.attr("y1", (_, i) => initialLabelXY[i].y + initialLabelXY[i].line_offset)
.attr("x2", (d, i) => {
const marker_offset: number = initialLabelXY[i].marker_offset;
const angle: number = initialLabelXY[i].theta - (d.label.aesthetics.label_position === "top" ? 180 : 0);
return visualObj.viewModel.plotProperties.xScale(d.x) + marker_offset * Math.cos(angle * Math.PI / 180);
})
.attr("x2", d => visualObj.viewModel.plotProperties.xScale(d.x))
.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
.attr("y2", (d, i) => {
const marker_offset: number = initialLabelXY[i].marker_offset;
const angle: number = initialLabelXY[i].theta -(d.label.aesthetics.label_position === "top" ? 180 : 0);
return visualObj.viewModel.plotProperties.yScale(d.value) + marker_offset * Math.sin(angle * Math.PI / 180);
})
.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)
Expand All @@ -63,13 +67,16 @@ const labelFormatting = function(selection: d3.Selection<d3.BaseType, plotData,
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;
.attr("transform", (d, i) => {
const marker_offset: number = initialLabelXY[i].marker_offset;
//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})`;
const angle: number = initialLabelXY[i].theta - (d.label.aesthetics.label_position === "top" ? 180 : 0);
const x_offset: number = marker_offset * Math.cos(angle * Math.PI / 180);
const y_offset: number = marker_offset * Math.sin(angle * Math.PI / 180);

return `translate(${x + x_offset}, ${y + y_offset}) rotate(${angle + (d.label.aesthetics.label_position === "top" ? 90 : 270)})`;
})
.style("fill", d => d.label.aesthetics.label_marker_colour)
.style("stroke", d => d.label.aesthetics.label_marker_outline_colour)
Expand All @@ -85,23 +92,39 @@ export default function drawLabels(selection: svgBaseType, visualObj: Visual) {
selection.append("g").classed("text-labels", true);
}

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;
e.subject.label.y = e.sourceEvent.y;
const d = e.subject;
// Get the angle and distance of label from the point
const x_val = visualObj.viewModel.plotProperties.xScale(d.x);
const y_val = visualObj.viewModel.plotProperties.yScale(d.value);
const angle = Math.atan2(e.sourceEvent.y - y_val, e.sourceEvent.x - x_val) * 180 / Math.PI;
const distance = Math.sqrt(Math.pow(e.sourceEvent.y - y_val, 2) + Math.pow(e.sourceEvent.x - x_val, 2));

const marker_offset: number = 10;
const x_offset: number = marker_offset * Math.cos(angle * Math.PI / 180);
const y_offset: number = marker_offset * Math.sin(angle * Math.PI / 180);

e.subject.label.angle = angle;
e.subject.label.distance = distance;
d3.select(this)
.select("text")
.attr("x", e.sourceEvent.x)
.attr("y", e.sourceEvent.y);

let line_offset: number = d.label.aesthetics.label_line_offset;
line_offset = d.label.aesthetics.label_position === "top" ? line_offset : -(line_offset + d.label.aesthetics.label_size / 2);

d3.select(this)
.select("line")
.attr("x1", e.sourceEvent.x)
.attr("y1", e.sourceEvent.y + line_offset);
});
.attr("y1", e.sourceEvent.y + line_offset)
.attr("x2", x_val + x_offset)
.attr("y2", y_val + y_offset);

d3.select(this)
.select("path")
.attr("transform", `translate(${x_val + x_offset}, ${y_val + y_offset}) rotate(${angle - 90})`);
});

selection.select(".text-labels")
.selectAll(".text-group-inner")
Expand Down
Loading