Mark Mark - 5 months ago 14
Javascript Question

Fill D3.js rect with multiple colors or split rect into multiple rects

Goal: Create a grid where the X axis contains job titles and the Y axis has Part Numbers for each of the jobs. If a job requires more than one of a certain part number it will split the cell vertically into 2/3/4/5... different cells but still fit in the same row/column.

Problem: I am having trouble appending multiple rects for certain data points. I tried using this answer here but had no luck. I tried using a for each loop with no luck as well. Any suggestion?

Current Progress:

enter image description here

Goal:

enter image description here

Code snippet in question:


dayOffset = dayFormat(dateExtent[0]);
rect = heatmap.selectAll('rect')
.data(data)
.enter().append('rect')
.attr('width', function (d) {
return Xcellscale/d.NumberofPart;
})
.attr('height', Ycellscale)
.attr('x', function (d) {
return xAxisScale(d.JobNumber);
})
.attr('y', function (d) {
return yAxisScale(d.PartNumber);
})
.attr('fill', '#000FF0');


JSON:

{
"data": [
{
"PartNumber": "a",
"JobNumber": "a",
"NumberofPart": "2",
"timestamp": "2014-09-25T00:00:00",
"value": {
"PM2.5": 30.22
}
},
{
"PartNumber": "b",
"JobNumber": "b",
"NumberofPart": "1",
"timestamp": "2014-09-25T01:00:00",
"value": {
"PM2.5": 41.61
}
},
{
"PartNumber": "c",
"JobNumber": "c",
"NumberofPart": "1",
"timestamp": "2014-09-25T02:00:00",
"value": {
"PM2.5": 50.71
}
},
{
"PartNumber": "d",
"JobNumber": "c",
"NumberofPart": "1",
"timestamp": "2014-09-25T02:00:00",
"value": {
"PM2.5": 50.71
}
},
{
"PartNumber": "e",
"JobNumber": "c",
"NumberofPart": "1",
"timestamp": "2014-09-25T02:00:00",
"value": {
"PM2.5": 50.71
}
},
{
"PartNumber": "f",
"JobNumber": "c",
"NumberofPart": "1",
"timestamp": "2014-09-25T02:00:00",
"value": {
"PM2.5": 50.71
}
},
{
"PartNumber": "g",
"JobNumber": "c",
"NumberofPart": "1",
"timestamp": "2014-09-25T02:00:00",
"value": {
"PM2.5": 1000.71
}
},
{
"PartNumber": "h",
"JobNumber": "c",
"NumberofPart": "1",
"timestamp": "2014-09-25T03:00:00",
"value": {
"PM2.5": 250.34
}
},
{
"PartNumber": "i",
"JobNumber": "d",
"NumberofPart": "1",
"timestamp": "2014-09-25T04:00:00",
"value": {
"PM2.5": 79.64
}
},
{
"PartNumber": "j",
"JobNumber": "e",
"NumberofPart": "2",
"timestamp": "2014-09-25T05:00:00",
"value": {
"PM2.5": 76.93
}
},
{
"PartNumber": "k",
"JobNumber": "f",
"NumberofPart": "2",
"timestamp": "2014-09-30T06:00:00",
"value": {
"PM2.5": 106.45
}
}
]
}


Here is all my code. It's based off of this D3.js Example and sorry about the extra code I haven't removed regarding the timestamps.

$(document).ready(function () {
//UI configuration
var itemSize = 18,
cellSize = itemSize - 1,
width = 800,
height = 800,
margin = { top: 20, right: 20, bottom: 20, left: 25 };

//formats
var hourFormat = d3.time.format('%H'),
dayFormat = d3.time.format('%j'),
timeFormat = d3.time.format('%Y-%m-%dT%X'),
monthDayFormat = d3.time.format('%m.%d');

//data vars for rendering
var dateExtent = null,
data = null,
dayOffset = 0,
colorCalibration = ['#00ff00', '#0033cc', '#ffcc00', '#ff6600', '#ff0000', '#9E0142'],
dailyValueExtent = {};

data = jsondata.data;

//axises and scales
var axisWidth = 0,
axisHeight = itemSize * 24,
xAxisScale = d3.scale.ordinal()
.domain(data.map(function (d) { return d["JobNumber"]; }))
.rangePoints([0, width]);
xAxis = d3.svg.axis()
.scale(xAxisScale)
.orient('top'),
yAxisScale = d3.scale.ordinal()
.domain(data.map(function (d) { return d["PartNumber"]; }))
.rangePoints([0, width]),
yAxis = d3.svg.axis()
.scale(yAxisScale)
.orient('left');


initCalibration();

var svg = d3.select('[role="heatmap"]');
var heatmap = svg
.attr('width', width)
.attr('height', height)
.append('g')
.attr('width', width - margin.left - margin.right)
.attr('height', height - margin.top - margin.bottom)
.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')');
var rect = null;


data.forEach(function (valueObj) {
valueObj['date'] = timeFormat.parse(valueObj['timestamp']);


});

dateExtent = d3.extent(data, function (d) {
return d.date;
});

svg.append('g')
.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')')
.attr('class', 'x axis')
.call(xAxis)
.append('text')
.text('JobNumber')
.attr('transform', 'translate(' + axisWidth + ',-10)');

svg.append('g')
.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')')
.attr('class', 'y axis')
.call(yAxis)
.append('text')
.text('PartNumber')
.attr('transform', 'translate(-10,' + axisHeight + ') rotate(-90)');

//render heatmap rects


Xcellscale = width / (xAxisScale.domain().length -1);
Ycellscale = height / (yAxisScale.domain().length - 1);

console.log(Xcellscale + " " + Ycellscale);
console.log(width + " " + height);
console.log(xAxisScale.domain().length + " " + data.map(function (d) { return d["PartNumber"]; }).length);

dayOffset = dayFormat(dateExtent[0]);
rect = heatmap.selectAll('rect')
.data(data)
.enter().append('rect')
.attr('width', function (d) {
return Xcellscale/d.NumberofPart;
})
.attr('height', Ycellscale)
.attr('x', function (d) {
return xAxisScale(d.JobNumber);
})
.attr('y', function (d) {
return yAxisScale(d.PartNumber);
})
.attr('fill', '#000FF0');

rect.filter(function (d) { return d.value['PM2.5'] > 0; })
.append('title')
.text(function (d) {
return monthDayFormat(d.date) + ' ' + d.value['PM2.5'];
});

//renderColor();


function initCalibration() {
d3.select('[role="calibration"] [role="example"]').select('svg')
.selectAll('rect').data(colorCalibration).enter()
.append('rect')
.attr('width', cellSize)
.attr('height', cellSize)
.attr('x', function (d, i) {
return i * itemSize;
})
.attr('fill', function (d) {
return d;
});

//bind click event
d3.selectAll('[role="calibration"] [name="displayType"]').on('click', function () {
renderColor();
});
}

function renderColor() {
var renderByCount = document.getElementsByName('displayType')[0].checked;

rect
.attr('width', function (d) {
return Xcellscale / d.NumberofPart;
})
.filter(function (d) {
return (d.value['PM2.5'] >= 0);
})
.transition()
.delay(function (d) {
return 1;
})
.duration(500)
.attrTween('fill', function (d, i, a) {
//choose color dynamicly
var colorIndex = d3.scale.quantize()
.range([0, 1, 2, 3, 4, 5])
.domain(( [0, 500]));

return d3.interpolate(a, '#00ff00');
})

}

//extend frame height in `http://bl.ocks.org/`
d3.select(self.frameElement).style("height", "600px");
});

Answer

Okay so I figured it out. This isn't the most eloquent way of getting data onto the heat map but it works pretty well. Rather than using selectall().data().enter() I just used a for loop through my data set and appended the rectangles that way. I usually see people using a mapping function instead of a for loop so if there is any problems that might be caused by that let me know so I can improve my code.

Here is my Code:

for (var i = 0; i < data.length; i++) {

    numberofparts = data[i].NumberofPart;
    for (var j = 0; j < data[i].NumberofPart; j++) {

        heatmap.append('rect')
        .attr('width', Xcellscale / numberofparts)
        .attr('height', Ycellscale)
        .attr('x', xAxisScale(data[i].JobNumber) + (j*(Xcellscale / numberofparts)))
        .attr('y', yAxisScale(data[i].PartNumber))
        .attr('fill', function () {
            if (j == 1) {
                return '#000FF0';
            }
            else
                return '#00000'
        });

    }
}

And here is a screenshot of my chart/heatmap now:

enter image description here

I hope this might help some people trying to do the same thing.

UPDATE 6/16/2016 Thanks Will and Robert with your help I improved my code using the .each() function and hopefully that will help me avoid problems that could occur using the for loop. I also referred to this example from bl.ocks.org for help.

My updated code (I renamed a bunch of variables from the json as well):

rect = heatmap.selectAll('svg')
    .data(data)
.enter();

rect.append('svg').each(function (d, i) {
    numberofparts = d.PartQuantity;
    for (var j = 0; j < d.PartQuantity; j++) {
        console.log(d);

        d3.select(this).append('rect')
        .attr('width', Xcellscale / numberofparts)
        .attr('height', Ycellscale)
        .attr('x', xAxisScale(d.JobOrderNumber)+(j * (Xcellscale / numberofparts)))
        .attr('y', yAxisScale(d.PartNumber))
        .attr('fill', function () {
                        return GetPartfromQuantities(materialdata, data[i].PartNumber);
                    });
    }
});

Here is how it looks now. I added more data to the set and added a coloring scheme.

I added more data an colors to the chart

Comments