Amcharts fill adapter used in legend gets overridden on toggling

Issue

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>

Answered By – Lain

This Answer collected from stackoverflow, is licensed under cc by-sa 2.5 , cc by-sa 3.0 and cc by-sa 4.0

Leave a Reply

(*) Required, Your email will not be published