Triad Balance

The Three Attributes

Attributes Calculated from Triangle Position:
0%
0%
0%
Click to select attributes based on position.
Balancing Norm to Use:
Triangle Orientation:

Triad Balance Diagrams

This interactive page presents what may be a new graphical user interface element — the "triad-balance" diagram. The purpose is to use a single click on a triangle to enter three values such as "Mind/Body/Sprit" or "Strength/Endurance/Flexibility" that are presumed to in some sense be "in balance", or where it is interesting if they are in balance. The Triad Balance widget is fully responsive and stylable in css.

This is an open source demonstration project of Public Invention. This grew out of the project Social Tetrahedrons instigated by Mark Frazier. Although I searched a good bit, I may have missed a previous implementation. If the triad-balance diagram has been implemented previously please inform me at <read.robert@gmail.com>.

Usage

Click near the yellow triangle and a red dot will appear within the triangle and will determine three values. These three values will either sum to 100% or the square of the square roots will sum to 100%. The closer to a corner of the triangle, the more strongly that attribute is attributed. The center of the triangle reflects all attributes equally — that is, "perfect balance." The weights of these attributes will be determined by your assertion, and displayed above the triangle.

Purpose

Clicking on the triangle gives you a two-dimensional specification. In some cases, that may be all you need. Suppose, however, that you wish to write an algorithm or analyze data for a large number of people or project to determine which is the most out of balance, or which has the highest "mind" value, or which two complement each other the most. The two-dimensional diagram is inconvenient for data analysis of any kind because it does not match the attributes you are trying to measure. Even if you imagine the points in polar coordinates, it is unclear what mix of the three attributes is represented by a point.

Instead, calculating a 3-dimensional "balance vector" representing the proportion of each of the three attributes from the 2-dimensions makes more sense. It is only possible to specify three values from a two-dimensional coordinate by assuming the values are in some sense balance and not completely independent of each other.

Additionally, to be useful, the function that produces the "balance vector" from a point on the triangle must have an inverse. That is, there must exist code which when given a "balance vector" and the shape of a triangle as input, computes the coordinates of the same point you started with. This allows you to, for example, store balance vectors in a database and reconstruct points on the GUI at will. Moving back and forth between the model and the view is essential.

L1 Norm vs. L2 Norm

To be able to write algorithms about the similarity or complementarity of, for example, a person to a project, in terms of Mind/Body/Spirit or Strength/Endurance/Flexibility balance, we need a function to translate a position on the triangle into a mathematical object of some kind. We also must be able to invert this function, to obtain a position on the triangle from a mathematical object.

The most obvious way to do this is to treat a given assertion as a vector with dimensions labeled, for example, Mind/Body/Spirit. Then a balance attribute vector maybe thought of as any vector of non-negative components of unit length. There are different ways to express the length of a vector. The L1 Norm is simply the sum of the absolute values, so by choosing that, all dimensions sum to 1.0.

However, it is also reasonable to use the L2 Norm, which is the "Euclidean distance." This is rather like saying every vector is a point on the surface of a sphere (or an eighth of a sphere) of radius 1.0. The components of the L2 Norm will usually sum to something greater than 1.0.

How to Use for Programmers

To use the Triad Balance Diagram, you need to include some common libraries and files from this repo into an HTML page which has an svg element you designate for the diagram, and provide a callback which will return to you the "balance vector" whenever the user clicks. These files are:

  1. TriadBalanceMath.js, a standalone file that needs only the THREE.js library and contains the math.
  2. TriadBalanceDiagram.js, which loads a diagram into an svg element that you provide.
  3. TriadBalance.css, which provides a example of styling all the aspects of the diagram which are stylable.
You can see that on this page, or you may prefer the somewhat simpler JSFiddle below.

Because the elements of the diagram are drawn with SVG, they can mostly be styled with CSS, which provides some non-programmatic flexibility. Other aspects of the configuration make more sense to set-up in JavaScript, such as the labels for the attributes.

 
#triad-balance-triangle {
    fill: lemonchiffon;
}

.triad-vertices-labels {
    fill: blue;
    text-anchor: middle;
}

#triad-vertex-label-0 {
    fill: green;
}

#triad-vertex-label-1 {
    fill: blue;
}

#triad-vertex-label-2 {
    fill: red;
}

#triangle_origin {
    fill: black;
    stroke: black;
    stroke-width: 1px;
}

.triad-marker {
    fill: red;
    stroke: red;
    stroke-width: 1px;
    r: 3px;
}
Other aspects of configuration are done in JavaScript, although they are relative to the size of the SVG elment, which will resize itself correctly. The user doe not have to specify the size of the diagram, other than to design and the control the size of the svg element which contains it (probably in .css). The responsive of the diagram is demonstrated by this page.

function init() {
  // HERE IS THE INITIALIZATION OF THE DIAGRAM
  let GLOBAL_SVG_ID = "create_svg";
  TBS = new triadb.TriadBalanceState();
  TBS.SVG_ELT = document.getElementById(GLOBAL_SVG_ID);
  TBS.CLICK_CALLBACK = ((tp,tpi,bal) => setBalance(bal));
  TBS.LABELS = THREE_DIMENSIONS[CUR_LABELS];

  // These are optional configuraton settings, which have defaults.
  TBS.POINTS_UPWARD = true;
  TBS.FONT_SIZE_RATIO_TO_HEIGHT = 1/20;
  // These value look good to me but I am not a web designer.
  // 4/5 is generally too high for this!
  TBS.SIDE_TO_MIN_BOUND = 7/10;
  // The percentage to push the origin down to make it look balanced.
  TBS.Y_DISP_PER_CENT = 10;

  TBS.NORM_TO_USE = (getRadioValue("norm") == 0 ? triadb.L1 : triadb.L2);

  triadb.initialize_triad_diagram(TBS);
}
The JSFiddle allows you to play with manipulating style the diagram (including colors, etc.) in .css.

There is a single callback which is called when the user clicks, and returns the balance vector, which is the heart of the diagram.

The diagram is responsive; that is, it will change size more or less correctly, you can try it on this page.

Additionally, I have tried to give all of the elements clear ids and classes in .css, so that significant styling may be done entirely in css. However, some actions (such as adding multiple markers on the same diagram) cannot be implemented with styling and would require extensions to the current JavaScript code.

Basic Geometric Algorithm

A diagram of the geometry of the algorithm with variables
Fig.1 - Geometric quantities represented by variables in the algorithm TriadBalance2to3

Please refer to the diagram above when considering the algorithm TriadBalance2to3 below. In this code, I use a slightly extended version of a light-weight, purely functional library, vec-la-fp by Francis Stokes, which has a nice write-up. This vector library is extremely small, keeping the load time for the total diagram code very low. Additionally, I use a slight modification of a routine GetRayToLineSegmentIntersection provided on Stack Overflow by ezolotko.

The basic geometric algorithm to compute the three values from a point on the triangle is straightforward. The basic insight is that any point specifies an amount of "balance", proportionally to how close it is to the center of the triangle, represented in the vector bal and an amount of imbalance, depending on where a ray intersects the edge, represented by the variable imb. The final result is a linear combination of these two unit-length vectors, which is guaranteed by linearity to have a unit-length norm. Note that the function is parametrized by the norming function, we can use either the L1 or L2 norm (or any other.)

Before we begin, let us note that the special case of the input point being precisely the origin or a vertex is in fact handled correctly, although this is perhaps not obvious; this is tested in our unit test suite.

The TriadBalance2to3 algorithm takes as input a triangle point, three vertices, and a selection of which norm to use (L1 and L2 norms are provided).


// tp is a point in the 2-dimensional triangle space
// wtc are the three vertices of an eqilateral triangle whose centroid is the origin
// LXnorm_and_length is a pair of functions to to normalize a vector and compute the length
// return the corresponding 3-vector in the attribute space
function TriadBalance2to3(p,wtc,LXnorm_and_length = L2) {
          

For clarity, we provide a name for the normalization function.


              let LXnormalize = LXnorm_and_length[0];
          

If the point is the exact center of the triangle, we cannot construct a ray to an edge, and we return a perfectly balanced vector.



  if (vec.scalarNear(1e-5,vec.mag(p),0)) {
    return LXnormalize([1,1,1]);
  }

              }
          

We then seek the point point_on_edge where the line from the center of the triangle through the point p intersects a triangle edge. Such a point must exists, and is on one edge or the intersection of two edges. Imagine a ray from the origin through point p to point_on_edge. The point point_on_edge is along the ray from origin to p and length is at most the distance from the origin to the edge along this ray (to keep the point inside the triangle.) Determining which edge the ray hits is done in a subroutine getEdgeAndPoint



  // Now we want to do a linear interpolation of how far we are from an edge,
  // but also how far the projection to the edge is between the vertices.
  // We must first decide which edges the line from the orign to p intersects.
  // If it intersects two segments, then it is aimed at a vertex.
  let [fe_idx,point_on_edge] = getEdgeAndPoint(wtc,p);
  // now point_on_edge is a point on edge fe_idx.
                                               

vec-la-fp provides a function clampMag which limits the length of a vector to an input value. Since we know the distance from the orign to point_on_edge, the act of clamping p brings it to the edge of the triangle if it is outside, and leaves it unchanged if inside. Compute the length from p to point_on_edge, and the total length from origin to p.



  // now point_on_edge is a point on edge fe_idx.
  const total_distance_to_edge = vec.dist([0,0],point_on_edge);

  // If the point is outside the triangle, we clamp (truncate if needed)
  // it's length so that it is precisely on the edge.
  const pc = vec.clampMag(0,total_distance_to_edge,p);

  const distance_to_p_o_e = vec.dist(pc,point_on_edge);
          

Determine the ratio ratio_p_to_edge. If this is close to zero, we are close to perfectly balanced; if it is close to 1, we are close to having zero for one attribute. Compute the a balanced vector bal with all values the same, scaled by the ratio to the ratio_p_to_edge.


  var ratio_p_to_edge =  distance_to_p_o_e/total_distance_to_edge;

  let bal = v3c.scale(ratio_p_to_edge,
                      LXnormalize([1,1,1]));
          

Now to compute the "imbalance", we linearly divide the edge into the distance to the two vertices. Possibly one of these lengths will be zero.



  // Now the remainder of the contribution
  // to the unit vector should come from the two
  // points on the edge, in linear proportion.
  // These coordinates are fe_idx and (fe_idx+1) % 3.
  const d1 = vec.dist(wtc[fe_idx],point_on_edge);
  const d2 = vec.dist(wtc[(fe_idx+1) % 3],point_on_edge);
          

We assign these two distances to the corresponding attributes in a 3-vector. The remaining one is zero. We then normalize the vector, driving it's length to one, and then scale it by an amount chosen such that by linearity the sum with the balance vector will be one: (1 - ratio_p_to_edge).


                let vs = [0,0,0];
                vs[fe_idx] = d2;
                vs[(fe_idx+1) % 3] = d1;

                let imb = v3c.scale(1 - ratio_p_to_edge,LXnormalize(vs));
            

Now the vector bal and the vector imb added together are the return value.


              // now construct a balanced vector proportional
              // to the length from the edge to the point p towards the axis
              // so that this be a unit vector if p is the origin.
              return v3c.add(imb,bal);
              }
          

The Inverse Function

The function TriadBalance2to3 calculates a 3-vector from the clicked point on the triangle; the inverse function invertTriadBalance2to3 gives you back the clicked point when given the 3-vector. The utility of producing a 3-vector to represent the 3 attributes in balance depends entirely on having such an inverse function; no alternative TriadBalance2to3 function which does not have an inverse can be useful.

The fundamental insight of this algorithm is that since the ray through the triangle point touches at most two edges, there is always a component of the attribute vector that is purely contributed by the "balance" component, and it is always a minimum of the values in the vector. We can therefore find this minimum, construct a balance vector with those values, subtract it from the input, and have a vector representing pure imbalance which has at least one zero in it. The two non-zero values represent a linear interpolation along the edge (or point, if two edges) that the ray strikes. Since we constructed the attribute value as a ratio of lengths between the vertices, we can use this fact to reconstruct, via linear interpolation, a point on the edge. A second linear interpolation from the point based on the imbalance length gives us the point in 2-space.


  // vec is a 3-vector in the attribute space
  // wtc are the three vertices of an eqilateral triangle whose centroid is the origin
  // LXnorm_and_length is a pair of functions to to normalize a vector and compute the length
  // return the corresponding 2-vector in the triangle space
  function invertTriadBalance2to3(v,wtc,LXnorm_and_length = L2) {
  let length = LXnorm_and_length[1];

  let min = Math.min(Math.min(v[0],v[1]),v[2]);

  let imb = [v[0] - min,v[1] - min,v[2] - min];
  let bal = v3c.sub(v,imb);
    bal.sub(imb);
          

Now it is the case that by construction, imb has has at least one zero (whichever attributes were minimal), and bal has all attributes equal.


              // Now that we have balance, we need to compute it's length,
              // which is dependent on the norm we chose!

              let imb_r = length(imb);
              let bal_r = length(bal);
              console.assert(Math.abs((bal_r+imb_r) - 1) <   1e-5);
                                                             

The ratio computed below is a ratio of values in attribute space; it is not obvious therefore that this can be used to perform a linear interpolation in the triangle coordinate space. However, we intensionally constructed the attribute vector in proportion to the distance in the triangle space, so this works no matter which norm we use in the attribute space.


  // Now we have the ratios. We need to determine the direction.
  // This is a function of the imbalance vector. We can determine
  // which side we are on, and then compute our position along that
  // to determine a point on the triangle, and then multiply by the imb_r
  // to obtain the actual point.
  // At least one value of imb will be zero.
  var from_v,to_v,ratio;
  // the points are OPPOSITE the zero
  // ratio will be the ratio along the triangle edge
  // it requires a little thought to understand which
  // of the other points should be the "from_v" and the "to_v"
  // for the interpolation which occurs later.
  let s = imb[0] + imb[1] + imb[2]; // one of these is always zero.
  if (imb[0] == 0) {
    from_v = wtc[2];
    to_v = wtc[1];
    ratio = imb[1]/s;
  } else if (imb[1] == 0) {
    from_v = wtc[0];
    to_v = wtc[2];
    ratio = imb[2]/s;
  } else if (imb[2] == 0) {
    from_v = wtc[1];
    to_v = wtc[0];
    ratio = imb[0]/s;
  }
          

The THREE.js library provides is a linear interpolation between two vectors, named lerpVectors, out of the box. We use this once to interpolate along the edge, and then once to interpolate from th origin toward this point.



  // The point on the triangle is by construction
  // on one edge of the triangle.
  const onTriangle = vec.lerp(from_v,to_v,ratio);
  // now onTriangle is a point on the triangle
  // now, having found that we interpolate a ray
  // to it of length imb_r...
  return vec.lerp([0,0],onTriangle,imb_r);
              }
          

Alternative Approaches

I am not a user interface expert; nonetheless I offer these comments.

The obvious alternative is to use three sliders. However, this at a minimum requires the user to make two clicks. It also does not visually represent the concept of interdependence, so possibly this approach is more intuitive.

A possible alternative which may be more attractive and in a sense more elegant is to use a circle, with the same triangle inscribed. This would be similar, but make better use of the screen space corresponding to the bounding box. The math would be slightly different, and probably simpler, than what is presented here.

Finally, it might be possible to define a GUI element that allows you to select from not three, but four or more dimensions, such as the four elements of antiquity: Earth/Air/Water/Fire. However, this would necessarily impose an even further constraint than "balance" on what is possible. For example, if any number of elements can be arranged radially in a natural way so that being close to one completely excludes being close to those that are two or more elements away, this could be a natural system.

How to Best Contribute

Public Invention is a non-profit promoting the idea of "Inventing in the public, for the public." We are always seeking volunteer Public Inventors and Invention Coaches. Additionally, we always need web designers, artists, programmers and writers, and this project is no exception, although it is at a stable point for what it is. For example, to make this more easily reusable the color and text configuration should be controlled by CSS.

This project is contributing JavaScript and math which makes implementing a TriadBalance diagram easy for a JavaScript programmer. However, the 200 lines of code needed to implement the math could easily be translated to some other language. As is, the code should be easy to include on any webpage. I would love to have somebody take this code even further make a d3 or React component out of it, so that it could be enjoyed by others as a plug-in as easy using another d3 GUI element, for example.

My understanding of the JavaScript event model is weak, as is my understanding of package management---both of these are areas where this project could probably be improved.

An optimization

The current approach works with any equilateral triangle centered on the origin, using vector math. If you fix the triangles orientation and size, it is possible to optimize some of the math so that fewer vector operations were required. This is represented by the code eqEdgeAlgebraically, eqPointOnEdgeAlgebraically and isCenteredEquilateral. I have implemented this only for the upward-pointing case. The downward pointing case would be similar. The algorithm described here is completely general, however, and should work with an arbitrary rotation.

The Test Suite

The file test/TriadBalanceTest.js contains a test suite that tests the math in ./src/TriadBalanceMath.js. Since these functions make the most sense in a browser, we normally invoke testAllTriadBalance(WORLD_TRIANGLE_COORDS) them from within a browser rather than with Mocha. When executed on loading, the will print failed assertions on your browser console or GREEN: TESTS ALL GREEN! message on the console in your browser.

A Weakness

The code sets the viewport of the SVG so that the center of the triangle is the center of the world coordinates. This might make it hard for a programmer to insert additional elements into the SVG, unless they are willing to do so relative to the center of the triangle, not the center of the bounding box. This would be a consideration if one wanted a richer graphical presentation, exemplified by the Scutum Fidei.

License

You are free to reuse this software under the terms of the GNU General Public License. Although the code is covered by the GPL, the algorithm is not. Please re-code the math and algorithm here and use it as you see fit.

Contact me (<read.robert@gmail.com>) with any questions or comments.

Acknowledgements

Thanks to Mark Frazier for the concept and Sean Johnson for a proofread and code review. Thanks to Francis Stokes for the vec-la-fp library, and the prism library supporting the literate-programming style attempted here.