javascripttypescriptamchartsamcharts4

Amcharts fill adapter used in legend gets overridden on toggling


I'm working on a project with a design in mind, that has custom legend look. Using React and TypeScript, I need to create a chart that looks similar to this: Required design

TLDR: Try toggling legend on this codepen. Questions listed below.

What I tried:

chart.legend = new Legend();
chart.legend.position = "top";
chart.legend.contentAlign = "left";
chart.legend.margin(0, 0, 30, 0);
// Disabled default markers that resemble color.
chart.legend.useDefaultMarker = true;
chart.legend.labels.template.marginLeft = 26;
chart.legend.labels.template.paddingTop = 3;

Then I started to modify legend itself

// Create checkbox outline
const marker = chart.legend.markers.template;
const markerColumn = marker.children.getIndex(0) as Sprite;
markerColumn.defaultState.properties.fillOpacity = 0;
markerColumn.defaultState.properties.strokeWidth = 1;
markerColumn.defaultState.properties.stroke = color("#3896CB");
markerColumn.defaultState.properties.strokeOpacity = 1;

Then I added both checkbox and circle sprite, following this tutorial:

const checkbox = marker.createChild(Image);
checkbox.width = 20;
checkbox.height = 20;
checkbox.verticalCenter = "top";
checkbox.horizontalCenter = "left";
checkbox.href = "https://cdn.onlinewebfonts.com/svg/img_207414.png";

const checkboxActiveState = checkbox.states.create("active");
checkboxActiveState.properties.opacity = 0;

const circle = marker.createChild(Circle);
circle.strokeWidth = 0;
circle.width = 8;
circle.height = 8;
circle.dx = 36;
circle.dy = 12;

Then I stumbled upon a problem: I didn't find a way to select EXACT container's children, so that I can fill them with any color I want. Most examples use templates, which change every single child. I found that adapters may help me. And wrote this code:

const colorValues: string[] = ["#8BC4D7", "#009299", "#1ABDB5", "#3896CB"];

function createSeries(chart: XYChart, data: ChartData[], field: string, name: string, setColor: string) {
  // ... Creating line series
  series.dummyData = {
     fill: setColor,
  };
  return series;
}

function colorAdapter(fill, target) {
  if (!target.dataItem) {
    return fill;
  }
  const settings = target.dataItem.dataContext.dummyData;
  return settings.fill;
}

// Finally, adding color via dummyData:
// Correctly sets colors on first render, BUT on legend toggle forgets them
circle?.adapter.add("fill", (fill, target) => {
  if (!target.dataItem) {
    return fill;
  }
  const settings = target.dataItem.dataContext.dummyData;
  return settings.fill;
});

Problem: Try toggling legend! The ones with dummy data remain gray after toggling them on again.

This is what happens, when I try toggle legend on and off (look at first two legend items):

You can play with similar case on this codepen.

UPDATE: Here's my codepen with exactly my case.

Questions:

  1. (Main question) How to solve this problem using adapters? Expected result: after toggling on, they should get their fill color back.
  2. (Just to expand knowledge) If possible, how to solve this problem without using adapters? How to, using hard-code, select any container's child I want?
  3. (Alternative solution?) What if I want these circles to remain color even after toggling? How can I do that?
  4. (Optional, this is probably somewhere in docs) I didn't quite understand how to replace sprite with an Image without external internet link. Can .href work with svgs through relative paths?

Solution

  • To get the desired colour, you can add the adapter to the state-changes. Applied to your codePen it would look like this:

    /**
     * ---------------------------------------
     * This demo was created using amCharts 4.
     * 
     * For more information visit:
     * https://www.amcharts.com/
     * 
     * Documentation is available at:
     * https://www.amcharts.com/docs/v4/
     * ---------------------------------------
     */
    
    // Themes begin
    am4core.useTheme(am4themes_animated);
    // Themes end
    
    // Create chart instance
    var chart = am4core.create("chartdiv", am4charts.XYChart);
    
    // Add data
    chart.data = [ {
      "year": "2003",
      "europe": 2.5,
      "namerica": 2.5,
      "asia": 2.1,
      "lamerica": 1.2,
      "meast": 0.2,
      "africa": 0.1
    }, {
      "year": "2004",
      "europe": 2.6,
      "namerica": 2.7,
      "asia": 2.2,
      "lamerica": 1.3,
      "meast": 0.3,
      "africa": 0.1
    }, {
      "year": "2005",
      "europe": 2.8,
      "namerica": 2.9,
      "asia": 2.4,
      "lamerica": 1.4,
      "meast": 0.3,
      "africa": 0.1
    } ];
    
    // Create axes
    var categoryAxis = chart.xAxes.push(new am4charts.CategoryAxis());
    categoryAxis.dataFields.category = "year";
    categoryAxis.title.text = "Local country offices";
    categoryAxis.renderer.grid.template.location = 0;
    categoryAxis.renderer.minGridDistance = 20;
    categoryAxis.renderer.cellStartLocation = 0.1;
    categoryAxis.renderer.cellEndLocation = 0.9;
    
    var  valueAxis = chart.yAxes.push(new am4charts.ValueAxis());
    valueAxis.min = 0;
    valueAxis.title.text = "Expenditure (M)";
    
    // Create series
    function createSeries(field, name, stacked) {
      var series = chart.series.push(new am4charts.ColumnSeries());
      series.dataFields.valueY = field;
      series.dataFields.categoryX = "year";
      series.name = name;
      series.columns.template.tooltipText = "{name}: [bold]{valueY}[/]";
      series.stacked = stacked;
      series.columns.template.width = am4core.percent(95);
      // Let's try red, for example
      series.dummyData = {
        fill: "red",
      };
    }
    
    createSeries("europe", "Europe", false);
    createSeries("namerica", "North America", true);
    createSeries("asia", "Asia", false);
    createSeries("lamerica", "Latin America", true);
    createSeries("meast", "Middle East", true);
    createSeries("africa", "Africa", true);
    
    // Add legend
    chart.legend = new am4charts.Legend();
    chart.legend.labels.template.marginLeft = 26;
    chart.legend.useDefaultMarker = true;
    
    const marker = chart.legend.markers.template;
    const markerColumn = marker.children.getIndex(0);
    
    const markerColumnActiveState = markerColumn.states.getKey("active");
    markerColumn.defaultState.properties.fillOpacity = 0;
    markerColumn.defaultState.properties.strokeWidth = 1;
    markerColumn.defaultState.properties.stroke = am4core.color("#000");
    markerColumn.defaultState.properties.strokeOpacity = 1;
    
    // Add custom image instead
    const checkbox = marker.createChild(am4core.Image);
    checkbox.width = 20;
    checkbox.height = 20;
    checkbox.verticalCenter = "top";
    checkbox.horizontalCenter = "left";
    
    checkbox.href = "https://cdn.onlinewebfonts.com/svg/img_207414.png";
    checkbox.dx = 1;
    checkbox.dy = 1;
    
    const checkboxActiveState = checkbox.states.create("active");
    checkboxActiveState.properties.opacity = 0;
    
    const circle = marker.createChild(am4core.Circle);
    circle.width = 8;
    circle.height = 8;
    circle.verticalCenter = "top";
    circle.horizontalCenter = "left";
    circle.dx = 36;
    circle.dy = 8;
    
    //REM: https://www.amcharts.com/docs/v4/concepts/states/
    const circleActiveState = circle.states.create("active");
    circleActiveState.adapter.add("fill", (fill, target) => {
      return "#000000"
    });
    
    const circleDefaultState = circle.states.create("default");
    circleDefaultState.adapter.add("fill", (fill, target) => {
        const settings = target?.sprite?.dataItem?.dataContext?.dummyData;
        return settings?.fill || fill;
    });
    <script src="https://www.amcharts.com/lib/4/core.js"></script>
    <script src="https://www.amcharts.com/lib/4/charts.js"></script>
    <script src="https://www.amcharts.com/lib/4/themes/animated.js"></script>
    
    <div id = 'chartdiv'></div>