diff --git a/pbiviz.json b/pbiviz.json index 1741a2a..d94929c 100644 --- a/pbiviz.json +++ b/pbiviz.json @@ -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" diff --git a/src/Classes/viewModelClass.ts b/src/Classes/viewModelClass.ts index d61c0e9..ae9d5dd 100644 --- a/src/Classes/viewModelClass.ts +++ b/src/Classes/viewModelClass.ts @@ -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 }; } @@ -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], @@ -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}); diff --git a/src/D3 Plotting Functions/drawValueLabels.ts b/src/D3 Plotting Functions/drawValueLabels.ts index aa7de33..a49d60d 100644 --- a/src/D3 Plotting Functions/drawValueLabels.ts +++ b/src/D3 Plotting Functions/drawValueLabels.ts @@ -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, 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) @@ -63,13 +67,16 @@ const labelFormatting = function(selection: d3.Selection { - 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) @@ -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")