Jim Armstrong
ngconf
Published in
14 min readApr 30, 2019

--

Scientific Visualization With Angular

Exploit Angular Directives, SVG, and quadratic Bezier curves for fun and profit.

In a previous generation of web applications, Adobe Flex proved the power of a declarative syntax for layout with code-behind for more advanced logic. Angular follows suit with a robust template-oriented architecture. One of my favorite aspects of the platform is Attribute Directives. Attribute Directives augment existing DOM elements, imbuing them with capabilities beyond the element’s normal function in the layout. Since the Directive is applied as an attribute of an element, custom behavior is expressed at the template level where it is easier for other developers to deconstruct an application’s logic.

As an example, it is easy to make an existing H1 tag clickable simply by adding a ‘make clickable’ Attribute Directive. The ‘click’ handler is also expressed in the template, so it is easier to deconstruct that portion of the application’s logic. It is not necessary to add to the markup (i.e. insert an anchor tag) in order to add the clickable behavior, and the Directive is easily reused elsewhere.

The Application

This tutorial covers the use of Angular Attribute Directives to enable an application to display scientific data in a SVG-rendered view. The specific application is visualization of the normal distribution curve. Now, those who have not been introduced to lies, damned lies, and statistics might feel uncomfortable having to deal with the mathematics of computing and displaying the normal curve. Don’t worry, though, because here is how we will handle that pesky math. Watch closely.

Equations … numerical analysis … function approximation … Bezier curves .. blah … blah … API.

All that complexity is conveniently encapsulated away into an API. Now, speaking of API, point your friendly neighborhood browser to this Github so you can follow along with the deconstruction

The application has the following folder structure:

/src/app

- directives (attribute directive for drawing the normal curve into a container)

- libs (all the math and drawing helper libraries)

- models (a simple model for normal distribution parameters)

  • services (a service to load model data)

Outline

In order to create the normal-distribution display, we need the following:

1 — An SVG library to render SVG (makes life easier). Easy is good.

2 — Create an Angular Attribute Directive to imbue an existing container (such as a DIV) with the ability to render the normal curve

3 — Compute axis-related information so that the y-up curve in ‘user’ coordinates can be mapped to y-down pixel coordinates in an SVG (or Canvas) display

4 — Use the supplied math libraries to return simple drawing primitives for the normal distribution curve

5 — Wire up some UI controls that allow parameters of the normal distribution to be interactively changed. This allows the drawing attribute to be used in an educational application, for example.

SVG.js

As a matter of personal preference, I use SVG.js as an SVG rendering library. Just in case you have never setup a third-party JS library for use in an Angular application, the process is quite simple. Open the angular.json file in the project root and look for the scripts array. Since this particular application uses SVG.js, add the path to the SVG.js library as shown below

“scripts”: [“./node_modules/svg.js/dist/svg.js”]

To use the library, add the following import to your Typescript file,

import * as SVG from ‘svg.js’;

and then variables may be declared as,

protected _surface: SVG.Doc;

SVG Attribute Directive

SVG can be rendered into a DIV, for example, so it is only necessary for a DIV to be present in the parent component’s template in order to serve as a container for the normal distribution display. The attribute name is defined in the Directive’s selector metadata.

/src/app/directives/svg-draw-normal.directive.ts

@Directive({
selector: '[svgDrawNormal]'
})

To use the Directive in a template, add the selector as an attribute,

/src/app/app.component.html

<div class="card-container bounded">
<div svgDrawNormal class="graph" hBuffer="10" vBuffer="5" hSpan="8" vSpan="1" (drawingUpdated)="onDrawingUpdated($event)"></div>

An Angular Directive is a Component without a template, so it has the same lifecycle. The Directive may have Inputs and emit Outputs. Inputs and Output events are all expressed on the HTML element to which the Directive is applied. Supporting variable declarations and code are exactly as one would expect with a Component,

/src/app/directives/svg-draw-normal.directive.ts

/**
* @type {number} hSpace Horizontal span in units (considered symmetric about zero on the x-axis); must be integer
*/
@Input('hSpan')
protected _hSpan: number;

/**
* @type {number} vSpan Vertical span in units (absolute with zero the implied minimum on the y-axis); must be integer
*/
protected _vSpan: number;

/**
* @type {number} (integer) Horizontal Buffer space in px
*/
@Input ('hBuffer')
protected _hBuffer: number;

/**
* @type {number} (integer) Vertical buffer space in px
*/
@Input ('vBuffer')
protected _vBuffer: number;

@Output ('drawingUpdated')
protected _drawingUpdated: EventEmitter<numericTriple>;

Before continuing, it is probably worth discussing the following variable definition,

@Output ('drawingUpdated')
protected _drawingUpdated: EventEmitter<numericTriple>;

What is a numericTriple? Type declarations are familiar to us old-school C++ programmers, but it’s likely to be an unfamiliar concept to those new to Typescript.

Typescript supports custom types (somewhat similar to C++ typedef) and it provides support for tuples. Note the export earlier in the SVG drawing Directive code,

export type numericTriple = [number, number, number];

This statement defines an ordered triple of numeric data. For purposes of this application, an Object (backed by an Interface) could also have been used. I chose the tuple since the emitted event is picked up by the handler as an Array of numbers,

/src/app/app.component.ts

public onDrawingUpdated(value: numericTriple): void
{
[this.xMin, this.xMax, this.maxNormal] = value;
}

Destructuring assignment may be used for easy variable assignment or Array operations could be immediately applied to further transform the event data. I use the latter technique extensively in application development involving coordinate or other ordered numerical data, so I hope this is something that adds to your proverbial bag of tricks :)

Since the Directive is a View Child of the parent component, its lifecycle events happen after the corresponding lifecycle event of the parent. In this application, the SVG drawing Directive is a View Child of the main app component. So, the Directive’s ngOnInit handler, for example, is called after the main app component’s ngOnInit method. So, we don’t want to delay obtaining a reference to the Directive’s containing HTML element and initializing the drawing surface.

On the topic of direct DOM references, only use them if absolutely required. In many cases, properties set on the host element can be used to control a Directive. The @HostBinding decorator offers a convenient means to separate rendering and the DOM layer. This mechanism allows binding to any class, property, or attribute of the host element, often eliminating the need for a direct reference to and manipulation of the containing HTML element.

For this application, a reference to the DOM element is required in order to initialize the SVG drawing surface.

The above actions are handled in the Directive’s constructor,

/src/app/directives/svg-draw-normal.directive.ts

constructor(protected _elementRef: ElementRef)
{
this._width = 0;
this._height = 0;
this._pxPerUnitX = 0;
this._pxPerUnitY = 0;
this._x = 0;
this._hBuffer = 0;
this._vBuffer = 0;

this._hSpan = 8; // (initialize to -4 to 4 on the x-axis)
this._vSpan = 1; // (initialize to 0 to 1 on the y-axis)

this._drawingUpdated = new EventEmitter<numericTriple>();

this._width = this._elementRef.nativeElement.clientWidth;
this._height = this._elementRef.nativeElement.clientHeight;

this._surface = SVG(this._elementRef.nativeElement).size('100%', '100%').viewbox(0, 0, this._width, this._height);

}

A normal distribution is defined by two parameters, a mean and standard deviation. It is also common to define an x-coordinate to compute the probability that a random variable, X, is less than or equal to x. The SVG drawing Directive has a public API that allows the normal curve to be initialized (or changed) with these three parameters. A separate mutator function for the x-coordinate is provided since that is more likely to be changed in an educational application, for example.

The public draw method is the only necessary code to understand and that is deconstructed in the following section.

Drawing a Function in SVG

Now for the fun part — we get to draw something imperatively!

If no information is available for a function other than the ability to evaluate the function at a particular value of the independent variable, then it is normally drawn as a sequence of small line segments. This approach requires deciding how small to make the independent variable increment in order for the graph to appear smooth without drawing an excessive number of segments. I’ve done some work on heuristics for this in the past, but some functions have properties that can be exploited to apply a cleaner and more powerful technique.

Ever since the Flash drawing API (circa Flash 6), a quadratic Bezier curve can be used as a drawing primitive in lieu of a sequence of small line segments. Work on fast rasterization of Bezier curves dates back to the 1970’s and great strides were made in this area in the 1990’s. Here are a couple references for those interested in the topic:

https://dspace5.zcu.cz/bitstream/11025/15933/1/CURVE6.pdf

http://graphicsinterface.org/wp-content/uploads/gi1992-3.pdf

If a function can be represented by a sequence of quadratic Bezier curves (quadratic spline) over an interval, then the function can be quickly and smoothly graphed at any scale by simply drawing a small number of quad Bezier curves. This is the technique used in the tutorial. Although it is not necessary to understand how the spline is constructed, a brief overview is provided at the end of the article for interested readers.

The other consideration in drawing a function is the difference in coordinate systems between the function and the drawing environment. A function is typically defined in a y-up coordinate system, i.e. increasing y-coordinates appear above one another. The drawing surface is y-down, so increasing y-coordinate values appear below one another. The function is also described in ‘user’ coordinates or a real-valued interval. The drawing surface is defined in pixel coordinates that are always greater than or equal to zero. This requires two conversions.

1 — Convert x- and y-values for the function into pixel coordinates for display

2 — Convert y-up user coordinates into y-down pixel coordinates

For purposes of this tutorial, drawing grid lines, major/minor axis tics/labels), and other details of a full function graph are not covered. If you really wish to dive into this topic, then here is a Github that contains a full-featured single-quadrant graphing engine, already packaged as an Angular Component. It is optimized for displaying functions defined in the first quadrant (i.e. x and y greater than or equal to zero). This is quite common for business and engineering applications. My Typescript Math Toolkit graph axis and line decorator classes are provided as part of the code distribution.

https://github.com/theAlgorithmist/Angular5-Graph-Engine

For SVG or Canvas, the drawing surface is defined by pixel width and height with an origin in the upper, left-hand corner. In the general case, the horizontal extent of this surface must be mapped to user coordinates in the range [a, b], where a and b are real numbers, a < b. The vertical extent of the display is mapped into a user interval [c, d]. Since the normal distribution values are always greater than or equal to zero, we can use a single number (a vertical span, v) to represent the mapping of pixel height to the vertical extent of the graph display.

Two conversion factors are used to move back and forth between pixel and user coordinates. These are represented by the variables,

/src/app/directives/svg-draw-normal.directive.ts

protected _pxPerUnitX: number;
protected _pxPerUnitY: number;

which represent the number of pixels per unit x and number of pixels per unit y. For example, if the pixel width of the drawing surface is 600 and that space covers the range -2 to 2 in user x-coordinate, then the number of pixels per unit x value is 600/4 = 150. Every horizontal increment of 150 px represents a move of one unit in user x-coordinate.

If a coordinate, x, is computed in user space, then the conversion to drawing surface (pixel) coordinates is

(x -a)*_pxPerUnitX

Since the pixel coordinate system is y-down, the equation is reversed for the y-coordinate conversion,

(d-y)*_pxPerUnitY

Note that if the visible user-coordinate range in y is the interval [c,d] that y = d maps to zero in pixels (top of the display space). The case y = c maps to (d-c)*(pixel height)/(d-c) or the bottom of the display space.

Now, what if we desire some buffer space around the graph? Without it, the function graph would bleed right into the outer edges of the drawing surface. This would look particularly bad if the curve is drawn with a pixel width of two or greater.

Addition of horizontal and vertical buffer space alters both the pixels per unit x and y as well as the conversion formulas. If the horizontal buffer space, for example, is hb, then the x-coordinates in [a, b] are mapped to the display (pixel) space [hb, w-hb] where w is the width of the drawing surface. The horizontal and vertical buffers are added into the coordinate conversion, so the above equation is now

x in px = (x-a)*_pxPerUnitX + hb

In the general case, [c, d] in user y-coordinates is mapped to [vb, h-vb] (h is the height of the surface), then

y in px = (d - y)*_pxPerUnitY + vb

Since the minimum user y-coordinate is zero for the normal distribution and a single vertical span is used in this tutorial, the pixels per unit x and y with buffers simplify to

/src/app/directives/svg-draw-normal.directive.ts

this._pxPerUnitX = (this._width - 2*hBuffer)/(b-a);
this._pxPerUnitY = (this._height - 2*vBuffer)/this._vSpan;

Drawing the normal curve into the SVG surface requires constructing a path, which is a sequence of quadratic Bezier curves in this example. The quadratic Bezier spline is constructed with a simple API call.

const controlPoints: Array<IControlPoints> = this._normal.toBezier(a, b);

Ah, that was easy :)

The path is constructed with a single M (move-to) followed by a sequence of Q (quad Bezier) draw commands. The complete code segment with support for buffers is,

/src/app/directives/svg-draw-normal.directive.ts

for (i = 0; i < n; ++i)
{
quad = controlPoints[i];

if (i == 0)
{
// moveTo
path = 'M ' + ((quad.x0-a)*this._pxPerUnitX + hBuffer).toFixed(3) + ',' + (vBuffer + (this._vSpan-quad.y0)*this._pxPerUnitY + vBuffer).toFixed(3);
}

// quadBezierTo
path += ' Q ' + ((quad.cx-a)*this._pxPerUnitX + hBuffer).toFixed(3) + ',' + (vBuffer + (this._vSpan-quad.cy)*this._pxPerUnitY).toFixed(3) + ' ' + ((quad.x1-a)*this._pxPerUnitX + hBuffer).toFixed(3) + ',' +
(vBuffer + (this._vSpan-quad.y1)*this._pxPerUnitY).toFixed(3);
}

this._surface.path(path).fill('none').stroke({width: 2, color: '#0000ff'});

This process draws the same smooth sequence of curves at any scale on any SVG surface of any width and height.

The Directive’s draw method also displays a line segment from the x-axis to the value of the normal curve at a specified x-coordinate. This is a segue into a natural extension of this demo, namely drawing the area under the curve in [a, x] and [x, b]. The value of the quadratic spline approach is that the individual curves can be subdivided (the exact method is called De Casteljau subdivision) to create a new sequence of quad Bezier curves. The new sequences can be drawn using the exact same code segment as above, making it easy (and efficient) to draw specific areas under the normal curve.

Summary

Function graphing is an important operation in science, engineering, and EdTech applications. I hope this demo gave you a brief but solid introduction to the topic. And, I hope that readers new to Angular have a new appreciation for the power of Angular Attribute Directives.

Thanks for reading and good luck with your Angular efforts!

Bonus: Your Journey To The Math Side

TL;DR — This material is only for readers interested in ‘behind the scenes math’. You can happily use Angular Directives and SVG drawing for the remainder of your career without reading another line.

Ah, welcome my young apprentice to a brief explanation of how a cartesian curve can be approximated by a quadratic Bezier spline. Now, a spline is simply a sequence of independent curves that could be cartesian or parametric. Splines vary based on conditions enforced at interpolation or join points. The simplest constraint is that the spline interpolates the joint point, i.e. the end point of one curve equals the beginning point of the next curve. This is called G-0 continuity or matching the zero-th derivative in terms of geometric constraints.

The next level of continuity is G-1 or matching the direction of the first derivative (tangent) when transitioning from one curve to another at a join point. If the tangents match in both magnitude and direction, that is called C-1 continuity. The spline used to approximate the normal distribution curve in this example is C-1 continuous.

Quadratic spline approximation is efficient for the normal curve because the curve is symmetric about the mean and it has only two (well-known) inflection points. An inflection point is where the sign of the second derivative changes, i.e. the curve moves from concave upward to concave downward (or vice versa). Quadratic curves do not handle inflections, so it’s important to place an interpolation point at an inflection if one is known in advance. The normal curve’s inflection points are at u +/- s where u is the mean and s is the standard deviation.

The normal curve has predictable curvature as the x-coordinate moves away from the mean and approaches zero as |x| increases. This means that only a small number of quad Bezier curves are needed to approximate the curve in [a, u]. Symmetry can be exploited to produce the remaining sequence in [u, b].

Tangents at join points are created by evaluating the first derivative of the normal curve and then extending a vector a unit amount in x in opposite directions at the join point. The y-coordinate of each tangent line at the corresponding x-coordinate is evaluated to provide two points on each tangent line.

Given two interpolation points and two tangents, an intersection point can be computed for the middle control point required to define a quadratic Bezier curve. These control points are called the geometric constraints of Bezier curve.

Now, how do we handle a requirement to draw the area under the normal curve at some arbitrary point, x? That x-coordinate is almost certain not to be at the x-value of an interpolation point; it’s likely to be between interpolation points.

A Bezier curve can be subdivided at any natural parameter value, t, in the open interval (0, 1) using De Casteljau subdivision.

https://en.wikipedia.org/wiki/De_Casteljau%27s_algorithm

Given a value of x in [a, b], first determine which quadratic Bezier curve contains that x-coordinate. Next, we need to determine the natural parameter value, t, that corresponds to that x-coordinate. This operation and even arc-length parameterization are handled by my Typescript Math Toolkit Quadratic Bezier supplied in the libs folder of the code distribution for this article.

Once the relevant curve is subdivided, the original spline can be separated into two independent splines, each of which represents the section of the normal curve in [a, x] and [x, b]. These can be used, for example, to draw shaded sections under the curve and above the x-axis.

In fact, these techniques can be extended even further to produce highly customized visualizations. The image below shows an example of a natural cubic spline fit to set of data points.

http://algorithmist.net/docs/spline.pdf

This cartesian spline was recursively subdivided to create an approximating quadratic Bezier spline. The sequence of quadratic Bezier curves was further subdivided at intersection points with the x-axis. A list of curves above and below the x-axis was created from that subdivision process.

Numerical integration was then applied to compute the ‘amount’ of curve above and below the horizontal axis. The final sequence of Bezier curves was then used to color the sections above and below the x-axis. The process is so efficient that it allows the complete curve to be smoothly animated into display, even on devices.

The breadth of applications that can be created using the techniques covered in this article is rather large. So, deconstruct the code, embrace the equation, and your journey to the math side will be complete :)

EnterpriseNG is coming November 4th & 5th, 2021.

Come hear top community speakers, experts, leaders, and the Angular team present for 2 stacked days on everything you need to make the most of Angular in your enterprise applications.
Topics will be focused on the following four areas:
• Monorepos
• Micro frontends
• Performance & Scalability
• Maintainability & Quality
Learn more here >> https://enterprise.ng-conf.org/

--

--

Jim Armstrong
ngconf
Editor for

Jim Armstrong is an applied mathematician who began his career writing assembly-language math libraries for supercomputers. He now works on FE apps in Angular.