Quantcast
Channel: SCN : Blog List - All Communities
Viewing all articles
Browse latest Browse all 2548

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

$
0
0

This is part of a tutorial series on creating extension components for Design Studio.

 

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:

Part11b.1.png

 

 

 

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.

MeasureTextPositioning.png

 

 

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

MeasurePositioning.png

 

 

This translates into the following JavaScript:

if (measureTextPositionType == "endpoint"){

  measurePosition = endPoints (outerRad, endAngleDeg);

  measureTextPosition = ["start", "1em"];

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

  measureTextPosition[0] = "end";

  }

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

  measureTextPosition[1] = "0em";

  }

}

else{

  if (measureTextPositionType == "top"){

  measurePosition = endPoints (outerRad, 0);

  measureTextPosition = ["middle", "-.15em"];

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

  }

  else if (measureTextPositionType == "upperCentral"){

  measurePosition = endPoints (outerRad/2, 0);

  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"){

  measurePosition = endPoints (outerRad/2, 180);

  measureTextPosition = ["middle", "1.1em"];

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

  }

  else if (measureTextPositionType == "bottom"){

  measurePosition = endPoints (outerRad, 180);

  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

GuideLinePositioningStart.png

 



End

GuideLinePositioningEnd.png

 

The flowchart for the guide positioning algorithm needed for the above pictures looks like the following:

GuidePositioning.png

 

 

 

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

  //Lower Right Quadrant

  // 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)){

  //Upper Right Quadrant

  if (isStart == true){

  textAnchor = "end";

  dominantBaseline = "0em";

  } else {

  textAnchor = "start";

  dominantBaseline = ".8em";

  }

  }

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

  //Upper Left Quadrant

  if (isStart == true){

  textAnchor = "end";

  dominantBaseline = ".8em";

  } else {

  textAnchor = "start";

  dominantBaseline = "0em";

  }

  } else {

  //Lower Left Quadrant

  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>

  <head>

  <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 innerRad = 0;

  //var outerRad = 70;

  var width = 400;

  var height = 400;

  var startAngleDeg = -45;

  var endAngleDeg = 45;

  var colorCode = "red";

 

 

  //Outer Dimensions & Positioning

  var paddingTop = 10;

  var paddingBottom = 10;

  var paddingLeft = 10;

  var paddingRight = 10;

 

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

 

  // Find the larger left/right padding

  var lrPadding = paddingLeft + paddingRight;

  var tbPadding = paddingTop + paddingBottom;

  var maxPadding = lrPadding;

  if (maxPadding < tbPadding){

  maxPadding = tbPadding

  }

 

  var outerRad = (width - 2*(maxPadding))/2;

  //var width = (outerRad * 2) + paddingLeft + paddingRight;

  //var height = (outerRad * 2) + paddingTop + paddingBottom;

 

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

  var offsetLeft = outerRad + paddingLeft;

  var offsetDown = outerRad + paddingTop;

 

  //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()

  .innerRadius(innerRad)

  .outerRadius(outerRad);

 

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

  .innerRadius(outerRad)

  .outerRadius(ringOuterRad)

  .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 lineData = [endPoints (outerRad, startAngleDeg), {x:offsetLeft, y:offsetDown}, endPoints (outerRad, endAngleDeg)];

  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}

  }

 

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

  //Lets add the indicator needle

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

 

  //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()

  .x(function(d) { return (d.x)*(outerRad/100); })

  .y(function(d) { return -1*(d.y)*(outerRad/100); })

  .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 + ")");

 

 

 

 

 

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

  //Lets add animations

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

 

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

  .innerRadius(innerRad)

  .outerRadius(outerRad);

 

 

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

  };

  });

  }

 

 

 

 

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

  //Lets add a legend

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

 

  //https://developer.mozilla.org/en/docs/Web/SVG/Attribute/text-anchor

 

  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"){

  measurePosition = endPoints (outerRad, endAngleDeg);

  measureTextPosition = ["start", "1em"];

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

  measureTextPosition[0] = "end";

  }

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

  measureTextPosition[1] = "0em";

  }

  }

  else{

  // Hack Alert!

  //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

  // https://msdn.microsoft.com/en-us/library/gg558060(v=vs.85).aspx

  if (measureTextPositionType == "top"){

  measurePosition = endPoints (outerRad, 0);

  measureTextPosition = ["middle", "-.15em"];

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

  }

  else if (measureTextPositionType == "upperCentral"){

  measurePosition = endPoints (outerRad/2, 0);

  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"){

  measurePosition = endPoints (outerRad/2, 180);

  measureTextPosition = ["middle", "1.1em"];

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

  }

  else if (measureTextPositionType == "bottom"){

  measurePosition = endPoints (outerRad, 180);

  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[0])

  //.attr("dominant-baseline", measureTextPosition[1]);

  .attr("dy", measureTextPosition[1]);

  }

 

  //Guide Positioning

  if (drawGuideText == true){

  var guidePositionStart = {};

  var guidePositionEnd = {};

  var isMiddleCO = false;

  if (guidePositioning == "end"){

  guidePositionStart = endPoints (outerRad, startAngleDeg);

  guidePositionEnd = endPoints (outerRad, endAngleDeg);

  }

  else {

  guidePositionStart = endPoints (outerRad/2, startAngleDeg);

  guidePositionEnd = endPoints (outerRad/2, endAngleDeg);

  }

  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[0])

  //.attr("dominant-baseline", guideTextPositionStart[1]);

  .attr("dy", guideTextPositionStart[1]);

 

 

  //End Text

  vis.append("text")

  .attr("transform", "translate(" + guidePositionEnd.x + "," + guidePositionEnd.y + ")")

  .text("endText")

  //.attr("text-anchor", "start")

  .attr("text-anchor", guideTextPositionEnd[0])

  //.attr("dominant-baseline", guideTextPositionEnd[1]);

  .attr("dy", guideTextPositionEnd[1]);

  }

 

 

 

 

 

 

  // 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

  // text-anchor: https://developer.mozilla.org/en/docs/Web/SVG/Attribute/text-anchor

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

  //Lower Right Quadrant

  // 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)){

  //Upper Right Quadrant

  if (isStart == true){

  textAnchor = "end";

  dominantBaseline = "0em";

  } else {

  textAnchor = "start";

  dominantBaseline = ".8em";

  }

  }

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

  //Upper Left Quadrant

  if (isStart == true){

  textAnchor = "end";

  dominantBaseline = ".8em";

  } else {

  textAnchor = "start";

  dominantBaseline = "0em";

  }

  } else {

  //Lower Left Quadrant

  if (isStart == true){

  textAnchor = "start";

  dominantBaseline = ".8em";

  } else {

  textAnchor = "end";

  dominantBaseline = "0em";

  }

  }

 

  return [textAnchor, dominantBaseline]

  }

 

 

 

  </script>

    </head>

  <body class='sapUiBody'>

  <div id='content'></div>

  </body>

</html>


Viewing all articles
Browse latest Browse all 2548

Trending Articles



<script src="https://jsc.adskeeper.com/r/s/rssing.com.1596347.js" async> </script>