Creating custom filters

amCharts 4 comes with a few basic rudimentary filters like "drop shadow", "desaturate", and a few others. However, you can do much much more with SVG filters. This tutorial will explain how you can create your own custom amCharts 4-compatible filter classes.

What is a Filter?

SVG filters

An SVG filter (represented by <filter> tag) is a set of rules that allow changing appearance of any element. It might be something very subtle, like reducing tone of the colors, or something that dramatically changes appearance of the element beyond recognition.

SVG filters is a very powerful concept opening a lot of possibilities. However, explaining how they work is beyond the scope of this tutorial. If you're not familiar with the concept, make sure you familiarize yourself on the topic. This MDN article seems like a good place to start.

amCharts 4 filters

amCharts 4 filter is a class that extends our own Filter class, and contains code that creates a set of filter rules (primitives).

You can apply amCharts 4 filter to any element by instantiating filter class and pushing it to element's filters array.

MORE INFO For more information about how filters are used in amCharts 4, please refer to "Using filters" tutorial.

Creating a filter

Filter class

Let's start with something very easy. Probably everyone is aware of the "Sepia" effect. It gives photos, or any other image, a dated old photo feel. Let's do that.

As we mentioned earlier in this tutorial, creating a new filter is about creating a class that extends built-in Filter class.

We're going to predictably call our new filter SepiaFilter.

class SepiaFilter extends am4core.Filter {
constructor() {
super();
// actual filter code goes in constructor
}
}
class SepiaFilter extends am4core.Filter {
constructor() {
super();
// actual filter code goes in constructor
}
}

Adding filter rules (primitives)

Filter rules (primitives) are always created in the constructor of our custom filter class.

We do that by adding an element to the paper property of the filter object.

Since we're not doing anything fancy with our target image - just changing some colors - we will just need one primitive.

For that job the best candidate is <feColorMatrix> primitive, which allows manipulating colors in a myriad of ways.

Here's how it would look like directly in the SVG:

<feColorMatrix type="matrix"
values="0.39 0.769 0.189 0 0
0.349 0.686 0.168 0 0
0.272 0.534 0.131 0 0
0 0 0 1 0" />

Let's see how that looks code-wise:

class SepiaFilter extends am4core.Filter {
constructor() {
super();
let matrix = this.paper.add("feColorMatrix");
matrix.attr({ "type": "matrix", "values": "0.39 0.769 0.189 0 0 0.349 0.686 0.168 0 0 0.272 0.534 0.131 0 0 0 0 0 1 0" });
this.filterPrimitives.push(matrix);
}
}
class SepiaFilter extends am4core.Filter {
constructor() {
super();
var matrix = this.paper.add("feColorMatrix");
matrix.attr({ "type": "matrix", "values": "0.39 0.769 0.189 0 0 0.349 0.686 0.168 0 0 0.272 0.534 0.131 0 0 0 0 0 1 0" });
this.filterPrimitives.push(matrix);
}
}

Let's bisect the above code.

  1. We first call super() which ensures that whatever is in the parent Filter class constructor gets executed. Just roll with it.
  2. Then we create an feColorMatrix element. The paper.add() method returns element object.
  3. We set properties of the filter element using its attr() method. Each item in the parameter object corresponds to the key/value attribute of the element.
  4. Finally, we push primitive element to own filterPrimitives list.

That's it. At this point our filter is ready to be used in real life situations.

Example

Now, let's try it on on a real (Pie) chart.

As you know, applying a filter to element is as easy as pushing its instance to filters list.

We can create filters for each element, like a Slice, individually:

pieSeries.columns.template.filters.push(new SepiaFilter());
pieSeries.columns.template.filters.push(new SepiaFilter());

Or we can apply directly to whole of the chart and its related containers:

chart.filters.push(new SepiaFilter());
chart.tooltipContainer.filters.push(new SepiaFilter());
chart.filters.push(new SepiaFilter());
chart.tooltipContainer.filters.push(new SepiaFilter());

Let's try it on already.

See the Pen amCharts 4: Sepia filter (pie chart) by amCharts team (@amcharts) on CodePen.0

Advanced example

Now, let's try something even more challenging. Something that needs multiple different primitives, as well as groups of primitives.

5 seconds of googling lands us on this Smaging Magazine showcase of cool SVG filters. It takes another 10 seconds to settle on the "Sketchy effect" demo.

Yep. We're going to apply this look to an actual chart!

The SVG code for such filter is a tiny little bit complicated than our previous example:

<!-- COLOR -->
<feFlood flood-color="#73DCFF" flood-opacity="0.75" result="COLOR-blu" />
<feFlood flood-color="#9673FF" flood-opacity="0.4" result="COLOR-red" />
<!-- COLOR END -->
<!-- Texture -->
<feTurbulence baseFrequency=".05" type="fractalNoise" numOctaves="3" seed="0" result="Texture_10" />
<feColorMatrix type="matrix"
values="0 0 0 0 0,
0 0 0 0 0,
0 0 0 0 0,
0 0 0 -2.1 1.1" in="Texture_10" result="Texture_20" />
<feColorMatrix result="Texture_30" type="matrix"
values="0 0 0 0 0,
0 0 0 0 0,
0 0 0 0 0,
0 0 0 -1.7 1.8" in="Texture_10" />
<!-- Texture -->
<!-- FILL -->
<feOffset dx="-3" dy="4" in="SourceAlpha" result="FILL_10"/>
<feDisplacementMap scale="17" in="FILL_10" in2="Texture_10" result="FILL_20" />
<feComposite operator="in" in="Texture_30" in2="FILL_20" result="FILL_40"/>
<feComposite operator="in" in="COLOR-blu" in2="FILL_40" result="FILL_50" />
<!-- FILL END-->
<!-- OUTLINE -->
<feMorphology operator="dilate" radius="3" in="SourceAlpha" result="OUTLINE_10" />
<feComposite operator="out" in="OUTLINE_10" in2="SourceAlpha" result="OUTLINE_20" />
<feDisplacementMap scale="7" in="OUTLINE_20" in2="Texture_10" result="OUTLINE_30" />
<feComposite operator="arithmetic" k2="-1" k3="1" in="Texture_20" in2="OUTLINE_30" result="OUTLINE_40" />
<!-- OUTLINE END-->
<!-- BEVEL OUTLINE -->
<feConvolveMatrix order="8,8" divisor="1"
kernelMatrix="1 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 1 " in="SourceAlpha" result="BEVEL_10" />
<feMorphology operator="dilate" radius="2" in="BEVEL_10" result="BEVEL_20" />
<feComposite operator="out" in="BEVEL_20" in2="BEVEL_10" result="BEVEL_30"/>
<feDisplacementMap scale="7" in="BEVEL_30" in2="Texture_10" result="BEVEL_40" />
<feComposite operator="arithmetic" k2="-1" k3="1" in="Texture_20" in2="BEVEL_40" result="BEVEL_50" />
<feOffset dx="-7" dy="-7" in="BEVEL_50" result="BEVEL_60"/>
<feComposite operator="out" in="BEVEL_60" in2="OUTLINE_10" result="BEVEL_70" />
<!-- BEVEL OUTLINE END -->
<!-- BEVEL FILL -->
<feOffset dx="-9" dy="-9" in="BEVEL_10" result="BEVEL-FILL_10"/>
<feComposite operator="out" in="BEVEL-FILL_10" in2="OUTLINE_10" result="BEVEL-FILL_20" />
<feDisplacementMap scale="17" in="BEVEL-FILL_20" in2="Texture_10" result="BEVEL-FILL_30" />
<feComposite operator="in" in="COLOR-red" in2="BEVEL-FILL_30" result="BEVEL-FILL_50" /> <!-- -->
<!-- BEVEL FILL END-->
<feMerge result="merge2">
<feMergeNode in="BEVEL-FILL_50" />
<feMergeNode in="BEVEL_70" />
<feMergeNode in="FILL_50" />
<feMergeNode in="OUTLINE_40" />
</feMerge>

Primitive groups

Essentially, the implementation is essentially the same as in our previous example - we just add a lot of primitives, instead of just one.

However, there is one important new thing: groups of primitives.

A group is just like any other primitive, except we create it using paper.addGroup() method, rather than paper.add().

Primitives that are supposed to go into a group are not pushed into filterPrimitives list, but rather added to the group element using its own add() method.

Filter dimensions

Another thing to keep to pay attention to is filter's dimensions. They indicate how big resulting image can be in comparison to original.

For something similar like replacing colors, this is not relevant, because our image does not get bigger.

For this advanced example, our image will be decorated in such way that some elements will "spill over" its original boundaries. That means we need to set allowed dimensions of the filter to be bigger than the original, or it will be clipped.

We do that by setting filter object's width and height properties. Those are set in percent. Which means if we set to 150 the resulting image can be 1.5 times bigger than original. Anything that goes beyond that threshold will be cut off.

Advanced example code

To make our code simpler, we're also going to create some additional methods to our custom class, that will save us a bit on code when adding primitives, groups, and group child primitives.

OK, so here's the full code for our new SketchyFilter class:

class SketchyFilter extends am4core.Filter {

constructor() {

super();

// Set default properties
this.width = 150;
this.height = 150;

// COLOR
this.addPrimitve("feFlood", { "flood-color": "#73DCFF", "flood-opacity": "0.75", "result": "COLOR-blu" });
this.addPrimitve("feFlood", { "flood-color": "#9673FF", "flood-opacity": "0.4", "result": "COLOR-red" });

// TEXTURE
this.addPrimitve("feTurbulence", { "baseFrequency": ".05", "type": "fractalNoise", "numOctaves": "3", "seed": "0", "result": "Texture_10" });
this.addPrimitve("feColorMatrix", { "type": "matrix", "values": "0 0 0 0 0, 0 0 0 0 0, 0 0 0 0 0, 0 0 0 -2.1 1.1", "in": "Texture_10", "result": "Texture_20" });
this.addPrimitve("feColorMatrix", { "result": "Texture_30", "type": "matrix", "values": "0 0 0 0 0, 0 0 0 0 0, 0 0 0 0 0, 0 0 0 -1.7 1.8", "in": "Texture_10" });

// FILL
this.addPrimitve("feOffset", { "dx": "-3", "dy": "4", "in": "SourceAlpha", "result": "FILL_10" });
this.addPrimitve("feDisplacementMap", { "scale": "17", "in": "FILL_10", "in2": "Texture_10", "result": "FILL_20" });
this.addPrimitve("feComposite", { "operator": "in", "in": "Texture_30", "in2": "FILL_20", "result": "FILL_40" });
this.addPrimitve("feComposite", { "operator": "in", "in": "COLOR-blu", "in2": "FILL_40", "result": "FILL_50" });

// OUTLINE
this.addPrimitve("feMorphology", { "operator": "dilate", "radius": "3", "in": "SourceAlpha", "result": "OUTLINE_10" });
this.addPrimitve("feComposite", { "operator": "out", "in": "OUTLINE_10", "in2": "SourceAlpha", "result": "OUTLINE_20" });
this.addPrimitve("feDisplacementMap", { "scale": "7", "in": "OUTLINE_20", "in2": "Texture_10", "result": "OUTLINE_30" });
this.addPrimitve("feComposite", { "operator": "arithmetic", "k2": "-1", "k3": "1", "in": "Texture_20", "in2": "OUTLINE_30", "result": "OUTLINE_40" });

// BEVEL OUTLINE
this.addPrimitve("feConvolveMatrix", { "order": "8,8", "divisor": "1", "kernelMatrix": "1 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 1 ", "in": "SourceAlpha", "result": "BEVEL_10" });
this.addPrimitve("feMorphology", { "operator": "dilate", "radius": "2", "in": "BEVEL_10", "result": "BEVEL_20" });
this.addPrimitve("feComposite", { "operator": "out", "in": "BEVEL_20", "in2": "BEVEL_10", "result": "BEVEL_30" });
this.addPrimitve("feDisplacementMap", { "scale": "7", "in": "BEVEL_30", "in2": "Texture_10", "result": "BEVEL_40" });
this.addPrimitve("feComposite", { "operator": "arithmetic", "k2": "-1", "k3": "1", "in": "Texture_20", "in2": "BEVEL_40", "result": "BEVEL_50" });
this.addPrimitve("feOffset", { "dx": "-7", "dy": "-7", "in": "BEVEL_50", "result": "BEVEL_60" });
this.addPrimitve("feComposite", { "operator": "out", "in": "BEVEL_60", "in2": "OUTLINE_10", "result": "BEVEL_70" });

// BEVEL FILL
this.addPrimitve("feOffset", { "dx": "-9", "dy": "-9", "in": "BEVEL_10", "result": "BEVEL-FILL_10" });
this.addPrimitve("feComposite", { "operator": "out", "in": "BEVEL-FILL_10", "in2": "OUTLINE_10", "result": "BEVEL-FILL_20" });
this.addPrimitve("feDisplacementMap", { "scale": "17", "in": "BEVEL-FILL_20", "in2": "Texture_10", "result": "BEVEL-FILL_30" });
this.addPrimitve("feComposite", { "operator": "in", "in": "COLOR-red", "in2": "BEVEL-FILL_30", "result": "BEVEL-FILL_50" });

// MERGE
let merge = this.addGroup("feMerge", { "result": "merge2" });
this.addPrimitveNode(merge, "feMergeNode", { "in": "BEVEL-FILL_50" });
this.addPrimitveNode(merge, "feMergeNode", { "in": "BEVEL_70" });
this.addPrimitveNode(merge, "feMergeNode", { "in": "FILL_50" });
this.addPrimitveNode(merge, "feMergeNode", { "in": "OUTLINE_40" });

}

addGroup(type, attr) {
let f = this.paper.addGroup(type);
f.attr(attr);
this.filterPrimitives.push(f);
return f;
}

addPrimitve(type, attr) {
let f = this.paper.add(type);
f.attr(attr);
this.filterPrimitives.push(f);
return f;
}

addPrimitveNode(parent, type, attr) {
let f = this.paper.add(type);
f.attr(attr);
parent.add(f);
return f;
}

}
class SketchyFilter extends am4core.Filter {

constructor() {

super();

// Set default properties
this.width = 150;
this.height = 150;

// COLOR
this.addPrimitve("feFlood", { "flood-color": "#73DCFF", "flood-opacity": "0.75", "result": "COLOR-blu" });
this.addPrimitve("feFlood", { "flood-color": "#9673FF", "flood-opacity": "0.4", "result": "COLOR-red" });

// TEXTURE
this.addPrimitve("feTurbulence", { "baseFrequency": ".05", "type": "fractalNoise", "numOctaves": "3", "seed": "0", "result": "Texture_10" });
this.addPrimitve("feColorMatrix", { "type": "matrix", "values": "0 0 0 0 0, 0 0 0 0 0, 0 0 0 0 0, 0 0 0 -2.1 1.1", "in": "Texture_10", "result": "Texture_20" });
this.addPrimitve("feColorMatrix", { "result": "Texture_30", "type": "matrix", "values": "0 0 0 0 0, 0 0 0 0 0, 0 0 0 0 0, 0 0 0 -1.7 1.8", "in": "Texture_10" });

// FILL
this.addPrimitve("feOffset", { "dx": "-3", "dy": "4", "in": "SourceAlpha", "result": "FILL_10" });
this.addPrimitve("feDisplacementMap", { "scale": "17", "in": "FILL_10", "in2": "Texture_10", "result": "FILL_20" });
this.addPrimitve("feComposite", { "operator": "in", "in": "Texture_30", "in2": "FILL_20", "result": "FILL_40" });
this.addPrimitve("feComposite", { "operator": "in", "in": "COLOR-blu", "in2": "FILL_40", "result": "FILL_50" });

// OUTLINE
this.addPrimitve("feMorphology", { "operator": "dilate", "radius": "3", "in": "SourceAlpha", "result": "OUTLINE_10" });
this.addPrimitve("feComposite", { "operator": "out", "in": "OUTLINE_10", "in2": "SourceAlpha", "result": "OUTLINE_20" });
this.addPrimitve("feDisplacementMap", { "scale": "7", "in": "OUTLINE_20", "in2": "Texture_10", "result": "OUTLINE_30" });
this.addPrimitve("feComposite", { "operator": "arithmetic", "k2": "-1", "k3": "1", "in": "Texture_20", "in2": "OUTLINE_30", "result": "OUTLINE_40" });

// BEVEL OUTLINE
this.addPrimitve("feConvolveMatrix", { "order": "8,8", "divisor": "1", "kernelMatrix": "1 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 1 ", "in": "SourceAlpha", "result": "BEVEL_10" });
this.addPrimitve("feMorphology", { "operator": "dilate", "radius": "2", "in": "BEVEL_10", "result": "BEVEL_20" });
this.addPrimitve("feComposite", { "operator": "out", "in": "BEVEL_20", "in2": "BEVEL_10", "result": "BEVEL_30" });
this.addPrimitve("feDisplacementMap", { "scale": "7", "in": "BEVEL_30", "in2": "Texture_10", "result": "BEVEL_40" });
this.addPrimitve("feComposite", { "operator": "arithmetic", "k2": "-1", "k3": "1", "in": "Texture_20", "in2": "BEVEL_40", "result": "BEVEL_50" });
this.addPrimitve("feOffset", { "dx": "-7", "dy": "-7", "in": "BEVEL_50", "result": "BEVEL_60" });
this.addPrimitve("feComposite", { "operator": "out", "in": "BEVEL_60", "in2": "OUTLINE_10", "result": "BEVEL_70" });

// BEVEL FILL
this.addPrimitve("feOffset", { "dx": "-9", "dy": "-9", "in": "BEVEL_10", "result": "BEVEL-FILL_10" });
this.addPrimitve("feComposite", { "operator": "out", "in": "BEVEL-FILL_10", "in2": "OUTLINE_10", "result": "BEVEL-FILL_20" });
this.addPrimitve("feDisplacementMap", { "scale": "17", "in": "BEVEL-FILL_20", "in2": "Texture_10", "result": "BEVEL-FILL_30" });
this.addPrimitve("feComposite", { "operator": "in", "in": "COLOR-red", "in2": "BEVEL-FILL_30", "result": "BEVEL-FILL_50" });

// MERGE
var merge = this.addGroup("feMerge", { "result": "merge2" });
this.addPrimitveNode(merge, "feMergeNode", { "in": "BEVEL-FILL_50" });
this.addPrimitveNode(merge, "feMergeNode", { "in": "BEVEL_70" });
this.addPrimitveNode(merge, "feMergeNode", { "in": "FILL_50" });
this.addPrimitveNode(merge, "feMergeNode", { "in": "OUTLINE_40" });

}

addGroup(type, attr) {
var f = this.paper.addGroup(type);
f.attr(attr);
this.filterPrimitives.push(f);
return f;
}

addPrimitve(type, attr) {
var f = this.paper.add(type);
f.attr(attr);
this.filterPrimitives.push(f);
return f;
}

addPrimitveNode(parent, type, attr) {
var f = this.paper.add(type);
f.attr(attr);
parent.add(f);
return f;
}

}

Now, let's try applying this to a Column series column template.

See the Pen amCharts 4: Custom SVG filters (sketchy) by amCharts team (@amcharts) on CodePen.0

Additional info

Performance

When creating and using filters please be aware that those are very resource hungry. If you are applying something fancy, consider disabling other intensive operations, like animations.