Search code examples
dc.jscrossfilter

DC Heatmap how to specify a 2 Color Map for Positive and Negative values


I am attempting to add a Heatmap to a DC/Crossfilter Dashboard. What I want to see is a horizontal line of small rectangles colored either Red of Green depending on whether the transaction return was positive or negative, this is to follow the sequence of transactions.

So I'm having trouble figuring how to relate the color to the transaction value (tried to use the type text"Profit"/"Loss" field but couldn't figure this out either). The following is where I'm at the moment but just seem to be getting the first half of the returns are green and the second half red. So stuck on the mechanics of relating color to value. Also there might be the odd zero value which I would like to match with the positive values.

I'm only a hobbyist here so don't have a great depth of knowledge and have read so many queries that nearly get there but end up making me aware of just how much I don't know. So any help here would be greatly appreciated as I take another step forward.

Here's the Fiddle

Cheers

Fred

Here's my code

var transactions = [
{date: "2020-01-23T11:15:11Z", sequence: 1, rturn: -280.00, type: "Loss"},
{date: "2020-01-23T11:22:19Z", sequence: 2, rturn: -43.75, type: "Loss"},
{date: "2020-01-23T11:28:47Z", sequence: 3, rturn: -4.05, type: "Loss"},
{date: "2020-01-23T11:33:26Z", sequence: 4, rturn: 9.47, type: "Profit"},
{date: "2020-01-23T11:50:34Z", sequence: 5, rturn: 0.11, type: "Profit"},
{date: "2020-01-23T11:53:40Z", sequence: 6, rturn: 16.46, type: "Profit"},
{date: "2020-01-23T12:16:34Z", sequence: 7, rturn: 19.23, type: "Profit"},
{date: "2020-01-23T12:24:03Z", sequence: 8, rturn: 26.65, type: "Profit"},
{date: "2020-01-23T12:38:19Z", sequence: 9, rturn: 7.70, type: "Profit"},
{date: "2020-01-23T13:12:50Z", sequence: 10, rturn: 9.80, type: "Profit"},
{date: "2020-01-23T13:27:43Z", sequence: 11, rturn: -15.58, type: "Loss"},
{date: "2020-01-23T13:35:45Z", sequence: 12, rturn: 6.18, type: "Profit"}
];

var facts = crossfilter(transactions);

var dimensionByType = facts.dimension(function(d){ return d.sequence; });
var groupByType = dimensionByType.group().reduceCount(function(d){ return d.type; });

var barChart = dc.heatMap('#heatMap')
      .width(1024)
      .height(250)
      .dimension(dimensionByType)
      .group(groupByType)
      .title(function(d){ return "Total Payment: $" + d.key + " => Tip: $" +d.value; })
      .xBorderRadius([25])
      .yBorderRadius([25])
      .colors(["steelblue","red"])
      .calculateColorDomain();

  dc.renderAll();
<!DOCTYPE html>
<html lang="EN">
    <head>
      <meta charset="utf-8">
      <title>Heat Map</title>
      <script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.1/d3.min.js"></script>
      <script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/crossfilter/1.3.12/crossfilter.min.js"></script>
      <script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/dc/2.1.9/dc.min.js"></script>
      <link rel="stylesheet" type="text/css" href="https://cdnjs.cloudflare.com/ajax/libs/dc/2.1.9/dc.min.css" />
    </head>
    <body>
        <h1>Transactions Result Sequence</h1>
        <div id="heatMap"></div>
    </body>
</html>


Solution

  • dc.js is built around a lot of "accessors" and "scales".

    "Accessors" take a group bin ({key,value} pair) and return some useful value.

    "Scales" (a D3 concept) take a value and map it to some visual encoding like color, X or Y.

    The heatmap uses

    • keyAccessor and a private d3.scaleBand for X
    • valueAccessor and a private d3.scaleBand for Y
    • colorAccessor and the scale .colors() for color.

    In your case, you want

    • to use sequence for X

        .keyAccessor(d => d.key)
      
    • to use 0 (or any constant value) for Y

        .valueAccessor(d => 0)
      
    • to use rturn or type for the color accessor, and a color scale that maps profit to green and loss to red

    You are using sequence for the dimension; that will provide d.key.

    A crossfilter group aggregates all the rows that fall into a bin assigned by the key.

    A simple way to see the profit or loss is to reduceSum using rturn:

    var groupByType = dimensionByType.group().reduceSum(row => row.rturn);
    

    There is only one row per sequence, so it just passes rturn through. Now d.value will have row.rturn and we can tell the colorAccessor to decide whether it's a profit or loss based on the sign:

          .colorAccessor(d => d.value >= 0 ? 'Profit' : 'Loss')
    

    and use an ordinal scale to assign profit to green and red to loss:

          .colors(d3.scale.ordinal().domain(['Profit', 'Loss']).range(["green","red"]))
    

    enter image description here

    There are lots of other ways to do this, using type or other color scales, but I think this is the simplest and it still works if your heatmap bins are aggregated.

    var transactions = [
    {date: "2020-01-23T11:15:11Z", sequence: 1, rturn: -280.00, type: "Loss"},
    {date: "2020-01-23T11:22:19Z", sequence: 2, rturn: -43.75, type: "Loss"},
    {date: "2020-01-23T11:28:47Z", sequence: 3, rturn: -4.05, type: "Loss"},
    {date: "2020-01-23T11:33:26Z", sequence: 4, rturn: 9.47, type: "Profit"},
    {date: "2020-01-23T11:50:34Z", sequence: 5, rturn: 0.11, type: "Profit"},
    {date: "2020-01-23T11:53:40Z", sequence: 6, rturn: 16.46, type: "Profit"},
    {date: "2020-01-23T12:16:34Z", sequence: 7, rturn: 19.23, type: "Profit"},
    {date: "2020-01-23T12:24:03Z", sequence: 8, rturn: 26.65, type: "Profit"},
    {date: "2020-01-23T12:38:19Z", sequence: 9, rturn: 7.70, type: "Profit"},
    {date: "2020-01-23T13:12:50Z", sequence: 10, rturn: 9.80, type: "Profit"},
    {date: "2020-01-23T13:27:43Z", sequence: 11, rturn: -15.58, type: "Loss"},
    {date: "2020-01-23T13:35:45Z", sequence: 12, rturn: 6.18, type: "Profit"}
    ];
    
    var facts = crossfilter(transactions);
    
    var dimensionByType = facts.dimension(function(d){ return d.sequence; });
    var groupByType = dimensionByType.group().reduceSum(row => row.rturn);
    
    var heatMap = dc.heatMap('#heatMap')
          .width(1024)
          .height(250)
          .dimension(dimensionByType)
          .group(groupByType)
          .keyAccessor(d => d.key)
          .valueAccessor(d => 0)
          .colorAccessor(d => d.value >= 0 ? 'Profit' : 'Loss')
          .title(function(d){ return "Total Payment: $" + d.key + " => Tip: $" +d.value; })
          .xBorderRadius([25])
          .yBorderRadius([25])
          .colors(d3.scale.ordinal().domain(['Profit', 'Loss']).range(["green","red"]))
          //.calculateColorDomain();
    
      dc.renderAll();
    <!DOCTYPE html>
    <html lang="EN">
        <head>
          <meta charset="utf-8">
          <title>Heat Map</title>
          <script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.1/d3.min.js"></script>
          <script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/crossfilter/1.3.12/crossfilter.min.js"></script>
          <script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/dc/2.1.9/dc.min.js"></script>
          <link rel="stylesheet" type="text/css" href="https://cdnjs.cloudflare.com/ajax/libs/dc/2.1.9/dc.min.css" />
        </head>
        <body>
            <h1>Transactions Result Sequence</h1>
            <div id="heatMap"></div>
        </body>
    </html>