We’ve many well-known chart sorts: bar, donut, line, pie, you identify it. All fashionable chart libraries help these. Then there are the chart sorts that don’t actually have a identify. Try this dreamt-up chart with stacked (nested) squares that may assist visualize relative sizes, or how totally different values evaluate to at least one one other:

What we’re making

With none interactivity, creating this design is pretty simple. One strategy to do it’s is to stack components (e.g. SVG <rect> components, and even HTML divs) in lowering sizes, the place all of their bottom-left corners contact the identical level.

However issues get trickier as soon as we introduce some interactivity. Right here’s the way it ought to be: Once we transfer our mouse over one of many shapes, we would like the others to fade out and transfer away.

We’ll create these irregular shapes utilizing rectangles and masks — literal <svg> with <rect> and <masks> components. In case you are solely new to masks, you’re in the precise place. That is an introductory-level article. In case you are extra seasoned, then maybe this cut-out impact is a trick which you can take with you.

Now, earlier than we start, you might marvel if a greater various to SVG to utilizing customized shapes. That’s undoubtedly a chance! However drawing shapes with a <path> could be intimidating, and even get messy. So, we’re working with “simpler” components to get the identical shapes and results.

For instance, right here’s how we must signify the most important blue form utilizing a <path>.

<svg viewBox="0 0 320 320" width="320" peak="320">
  <path d="M320 0H0V56H264V320H320V0Z" fill="#264653"/>
</svg>

If the 0H0V56… doesn’t make any sense to you, take a look at “The SVG path Syntax: An Illustrated Information” for a radical clarification of the syntax.

The fundamentals of the chart

Given a knowledge set like this:

kind DataSetEntry = {
  label: string;
  worth: quantity;
};

kind DataSet = DataSetEntry[];

const rawDataSet: DataSet = [
  { label: 'Bad', value: 1231 },
  { label: 'Beginning', value: 6321 },
  { label: 'Developing', value: 10028 },
  { label: 'Accomplished', value: 12123 },
  { label: 'Exemplary', value: 2120 }
];

…we wish to find yourself with an SVG like this:

<svg viewBox="0 0 320 320" width="320" peak="320">
  <rect width="320" peak="320" y="0" fill="..."></rect>
  <rect width="264" peak="264" y="56" fill="..."></rect>
  <rect width="167" peak="167" y="153" fill="..."></rect>
  <rect width="56" peak="56" y="264" fill="..."></rect>
  <rect width="32" peak="32" y="288" fill="..."></rect>
</svg>

Figuring out the very best worth

It would grow to be obvious in a second why we’d like the very best worth. We are able to use the Math.max() to get it. It accepts any variety of arguments and returns the very best worth in a set.

const dataSetHighestValue: quantity = Math.max(
  ...rawDataSet.map((entry: DataSetEntry) => entry.worth)
);

Since we’ve got a small dataset, we will simply inform that we’ll get 12123.

Calculating the dimension of the rectangles

If we take a look at the design, the rectangle representing the very best worth (12123) covers the complete space of the chart.

We arbitrarily picked 320 for the SVG dimensions. Since our rectangles are squares, the width and peak are equal. How can we make 12123 equal to 320? How in regards to the much less “particular” values? How large is the 6321 rectangle?

Requested one other manner, how can we map a quantity from one vary ([0, 12123]) to a different one ([0, 320])? Or, in additional math-y phrases, how can we scale a variable to an interval of [a, b]?

For our functions, we’re going to implement the perform like this:

const remapValue = (
  worth: quantity,
  fromMin: quantity,
  fromMax: quantity,
  toMin: quantity,
  toMax: quantity
): quantity => {
  return ((worth - fromMin) / (fromMax - fromMin)) * (toMax - toMin) + toMin;
};

remapValue(1231, 0, 12123, 0, 320); // 32
remapValue(6321, 0, 12123, 0, 320); // 167
remapValue(12123, 0, 12123, 0, 320); // 320

Since we map values to the identical vary in our code, as a substitute of passing the minimums and maximums time and again, we will create a wrapper perform:

const valueRemapper = (
  fromMin: quantity,
  fromMax: quantity,
  toMin: quantity,
  toMax: quantity
) => {
  return (worth: quantity): quantity => {
    return remapValue(worth, fromMin, fromMax, toMin, toMax);
  };
};

const remapDataSetValueToSvgDimension = valueRemapper(
  0,
  dataSetHighestValue,
  0,
  svgDimension
);

We are able to use it like this:

remapDataSetValueToSvgDimension(1231); // 32
remapDataSetValueToSvgDimension(6321); // 167
remapDataSetValueToSvgDimension(12123); // 320

Creating and inserting the DOM components

What stays has to do with DOM manipulation. We’ve to create the <svg> and the 5 <rect> components, set their attributes, and append them to the DOM. We are able to do all this with the essential createElementNS, setAttribute, and the appendChild features.

Discover that we’re utilizing the createElementNS as a substitute of the extra frequent createElement. It is because we’re working with an SVG. HTML and SVG components have totally different specs, in order that they fall below a unique namespace URI. It simply occurs that the createElement conveniently makes use of the HTML namespace! So, to create an SVG, we’ve got to be this verbose:

doc.createElementNS('http://www.w3.org/2000/svg', 'svg') as SVGSVGElement;

Absolutely, we will create one other helper perform:

const createSvgNSElement = (factor: string): SVGElement => {
  return doc.createElementNS('http://www.w3.org/2000/svg', factor);
};

Once we are appending the rectangles to the DOM, we’ve got to concentrate to their order. In any other case, we must specify the z-index explicitly. The primary rectangle needs to be the most important, and the final rectangle needs to be the smallest. Greatest to kind the info earlier than the loop.

const information = rawDataSet.kind(
  (a: DataSetEntry, b: DataSetEntry) => b.worth - a.worth
);

information.forEach((d: DataSetEntry, index: quantity) => {
  const rect: SVGRectElement = createSvgNSElement('rect') as SVGRectElement;
  const rectDimension: quantity = remapDataSetValueToSvgDimension(d.worth);

  rect.setAttribute('width', `${rectDimension}`);
  rect.setAttribute('peak', `${rectDimension}`);
  rect.setAttribute('y', `${svgDimension - rectDimension}`);

  svg.appendChild(rect);
});

The coordinate system begins from the top-left; that’s the place the [0, 0] is. We’re all the time going to attract the rectangles from the left facet. The x attribute, which controls the horizontal place, defaults to 0, so we don’t must set it. The y attribute controls the vertical place.

To present the visible impression that all the rectangles originate from the identical level that touches their bottom-left corners, we’ve got to push the rectangles down so to talk. By how a lot? The precise quantity that the rectangle doesn’t fill. And that worth is the distinction between the dimension of the chart and the actual rectangle. If we put all of the bits collectively, we find yourself with this:

We already added the code for the animation to this demo utilizing CSS.

Cutout rectangles

We’ve to show our rectangles into irregular shapes that kind of seem like the quantity seven, or the letter L rotated 180 levels.

If we give attention to the “lacking elements” then we will see they cutouts of the identical rectangles we’re already working with.

We wish to conceal these cutouts. That’s how we’re going to find yourself with the L-shapes we would like.

Masking 101

A masks is one thing you outline and later apply to a component. Usually, the masks is inlined within the <svg> factor it belongs to. And, typically, it ought to have a singular id as a result of we’ve got to reference it with a view to apply the masks to a component.

<svg>
  <masks id="...">
    <!-- ... -->
  </masks>
</svg>

Within the <masks> tag, we put the shapes that function the precise masks. We additionally apply the masks attribute to the weather.

<svg>
  <masks id="myCleverlyNamedMask">
    <!-- ... -->
  </masks>
  <rect masks="url(#myCleverlyNamedMask)"></rect>
</svg>

That’s not the one strategy to outline or apply a masks, nevertheless it’s probably the most simple manner for this demo. Let’s do a little bit of experimentation earlier than writing any code to generate the masks.

We mentioned that we wish to cowl the cutout areas that match the sizes of the prevailing rectangles. If we take the most important factor and we apply the earlier rectangle as a masks, we find yourself with this code:

<svg viewBox="0 0 320 320" width="320" peak="320">
  <masks id="theMask">
    <rect width="264" peak="264" y="56" fill=""></rect>
  </masks>
  <rect width="320" peak="320" y="0" fill="#264653" masks="url(#theMask)"></rect>
</svg>

The factor contained in the masks wants a fill worth. What ought to that be? We’ll see solely totally different outcomes primarily based on the fill worth (shade) we select.

The white fill

If we use a white worth for the fill, then we get this:

Now, our massive rectangle is identical dimension because the masking rectangle. Not precisely what we needed.

The black fill

If we use a black worth as a substitute, then it appears like this:

We don’t see something. That’s as a result of what’s stuffed with black is what turns into invisible. We management the visibility of masks utilizing white and black fills. The dashed traces are there as a visible support to reference the scale of the invisible space.

The grey fill

Now let’s use one thing in-between white and black, say grey:

It’s neither totally opaque or stable; it’s clear. So, now we all know we will management the “diploma of visibility” right here through the use of one thing totally different than white and black values which is an effective trick to maintain in our again pockets.

The final bit

Right here’s what we’ve coated and discovered about masks up to now:

  • The factor contained in the <masks> controls the dimension of the masked space.
  • We are able to make the contents of the masked space seen, invisible, or clear.

We’ve solely used one form for the masks, however as with every common objective HTML tag, we will nest as many youngster components in there as we would like. The truth is, the trick to realize what we would like is utilizing two SVG <rect> components. We’ve to stack them one on prime of the opposite:

<svg viewBox="0 0 320 320" width="320" peak="320">
  <masks id="maskW320">
    <rect width="320" peak="320" y="0" fill="???"></rect>
    <rect width="264" peak="264" y="56" fill="???"></rect>
  </masks>
  <rect width="320" peak="320" y="0" fill="#264653" masks="url(#maskW320)"></rect>
</svg>

One among our masking rectangles is stuffed with white; the opposite is stuffed with black. Even when we all know the principles, let’s check out the probabilities.

<masks id="maskW320">
  <rect width="320" peak="320" y="0" fill="black"></rect>
  <rect width="264" peak="264" y="56" fill="white"></rect>
</masks>

The <masks> is the dimension of the most important factor and the most important factor is stuffed with black. Meaning the whole lot below that space is invisible. And the whole lot below the smaller rectangle is seen.

Now let’s do flip issues the place the black rectangle is on prime:

<masks id="maskW320">
  <rect width="320" peak="320" y="0" fill="white"></rect>
  <rect width="264" peak="264" y="56" fill="black"></rect>
</masks>

That is what we would like!

Every part below the most important white-filled rectangle is seen, however the smaller black rectangle is on prime of it (nearer to us on the z-axis), masking that half.

Producing the masks

Now that we all know what we’ve got to do, we will create the masks with relative ease. It’s just like how we generated the coloured rectangles within the first place — we create a secondary loop the place we create the masks and the 2 rects.

This time, as a substitute of appending the rects on to the SVG, we append it to the masks:

information.forEach((d: DataSetEntry, index: quantity) => {
  const masks: SVGMaskElement = createSvgNSElement('masks') as SVGMaskElement;

  const rectDimension: quantity = remapDataSetValueToSvgDimension(d.worth);
  const rect: SVGRectElement = createSvgNSElement('rect') as SVGRectElement;

  rect.setAttribute('width', `${rectDimension}`);
  // ...setting the remainder of the attributes...

  masks.setAttribute('id', `maskW${rectDimension.toFixed()}`);

  masks.appendChild(rect);

  // ...creating and setting the attributes for the smaller rectangle...

  svg.appendChild(masks);
});

information.forEach((d: DataSetEntry, index: quantity) => {
    // ...our code to generate the coloured rectangles...
});

We may use the index because the masks’s ID, however this appears a extra readable choice, a minimum of to me:

masks.setAttribute('id', `maskW${rectDimension.toFixed()}`); // maskW320, masW240, ...

As for including the smaller rectangle within the masks, we’ve got easy accessibility the worth we’d like as a result of we beforehand ordered the rectangle values from highest to lowest. Meaning the following factor within the loop is the smaller rectangle, the one we must always reference. And we will try this by its index.

// ...earlier half the place we created the masks and the rectangle...

const smallerRectIndex = index + 1;

// there is not any subsequent one after we are on the smallest
if (information[smallerRectIndex] !== undefined) {
  const smallerRectDimension: quantity = remapDataSetValueToSvgDimension(
    information[smallerRectIndex].worth
  );
  const smallerRect: SVGRectElement = createSvgNSElement(
    'rect'
  ) as SVGRectElement;

  // ...setting the rectangle attributes...

  masks.appendChild(smallerRect);
}

svg.appendChild(masks);

What’s left is so as to add the masks attribute to the coloured rectangle in our unique loop. It ought to match the format we selected:

rect.setAttribute('masks', `url(#maskW${rectDimension.toFixed()})`); // maskW320, maskW240, ...

The ultimate end result

And we’re carried out! We’ve efficiently made a chart that’s made out of nested squares. It even comes aside on mouse hover. And all it took was some SVG utilizing the <masks> factor to attract the cutout space of every sq..

#Create #Animated #Chart #Nested #Squares #Masks

Leave a Reply

Your email address will not be published. Required fields are marked *