Technology Blogs by SAP
Learn how to extend and personalize SAP applications. Follow the SAP technology blog for insights into SAP BTP, ABAP, SAP Analytics Cloud, SAP HANA, and more.
cancel
Showing results for 
Search instead for 
Did you mean: 
david_stocker
Advisor
Advisor

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

In this installment, we'll be constructing the basic positioning visualizer JavaScript code.  As we did with the initial arc definition in Part 2a and the initial variable controlled arc definition in Part 3a, we'll first use raw html5 as a sandbox.  The positioning visualizer that we're going to build for our gauge component will consist of the following:

  • It will represent the component at a 1:1 scale.

  • It will draw a black rectangle, indicating the borders of the component

  • It will draw the padding margins as blue lines, within the component.

  • It will represent the potential arc of the gauge (from -180° to +180°) as a black circle.

  • This circle will be positioned as the actual gauge within the canvas, allowing the designer to see where the padding margins are and how these padding margins affect the size and positioning of the gauge.

  • There will be white crosshairs, centered on the centroid of the circle, allowing the designer to easily see there the origin of the arc is.

The visualizer will look something like this:

In order to construct it in D3, we'll break the visualizer down into its constituent components.  In terms of raw shapes, we have the following:

  • 4 blue rectangles.  D3 allows us to draw a rectangle, but we won't use that feature, as it is filled in by default.  Instead, we'll use paths and draw each rectangle by tracing through each of the corners and returning to the original, as if we were drawing with a pencil.

  • 1 black rectangle, drawn the same way.

  • The crosshairs, which will be drawn as two lines.

  • The circle, which will be drawn the same way as we've been drawing the gauge; as an arc.  The color will be fixed to black and the start/end angles will be -180° and 180° respectively.

  • We'll use a consistent line stroke thickness of two pixels.

Constructing the basic Javascript for drawing the positioning visualizer

For our APS Javascript, we'll be using the four padding properties.  In the actual component, we'll be following the pattern of me._<propertyName> for the local copies of these four properties.  In in sandbox, we'll dispense with the me. Prefix, but we'll keep the underscore.  The padding sizes are defined as follows:


  //Outer Dimensions & Positioning


  _paddingTop = 0;


  _paddingBottom = 0;


  _paddingLeft = 0;


  _paddingRight = 0







We'll define the stroke thicknesses and set a variable for the height and width.  We're not following the underscore convention, even though height and width are properties.  They are not accessible via the normal methods however.  We'll cover synching them later, but for now we'll just define variables for height and width.


  //Viz definitiions


  var lineThickness = 2;



  //Height and Width Proxies


  widthProxy = 200;


  heightProxy = 200;







Next, we clear any SVG elements from the HTML file's content div and re-insert one.  While we're at it, we'll declare the PI variable


// Clear any existing content.  We'll redraw from scratch


d3.select("#content").selectAll("*").remove();


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



var pi = Math.PI;







Drawing the Gauge Dummy

Now we'll actually start drawing the visualizer itself, starting with the gauge dummy.  The code should be familiar by now.  We determine what the largest padding value is and combine with the smaller of height and width to calculate the outer radius.  When we draw the "gauge", we'll draw a black 360° circle (from -180° to +180°).


//Determing the position of the gauge dummy (black circle)


// Find the larger left/right padding


var lrPadding = _paddingLeft + _paddingRight;


var tbPadding = _paddingTop + _paddingBottom;


var maxPadding = lrPadding;


if (maxPadding < tbPadding){


  maxPadding = tbPadding


}




//Do the same with the overall height and width


var smallerAxis = heightProxy;


if (widthProxy < smallerAxis){


  smallerAxis = widthProxy


}



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




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


var offsetLeft = outerRad + _paddingLeft;


var offsetDown = outerRad + _paddingTop;




//The black Circle


var arcDef = d3.svg.arc()


  .innerRadius(0)


  .outerRadius(outerRad)


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


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




var guageDummy = vis.append("path")


  .style("fill", "black")


  .attr("width", widthProxy).attr("height", heightProxy) // Added height and width so arc is visible


  .attr("transform", "translate(" + offsetLeft + "," + offsetDown + ")")


  .attr("d", arcDef);







Drawing the Line Strokes

When we define our paths, we'll be defining them via a set of x/y coordinates and then D3 will draw its "pencil stokes" between them.  The waypoint "data" of a line is a list of Javascript Objects; each with a pair of x and y coordinate properties.  Strictly speaking, the properties don't have to be called "x" and "y".  We could also call them "fred" and "frank", but "x" and "y" are self descriptive, and we'll stick with them.

Below is an example Line Data list.  It defines an outer box - the component outline - for the component.  It:

  1. Starts at the upper left; 0,0
  2. Moves across to the upper right corner; widthProxy,0
  3. Moves down to the lower right corner; widthProxy, heightProxy
  4. Moves across to the lower left corner; 0, heightProxy
  5. Returns to the starting position in the upper left; 0,0

[


  {"x":0, "y":0},


  {"x": widthProxy, "y":0},


  {"x": widthProxy, "y":heightProxy},


  {"x":0, "y":heightProxy},


  {"x":0, "y":0}


];







We can now define all of our lines.  When we draw the padding boxes, keep in mind that each is anchored on the appropriate side of the component outline.  The crosshairs a s defined by two strokes, one from the top center of the circle, down to the bottom center.  The other goes from the left middle, to the right middle.


var lineDataOuter = [{"x":0, "y":0}, {"x": widthProxy, "y":0}, {"x": widthProxy, "y":heightProxy}, {"x":0, "y":heightProxy}, {"x":0, "y":0}];


var lineDataPaddingLeft = [{"x":0, "y":0}, {"x":_paddingLeft, "y":0}, {"x":_paddingLeft, "y":heightProxy}, {"x":0, "y":heightProxy}, {"x":0, "y":0}];


var lineDataPaddingRight = [{"x":( widthProxy - _paddingRight), "y":0}, {"x": widthProxy, "y":0}, {"x": widthProxy, "y":heightProxy}, {"x":( widthProxy - _paddingRight), "y":heightProxy}, {"x":( widthProxy - _paddingRight), "y":0}];


var lineDataPaddingUpper= [{"x":0, "y":0}, {"x": widthProxy, "y":0}, {"x": widthProxy, "y":_paddingTop}, {"x":0, "y":_paddingTop}, {"x":0, "y":0}];


var lineDataPaddingLower = [{"x":0, "y":(heightProxy - _paddingBottom)}, {"x": widthProxy, "y":(heightProxy - _paddingBottom)}, {"x": widthProxy, "y":heightProxy}, {"x":0, "y":heightProxy}, {"x":0, "y":(heightProxy - _paddingBottom)}];



var lineDataCrosshairsHorizontal = [{"x":_paddingLeft, "y":(_paddingTop + outerRad) }, {"x":(_paddingLeft + 2*outerRad), "y":(_paddingTop + outerRad) }];


var lineDataCrosshairsVertical = [{"x":(_paddingLeft  + outerRad), "y":_paddingTop }, {"x":(_paddingLeft  + outerRad), "y":(_paddingTop + 2*outerRad) }];







The Line Accessor

In between the coordinates that we just defined, D3 is going to do something called interpolation; effectively filling in the blanks to convert your waypoint coordinates to SVG paths.  There is an excellent overview of SVG paths in D3 at DashingD3.com if you are interested.  We'll simply use the "cookbook version" of a linear interpolator; one that consumes waypoint data defined in terms of X and Y coordinates and draws straight lines between them.


//Line Accessor Function


var lineAccessor = d3.svg.line()


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


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


  .interpolate("linear");







Adding the boxes

The lines of the boxes are drawn just like the gauge arc,  by appending the path defined by the data and line accessor to the vis svg element.   There are a couple of minor differences:

  • When we defined the path for the gauge arc, we created a d3.svg.arc() instance.  This time, we'll be filling the "d" attribute with the results of a lineAccessor() function.  The input property of the accessor will be our lne data.

  • We'll use an empty fill.

  • Paths can have stroke colors and widts attributes.  We will assign values to these attributes.

To add the component outline to the svg element, our code would look like this:


var borderLinesOuter = vis


  .attr("width",  widthProxy).attr("height", heightProxy) // Added height and width so line is visible


  .append("path")


  .attr("d", lineAccessor(lineDataOuter))


  .attr("stroke", "black")


  .attr("stroke-width", lineThickness)


  .attr("fill", "none");



For all lines, it would look like this:


var borderLinesPaddingLeft = vis


  .attr("width",  widthProxy).attr("height", heightProxy) // Added height and width so line is visible


  .append("path")


  .attr("d", lineAccessor(lineDataPaddingLeft))


  .attr("stroke", "blue")


  .attr("stroke-width", lineThickness)


  .attr("fill", "none");


var borderLinesPaddingRight = vis


  .attr("width",  widthProxy).attr("height", heightProxy) // Added height and width so line is visible


  .append("path")


  .attr("d", lineAccessor(lineDataPaddingRight))


  .attr("stroke", "blue")


  .attr("stroke-width", lineThickness)


  .attr("fill", "none");


var borderLinesPaddingUpper = vis


  .attr("width",  widthProxy).attr("height", heightProxy) // Added height and width so line is visible


  .append("path")


  .attr("d", lineAccessor(lineDataPaddingUpper))


  .attr("stroke", "blue")


  .attr("stroke-width", lineThickness)


  .attr("fill", "none");


var borderLinesPaddingLower = vis


  .attr("width",  widthProxy).attr("height", heightProxy) // Added height and width so line is visible


  .append("path")


  .attr("d", lineAccessor(lineDataPaddingLower))


  .attr("stroke", "blue")


  .attr("stroke-width", lineThickness)


  .attr("fill", "none");




var borderLinesOuter = vis


  .attr("width",  widthProxy).attr("height", heightProxy) // Added height and width so line is visible


  .append("path")


  .attr("d", lineAccessor(lineDataOuter))


  .attr("stroke", "black")


  .attr("stroke-width", lineThickness)


  .attr("fill", "none");


var borderLinesCrosshairHorizontal = vis


  .attr("width",  widthProxy).attr("height", heightProxy) // Added height and width so line is visible


  .append("path")


  .attr("d", lineAccessor(lineDataCrosshairsHorizontal))


  .attr("stroke", "white")


  .attr("stroke-width", lineThickness)


  .attr("fill", "none");


var borderLinesCrosshairVertical = vis


  .attr("width",  widthProxy).attr("height", heightProxy) // Added height and width so line is visible


  .append("path")


  .attr("d", lineAccessor(lineDataCrosshairsVertical))


  .attr("stroke", "white")


  .attr("stroke-width", lineThickness)


  .attr("fill", "none");







The completed html file look like this:


<!DOCTYPE html>


<html>


  <head>


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


  <title>Part 4</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>


  //Outer Dimensions & Positioning


  _paddingTop = 0;


  _paddingBottom = 0;


  _paddingLeft = 0;


  _paddingRight = 0;



  //Viz definitiions


  var lineThickness = 2;



  //Height and Width Proxies


  widthProxy = 200;


  heightProxy = 200;




  // Clear any existing content.  We'll redraw from scratch


  d3.select("#content").selectAll("*").remove();


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




  var pi = Math.PI;




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


  //Gauge Dummy


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




  //Determing the position of the gauge dummy (black circle)


  // Find the larger left/right padding


  var lrPadding = _paddingLeft + _paddingRight;


  var tbPadding = _paddingTop + _paddingBottom;


  var maxPadding = lrPadding;


  if (maxPadding < tbPadding){


  maxPadding = tbPadding


  }



  //Do the same with the overall height and width


  var smallerAxis = heightProxy;


  if (widthProxy < smallerAxis){


  smallerAxis = widthProxy


  }



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




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


  var offsetLeft = outerRad + _paddingLeft;


  var offsetDown = outerRad + _paddingTop;




  //The black Circle


  var arcDef = d3.svg.arc()


  .innerRadius(0)


  .outerRadius(outerRad)


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


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




  var guageDummy = vis.append("path")


  .style("fill", "black")


  .attr("width", widthProxy).attr("height", heightProxy) // Added height and width so arc is visible


  .attr("transform", "translate(" + offsetLeft + "," + offsetDown + ")")


  .attr("d", arcDef);






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


  //Line Data


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


  var lineDataOuter = [{"x":0, "y":0}, {"x": widthProxy, "y":0}, {"x": widthProxy, "y":heightProxy}, {"x":0, "y":heightProxy}, {"x":0, "y":0}];


  var lineDataPaddingLeft = [{"x":0, "y":0}, {"x":_paddingLeft, "y":0}, {"x":_paddingLeft, "y":heightProxy}, {"x":0, "y":heightProxy}, {"x":0, "y":0}];


  var lineDataPaddingRight = [{"x":( widthProxy - _paddingRight), "y":0}, {"x": widthProxy, "y":0}, {"x": widthProxy, "y":heightProxy}, {"x":( widthProxy - _paddingRight), "y":heightProxy}, {"x":( widthProxy - _paddingRight), "y":0}];


  var lineDataPaddingUpper= [{"x":0, "y":0}, {"x": widthProxy, "y":0}, {"x": widthProxy, "y":_paddingTop}, {"x":0, "y":_paddingTop}, {"x":0, "y":0}];


  var lineDataPaddingLower = [{"x":0, "y":(heightProxy - _paddingBottom)}, {"x": widthProxy, "y":(heightProxy - _paddingBottom)}, {"x": widthProxy, "y":heightProxy}, {"x":0, "y":heightProxy}, {"x":0, "y":(heightProxy - _paddingBottom)}];




  var lineDataCrosshairsHorizontal = [{"x":_paddingLeft, "y":(_paddingTop + outerRad) }, {"x":(_paddingLeft + 2*outerRad), "y":(_paddingTop + outerRad) }];


  var lineDataCrosshairsVertical = [{"x":(_paddingLeft  + outerRad), "y":_paddingTop }, {"x":(_paddingLeft  + outerRad), "y":(_paddingTop + 2*outerRad) }];




  //Line Accessor Function


  var lineAccessor = d3.svg.line()


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


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


  .interpolate("linear");




  var borderLinesPaddingLeft = vis


  .attr("width",  widthProxy).attr("height", heightProxy) // Added height and width so line is visible


  .append("path")


  .attr("d", lineAccessor(lineDataPaddingLeft))


  .attr("stroke", "blue")


  .attr("stroke-width", lineThickness)


  .attr("fill", "none");


  var borderLinesPaddingRight = vis


  .attr("width",  widthProxy).attr("height", heightProxy) // Added height and width so line is visible


  .append("path")


  .attr("d", lineAccessor(lineDataPaddingRight))


  .attr("stroke", "blue")


  .attr("stroke-width", lineThickness)


  .attr("fill", "none");


  var borderLinesPaddingUpper = vis


  .attr("width",  widthProxy).attr("height", heightProxy) // Added height and width so line is visible


  .append("path")


  .attr("d", lineAccessor(lineDataPaddingUpper))


  .attr("stroke", "blue")


  .attr("stroke-width", lineThickness)


  .attr("fill", "none");


  var borderLinesPaddingLower = vis


  .attr("width",  widthProxy).attr("height", heightProxy) // Added height and width so line is visible


  .append("path")


  .attr("d", lineAccessor(lineDataPaddingLower))


  .attr("stroke", "blue")


  .attr("stroke-width", lineThickness)


  .attr("fill", "none");




  var borderLinesOuter = vis


  .attr("width",  widthProxy).attr("height", heightProxy) // Added height and width so line is visible


  .append("path")


  .attr("d", lineAccessor(lineDataOuter))


  .attr("stroke", "black")


  .attr("stroke-width", lineThickness)


  .attr("fill", "none");


  var borderLinesCrosshairHorizontal = vis


  .attr("width",  widthProxy).attr("height", heightProxy) // Added height and width so line is visible


  .append("path")


  .attr("d", lineAccessor(lineDataCrosshairsHorizontal))


  .attr("stroke", "white")


  .attr("stroke-width", lineThickness)


  .attr("fill", "none");


  var borderLinesCrosshairVertical = vis


  .attr("width",  widthProxy).attr("height", heightProxy) // Added height and width so line is visible


  .append("path")


  .attr("d", lineAccessor(lineDataCrosshairsVertical))


  .attr("stroke", "white")


  .attr("stroke-width", lineThickness)


  .attr("fill", "none");


  </script>


    </head>


  <body class='sapUiBody'>



  </body>


</html>







And at this is what we see in the browser, if the padding values are all zero.

As always, the completed extension (as of part 4) is available as a Github repository.

Next time, we'll put this into the APS window.

1 Comment