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:
- Starts at the upper left; 0,0
- Moves across to the upper right corner; widthProxy,0
- Moves down to the lower right corner; widthProxy, heightProxy
- Moves across to the lower left corner; 0, heightProxy
- 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.
Next time, we'll put this into the APS window.