 # Your First Extension: Part 11b – D3 Text Dynamic Positioning Algorithms

Last time, we took out first look at text elements in D3 and the attributes that we need to position our text.  Now we’ll work out how the positioning of our callouts.  In general, we are going to have to types of callouts available to designers; measure value callouts and guide line value callouts.

• Measure Callout – This callout tracks the value of the measureVal property, or the  endAngleDeg property if useMeasures has been disabled.
• Guide Line Callout – This callout pair tracks the measureMin and measureMax properties, or the startAngleDeg and endAngleDegMax if useMeasures has been disabled.

In the end, out sandbox html file will produce something like this: ## Measure Callout Positioning

When the designer elects to enable measure callouts on the gauge, we want to have a couple of options as to placing the callouts.

• The designer can select from a fixed number of vertical positions, along the horizontal centerline of the gauge.
• The designer can elect to have the endpoint of the indicator needle, (or where it would be if the indicator is not enabled) anchored to the endpoint, or along the middle of the guide.  When anchored to the “endpoint”, the callout shall be either right to left justified, as needed. The positioning of the measure text callout is fairly straightforward.

Unless we are positioning on the endpoint, we are on the vertical center axis.  The Y axis translation offset of the text element is always the same Y axis translation as the main arc; offsetLeft.  We’ll calculate the vertical position, based on a set of arbitrary vertical axis translation values. (see image, above).  If we are positioning at the endpoint, then we’ll calculate the endpoint position with basic trigonometry.  The function to do this calculation is very simple:

function endPoints (lineLength, lineAngle){

var endX = offsetLeft + (lineLength * Math.sin(lineAngle * (pi/180)));

var endY = offsetDown – (lineLength * Math.cos(lineAngle * (pi/180)));

return {x:endX, y:endY}

}

The flow chart below, lays out the X and Y axis offset translations, as well as the final vertical anchoring of the text, presuming the use of the SVG dominant baseline property.  Since we’re using the dy attribute instead, this will translate as follows:

dominant-baseline: text-before-edge   =>   dy: 0em

dominant-baseline: text-after-edge      =>   dy: 1em This translates into the following JavaScript:

if (measureTextPositionType == “endpoint”){

measureTextPosition = [“start”, “1em”];

if ((measurePosition.x – offsetLeft) < 0){

measureTextPosition = “end”;

}

if ((measurePosition.y – offsetDown) < 0){

measureTextPosition = “0em”;

}

}

else{

if (measureTextPositionType == “top”){

measureTextPosition = [“middle”, “-.15em”];

//measureTextPosition = [“middle”, “text-before-edge”];

}

else if (measureTextPositionType == “upperCentral”){

measureTextPosition = [“middle”, “-.15em”];

//measureTextPosition = [“middle”, “text-before-edge”];

}

else if (measureTextPositionType == “upperIdeographic”){

measurePosition = endPoints (1, 0);

measureTextPosition = [“middle”, “-.15em”];

//measureTextPosition = [“middle”, “text-before-edge”];

}

else if (measureTextPositionType == “lowerIdeographic”){

measurePosition = endPoints (1, 180);

measureTextPosition = [“middle”, “1.1em”];

//measureTextPosition = [“middle”, “text-after-edge”];

}

else if (measureTextPositionType == “lowerCentral”){

measureTextPosition = [“middle”, “1.1em”];

//measureTextPosition = [“middle”, “text-after-edge”];

}

else if (measureTextPositionType == “bottom”){

measureTextPosition = [“middle”, “1.1em”];

//measureTextPosition = [“middle”, “text-after-edge”];

}

}

## Guide Line Positioning

If guide line callouts are enabled, we will create a pair of text callouts; one for start angle and one for end angle.  We want to be able to position these guide line callouts at either the endpoint of the guide line, or along the midpoint.  In both cases, we use the endpoint() function, above and simply use lineLength = outerRadius; or lineLength = outerRadius/2.

In general, we want to follow two basic rules:

• The text-anchor property is set o that the text stays outside the gauge arc.  The gauge is always presumed to run clockwise.  This means that start angle value should be positioned to come “before” the start guide line angle.   And the end angle value should come “after” the end angle value.
• The dy value (either 0em or .8em) is set so that the text should be inside the gauge ring.

The two images below show the positioning rules for start angle callouts and end angle callouts:

### Start ### End The flowchart for the guide positioning algorithm needed for the above pictures looks like the following: This flow translates into the following JavaScript function:

function textPositioning (x, y, isStart){

var relativeOffsetX = x – offsetLeft;

var relativeOffsetY = y – offsetDown;

if (isStart == undefined){

isStart = false;

}

var dominantBaseline = null;

var textAnchor = null;

if ((relativeOffsetX >= 0) && (relativeOffsetY >= 0)){

// Both middle and enf have a negative dominant baseline

if (isStart == true){

textAnchor = “start”;

dominantBaseline = “0em”;

} else {

textAnchor = “end”;

dominantBaseline = “.8em”;

}

} else if ((relativeOffsetX >= 0) && (relativeOffsetY < 0)){

if (isStart == true){

textAnchor = “end”;

dominantBaseline = “0em”;

} else {

textAnchor = “start”;

dominantBaseline = “.8em”;

}

}

else if ((relativeOffsetX < 0) && (relativeOffsetY < 0)){

if (isStart == true){

textAnchor = “end”;

dominantBaseline = “.8em”;

} else {

textAnchor = “start”;

dominantBaseline = “0em”;

}

} else {

if (isStart == true){

textAnchor = “start”;

dominantBaseline = “.8em”;

} else {

textAnchor = “end”;

dominantBaseline = “0em”;

}

}

return [textAnchor, dominantBaseline]

}

Putting it all together, into our sandbox html file, we get the following code, which displays this blog post.  Next time, we’ll migrate this new code into our component. You can experiment with different measure text positions, by altering the value of measureTextPositionType, on line 205.

<!DOCTYPE html>

<html>

<meta http-equiv=’X-UA-Compatible’ content=’IE=edge’ />

<title>Part 7</title>

<div id=’content’></div>

<script src=”https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.6/d3.min.js” charset=”utf-8″></script>

<!– <script src=”file://d3/d3.js” charset=”utf-8″></script>–>

<script>

var vis = d3.select(“#content”).append(“svg:svg”).attr(“width”, “100%”).attr(“height”, “100%”);

var pi = Math.PI;

//Viz definitiions

var width = 400;

var height = 400;

var startAngleDeg = -45;

var endAngleDeg = 45;

var colorCode = “red”;

//Outer Dimensions & Positioning

//The total size of the component is calculated from its parts

// Find the larger left/right padding

}

//The offset will determine where the center of the arc shall be

//Don’t let the arc have a negative length

if (endAngleDeg < startAngleDeg){

endAngleDeg = startAngleDeg;

alert(“End angle may not be less than start angle!”);

}

var arcDef = d3.svg.arc()

var guageArc = vis.append(“path”)

.datum({endAngle: startAngleDeg * (pi/180), startAngle: startAngleDeg * (pi/180)})

.style(“fill”, “orange”)

.attr(“transform”, “translate(” + offsetLeft + “,” + offsetDown + “)”)

.attr(“d”, arcDef);

///////////////////////////////////////////

//Lets build a border ring around the gauge

///////////////////////////////////////////

//var visRing = d3.select(“#content”).append(“svg:svg”).attr(“width”, “100%”).attr(“height”, “100%”);

var ringThickness = 2;

var ringOuterRad = outerRad + ringThickness;  //Outer ring starts at the outer radius of the inner arc

var ringColorCode = “black”;

var ringStartAngleDeg = 0;

var ringEndAngleDeg = 360;

//Don’t let the arc have a negative length

if (ringEndAngleDeg < ringStartAngleDeg){

ringEndAngleDeg = ringStartAngleDeg;

alert(“End angle of outer ring may not be less than start angle!”);

}

var ringArcDefinition = d3.svg.arc()

.startAngle(ringStartAngleDeg * (pi/180)) //converting from degs to radians

.endAngle(ringEndAngleDeg * (pi/180)) //converting from degs to radians

var ringArc = vis

.append(“path”)

.attr(“d”, ringArcDefinition)

.attr(“fill”, ringColorCode)

.attr(“transform”, “translate(” + offsetLeft + “,” + offsetDown + “)”);

///////////////////////////////////////////

//Lets build a the start and end lines

///////////////////////////////////////////

var bracketThickness = 2;

var visStartBracket = d3.select(“#content”).append(“svg:svg”).attr(“width”, “100%”).attr(“height”, “100%”);

var lineFunction = d3.svg.line()

.x(function(d) { return d.x; })

.y(function(d) { return d.y; })

.interpolate(“linear”);

var borderLines = vis

.attr(“width”, width).attr(“height”, height) // Added height and width so line is visible

.append(“path”)

.attr(“stroke”, ringColorCode)

.attr(“stroke-width”, bracketThickness)

.attr(“fill”, “none”);

//Helper function

function endPoints (lineLength, lineAngle){

var endX = offsetLeft + (lineLength * Math.sin(lineAngle * (pi/180)));

var endY = offsetDown – (lineLength * Math.cos(lineAngle * (pi/180)));

return {x:endX, y:endY}

}

///////////////////////////////////////////

///////////////////////////////////////////

//needleWaypoints is defined with positive y axis being up

var needleWaypoints = [{x: 0,y: 100}, {x: 10,y: 0}, {x: 0,y: -10}, {x: -10,y: 0}, {x: 0,y: 100}]

//we need to invert the y-axis and scale the indicator to the gauge.

//  If Y = 100, then that is 100% of outer radius.  So of Y = 100 and outerRad = 70, then the scaled Y will be 70.

var needleFunction = d3.svg.line()

.interpolate(“linear”);

var needle = vis

.append(“g”)

.attr(“transform”, “translate(” + offsetLeft + “,” + offsetDown + “)”)

.append(“path”)

.attr(“d”, needleFunction(needleWaypoints))

.attr(“stroke”, ringColorCode)

.attr(“stroke-width”, bracketThickness)

.attr(“fill”, ringColorCode)

.attr(“transform”, “rotate(” + startAngleDeg + “)”);

///////////////////////////////////////////

///////////////////////////////////////////

var delayNeedle = 500;

var durationNeedle = 10000;

var easeNeedle = “elastic”; //https://github.com/mbostock/d3/wiki/Transitions#d3_ease

var delayBorderLines = 500;

var durationBorderLines = 1000;

var easeBorderLines = “linear”; //https://github.com/mbostock/d3/wiki/Transitions#d3_ease

var durationArc = 5000;

var easeArc= “linear”; //https://github.com/mbostock/d3/wiki/Transitions#d3_ease

//Arcs are in radians, but rotation transformations are in degrees.  Kudos to D3 for consistency

needle.transition()

.attr(“transform”, “rotate(” + endAngleDeg + “)”)

.duration(durationNeedle)

.delay(delayNeedle)

.ease(easeNeedle);

borderLines.transition()

.attr(“d”, lineFunction(lineData))

.duration(durationBorderLines)

.delay(delayBorderLines)

.ease(easeBorderLines);

var arcStepDef = d3.svg.arc()

guageArc.transition()

.duration(durationArc)

.call(arcTween, endAngleDeg * (pi/180));

//This blog post explains using attrTween for arcs: http://bl.ocks.org/mbostock/5100636

// Function adapted from this example

// Creates a tween on the specified transition’s “d” attribute, transitioning

// any selected arcs from their current angle to the specified new angle.

function arcTween(transition, newAngle) {

transition.attrTween(“d”, function(d) {

var interpolate = d3.interpolate(d.endAngle, newAngle);

return function(t) {

d.endAngle = interpolate(t);

return arcDef(d);

};

});

}

///////////////////////////////////////////

///////////////////////////////////////////

var guidePositioning = “end”;  //”end” and “midpoint”

var measureTextPositionType = “upperCentral”;

var drawGuideText = true;

var drawMeasureText = true;

//Measure Text Positioning

if (drawMeasureText == true){

var measurePosition = {};

var measureTextPosition = {};

if (measureTextPositionType == “endpoint”){

measureTextPosition = [“start”, “1em”];

if ((measurePosition.x – offsetLeft) < 0){

measureTextPosition = “end”;

}

if ((measurePosition.y – offsetDown) < 0){

measureTextPosition = “0em”;

}

}

else{

//As of now, MS browsers don”t support the dominant baseline SVG property.

//  Using the dy property with a Xem offset is the hackish workaround

if (measureTextPositionType == “top”){

measureTextPosition = [“middle”, “-.15em”];

//measureTextPosition = [“middle”, “text-before-edge”];

}

else if (measureTextPositionType == “upperCentral”){

measureTextPosition = [“middle”, “-.15em”];

//measureTextPosition = [“middle”, “text-before-edge”];

}

else if (measureTextPositionType == “upperIdeographic”){

measurePosition = endPoints (1, 0);

measureTextPosition = [“middle”, “-.15em”];

//measureTextPosition = [“middle”, “text-before-edge”];

}

else if (measureTextPositionType == “lowerIdeographic”){

measurePosition = endPoints (1, 180);

measureTextPosition = [“middle”, “1.1em”];

//measureTextPosition = [“middle”, “text-after-edge”];

}

else if (measureTextPositionType == “lowerCentral”){

measureTextPosition = [“middle”, “1.1em”];

//measureTextPosition = [“middle”, “text-after-edge”];

}

else if (measureTextPositionType == “bottom”){

measureTextPosition = [“middle”, “1.1em”];

//measureTextPosition = [“middle”, “text-after-edge”];

}

}

vis.append(“text”)

.attr(“transform”, “translate(” + measurePosition.x+ “,” + measurePosition.y+ “)”)

.text(“measureText”)

.attr(“text-anchor”, measureTextPosition)

//.attr(“dominant-baseline”, measureTextPosition);

.attr(“dy”, measureTextPosition);

}

//Guide Positioning

if (drawGuideText == true){

var guidePositionStart = {};

var guidePositionEnd = {};

var isMiddleCO = false;

if (guidePositioning == “end”){

}

else {

}

var guideTextPositionStart = textPositioning (guidePositionStart.x, guidePositionStart.y, true);

var guideTextPositionEnd= textPositioning (guidePositionEnd.x, guidePositionEnd.y);

//Start Text

vis.append(“text”)

.attr(“transform”, “translate(” + guidePositionStart.x + “,” + guidePositionStart.y + “)”)

.text(“startText”)

.attr(“text-anchor”, guideTextPositionStart)

//.attr(“dominant-baseline”, guideTextPositionStart);

.attr(“dy”, guideTextPositionStart);

//End Text

vis.append(“text”)

.attr(“transform”, “translate(” + guidePositionEnd.x + “,” + guidePositionEnd.y + “)”)

.text(“endText”)

//.attr(“text-anchor”, “start”)

.attr(“text-anchor”, guideTextPositionEnd)

//.attr(“dominant-baseline”, guideTextPositionEnd);

.attr(“dy”, guideTextPositionEnd);

}

// Helper function to determine the vertical alignment (called ‘dominant-baseline’) and horizontal alignment (called ‘ text-anchor’)

// In essence, this function tries to find a readable position for the text, so that it lies ourside the main arc, no matter the current

// start and end points:

// If x is to the left of the gauge’s centerline, then the text should be anchored to the left of x.  Otherwise to the right

// If y id below the centerline, then the text should be below y.  Otherwise above

// dominant-baseline: http://bl.ocks.org/eweitnauer/7325338

function textPositioning (x, y, isStart){

var relativeOffsetX = x – offsetLeft;

var relativeOffsetY = y – offsetDown;

if (isStart == undefined){

isStart = false;

}

var dominantBaseline = null;

var textAnchor = null;

if ((relativeOffsetX >= 0) && (relativeOffsetY >= 0)){

// Both middle and enf have a negative dominant baseline

if (isStart == true){

textAnchor = “start”;

dominantBaseline = “0em”;

} else {

textAnchor = “end”;

dominantBaseline = “.8em”;

}

} else if ((relativeOffsetX >= 0) && (relativeOffsetY < 0)){

if (isStart == true){

textAnchor = “end”;

dominantBaseline = “0em”;

} else {

textAnchor = “start”;

dominantBaseline = “.8em”;

}

}

else if ((relativeOffsetX < 0) && (relativeOffsetY < 0)){

if (isStart == true){

textAnchor = “end”;

dominantBaseline = “.8em”;

} else {

textAnchor = “start”;

dominantBaseline = “0em”;

}

} else {

if (isStart == true){

textAnchor = “start”;

dominantBaseline = “.8em”;

} else {

textAnchor = “end”;

dominantBaseline = “0em”;

}

}

return [textAnchor, dominantBaseline]

}

</script>