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.
In the end, out sandbox html file will produce something like this:
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 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"){
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"];
}
}
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 two images below show the positioning rules for start angle callouts and end angle callouts:
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)){
//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>
You must be a registered user to add a comment. If you've already registered, sign in. Otherwise, register and sign in.
User | Count |
---|---|
26 | |
22 | |
19 | |
13 | |
10 | |
9 | |
9 | |
8 | |
7 | |
7 |