Arash Howaida
Arash Howaida

Reputation: 2617

Dynamic color mapping

I am using d3 to color code the fill of circles based on an attribute called type. It may have only one type, or it may have a maximum of two. My data is in this structure:

var data = [{'company':'companyA','products':23,'aum':25997692757,'type':['Industry senior exe'],'date':'2015-02'},
{'company':'companyB','products':24,'aum':3548692757,'type':['Industry senior exe','Star'],'date':'2016-02'}
];

Note that the type attribute is a list.

var colorMap = {
    'Industry senior exe':'blue',
    'Star':'Gray'
};

If there is only one type then the fill of the circle is trivial:

.style('fill', function(d) { return colorMap[d.type[0]})

However what seems to be a bit loftier is how to handle the case when the length of the list is longer -- d.type[1]. My goal is: if type is more than one, the circle will be half blue and half gray. It sound simple, but it has really stumped me.

Question

How can I handle the dynamic fill logic as articulated above in d3?

Upvotes: 2

Views: 448

Answers (1)

Gerardo Furtado
Gerardo Furtado

Reputation: 102174

Simply put: you cannot (under the current specifications) fill one element like <circle> with more than one colour. Therefore, one possible solution is using an SVG <linearGradient>.

The drawback in that approach is that supposing you have several different circles with several different colour choices, giving a huge amount of combinations, you'll need to create that same huge amount of linear gradients.

For instance, using an each after your enter selection:

var gradient = svg.append("defs")
    .append("linearGradient")
    .attr("id", "gradient" + i)
    .attr("x1", "0%")
    .attr("x2", "100%")
  gradient.append("stop")
    .attr("offset", "50%")
    .attr("stop-color", colorMap[d.type[0]])
  gradient.append("stop")
    .attr("offset", "50%")
    .attr("stop-color", d.type.length === 2 ? colorMap[d.type[1]] : colorMap[d.type[0]]);

Here is the demo using your data:

var data = [{
    'company': 'companyA',
    'products': 23,
    'aum': 25997692757,
    'type': ['Industry senior exe'],
    'date': '2015-02'
  },
  {
    'company': 'companyB',
    'products': 24,
    'aum': 3548692757,
    'type': ['Industry senior exe', 'Star'],
    'date': '2016-02'
  }
];
var colorMap = {
  'Industry senior exe': 'blue',
  'Star': 'gray'
};
var svg = d3.select("svg");
var circles = svg.selectAll(null)
  .data(data)
  .enter()
  .append("circle")
  .attr("cy", 50)
  .attr("cx", (_, i) => 50 + i * 100)
  .attr("r", 40);
circles.each(function(d, i) {
  var gradient = svg.append("defs")
    .append("linearGradient")
    .attr("id", "gradient" + i)
    .attr("x1", "0%")
    .attr("x2", "100%")
  gradient.append("stop")
    .attr("offset", "50%")
    .attr("stop-color", colorMap[d.type[0]])
  gradient.append("stop")
    .attr("offset", "50%")
    .attr("stop-color", d.type.length === 2 ? colorMap[d.type[1]] : colorMap[d.type[0]]);
  d3.select(this)
    .attr("fill", "url(#gradient" + i + ")")
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<svg></svg>

On the other hand, if you have only two combinations — all blue and blue-gray — you'll need only two linear gradients.

Upvotes: 3

Related Questions