Create an Animated Chart of Nested Squares Utilizing Masks

No Comments

We’ve got many well-known chart sorts: bar, donut, line, pie, you title it. All common chart libraries assist these. Then there are the chart sorts that don’t actually have a title. 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 easy. One method to do it’s is to stack parts (e.g. SVG <rect> parts, 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 must 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> parts. In case you are solely new to masks, you might be in the correct place. That is an introductory-level article. In case you are extra seasoned, then maybe this cut-out impact is a trick that you would be able to take with you.

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

For instance, right here’s how we must characterize 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 rationalization of the syntax.

The fundamentals of the chart

Given an information 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 need 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 is going to turn out to be obvious in a second why we want 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 now we have a small dataset, we are able to 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 your entire 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 massive is the 6321 rectangle?

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

For our functions, we’re going to implement the operate 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 again and again, we are able to create a wrapper operate:

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 parts

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

Discover that we’re utilizing the createElementNS as a substitute of the extra frequent createElement. It’s because we’re working with an SVG. HTML and SVG parts have totally different specs, so that they fall beneath a distinct namespace URI. It simply occurs that the createElement conveniently makes use of the HTML namespace! So, to create an SVG, now we have to be this verbose:

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

Certainly, we are able to create one other helper operate:

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

Once we are appending the rectangles to the DOM, now we have to concentrate to their order. In any other case, we must specify the z-index explicitly. The primary rectangle must be the most important, and the final rectangle must be the smallest. Greatest to type the information earlier than the loop.

const information = rawDataSet.type(
(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 should set it. The y attribute controls the vertical place.

To offer the visible impression that the entire rectangles originate from the identical level that touches their bottom-left corners, now we have 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:

CodePen Embed Fallback

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

Cutout rectangles

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

If we concentrate on the “lacking components” then we are able to see they cutouts of the identical rectangles we’re already working with.

We need to cover 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. Sometimes, the masks is inlined within the <svg> component it belongs to. And, usually, it ought to have a novel id as a result of now we have to reference it in an effort 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 method to outline or apply a masks, but it surely’s probably the most easy means for this demo. Let’s do a little bit of experimentation earlier than writing any code to generate the masks.

We mentioned that we need to cowl the cutout areas that match the sizes of the prevailing rectangles. If we take the most important component 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 component 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 (colour) we select.

The white fill

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

Now, our giant rectangle is similar 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 crammed with black is what turns into invisible. We management the visibility of masks utilizing white and black fills. The dashed strains are there as a visible assist 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 are able to 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 realized about masks up to now:

The component 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 got solely used one form for the masks, however as with all basic function HTML tag, we are able to nest as many baby parts in there as we would like. In reality, the trick to attain what we would like is utilizing two SVG <rect> parts. We’ve got to stack them one on high 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>

Certainly one of our masking rectangles is crammed with white; the opposite is crammed with black. Even when we all know the foundations, let’s check out the chances.

<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 component and the most important component is crammed with black. Meaning every little thing beneath that space is invisible. And every little thing beneath the smaller rectangle is seen.

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

<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 thing beneath the most important white-filled rectangle is seen, however the smaller black rectangle is on high of it (nearer to us on the z-axis), masking that half.

Producing the masks

Now that we all know what now we have to do, we are able to create the masks with relative ease. It’s much 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, not less than to me:

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

As for including the smaller rectangle within the masks, now we have quick access the worth we want as a result of we beforehand ordered the rectangle values from highest to lowest. Meaning the subsequent component within the loop is the smaller rectangle, the one we should always reference. And we are able to 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 authentic loop. It ought to match the format we selected:

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

The ultimate consequence

And we’re executed! 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> component to attract the cutout space of every sq..

CodePen Embed Fallback

The publish Create an Animated Chart of Nested Squares Utilizing Masks appeared first on CSS-Methods. You possibly can assist CSS-Methods by being an MVP Supporter.

    About Marketing Solution Australia

    We are a digital marketing company with a focus on helping our customers achieve great results across several key areas.

    Request a free quote

    We offer professional SEO services that help websites increase their organic search score drastically in order to compete for the highest rankings even when it comes to highly competitive keywords.

    Subscribe to our newsletter!

    More from our blog

    See all posts

    Leave a Comment