nvd3-fork
Version:
FORK! of NVD3, a reusable charting library written in d3.js
827 lines (694 loc) • 37.2 kB
JavaScript
/*
Improvements:
- consistenly apply no-hover classes to rect isntead of to containing g, see example CSS style for .no-hover rect, rect.no-hover
- row/column order (user specified) or 'ascending' / 'descending'
- I haven't tested for transitions between changing datasets
*/
nv.models.heatMap = function() {
"use strict";
//============================================================
// Public Variables with Default Settings
//------------------------------------------------------------
var margin = {top: 0, right: 0, bottom: 0, left: 0}
, width = 960
, height = 500
, id = Math.floor(Math.random() * 10000) //Create semi-unique ID in case user doesn't select one
, container
, xScale = d3.scale.ordinal()
, yScale = d3.scale.ordinal()
, colorScale = false
, getX = function(d) { return d.x }
, getY = function(d) { return d.y }
, getCellValue = function(d) { return d.value }
, showCellValues = true
, cellValueFormat = function(d) { return typeof d === 'number' ? d.toFixed(0) : d }
, cellAspectRatio = false // width / height of cell
, cellRadius = 2
, cellBorderWidth = 4 // pixels between cells
, normalize = false
, highContrastText = true
, xDomain
, yDomain
, xMetaColorScale = nv.utils.defaultColor()
, yMetaColorScale = nv.utils.defaultColor()
, missingDataColor = '#bcbcbc'
, missingDataLabel = ''
, metaOffset = 5 // spacing between meta rects and cells
, xRange
, yRange
, xMeta
, yMeta
, colorRange
, colorDomain
, dispatch = d3.dispatch('chartClick', 'elementClick', 'elementDblClick', 'elementMouseover', 'elementMouseout', 'elementMousemove', 'renderEnd')
, duration = 250
, xMetaHeight = function(d) { return cellHeight / 3 }
, yMetaWidth = function(d) { return cellWidth / 3 }
, showGrid = false
;
//============================================================
// Aux helper function for heatmap
//------------------------------------------------------------
// choose high contrast text color based on background
// shameful steal: https://github.com/alexandersimoes/d3plus/blob/master/src/color/text.coffee
function cellTextColor(bgColor) {
if (highContrastText) {
var rgbColor = d3.rgb(bgColor);
var r = rgbColor.r;
var g = rgbColor.g;
var b = rgbColor.b;
var yiq = (r * 299 + g * 587 + b * 114) / 1000;
return yiq >= 128 ? "#404040" : "#EDEDED"; // dark text else light text
} else {
return 'black';
}
}
/* go through heatmap data and generate array of values
* for each row/column or for entire dataset; for use in
* calculating means/medians of data for normalizing
* @param {str} axis - 'row', 'col' or null
*
* @returns {row/column index: [array of values for row/col]}
* note that if axis is not specified, the return will be
* {0: [all values in heatmap]}
*/
function getHeatmapValues(data, axis) {
var vals = {};
data.forEach(function(cell, i) {
if (axis == 'row') {
if (!(getIY(cell) in vals)) vals[getIY(cell)] = [];
vals[getIY(cell)].push(getCellValue(cell));
} else if (axis == 'col') {
if (!(getIX(cell) in vals)) vals[getIX(cell)] = [];
vals[getIX(cell)].push(getCellValue(cell));
} else if (axis == null) { // if calculating stat over entire dataset
if (!(0 in vals)) vals[0] = [];
vals[0].push(getCellValue(cell));
}
})
return vals;
}
// calculate the median absolute deviation of the given array of data
// https://en.wikipedia.org/wiki/Median_absolute_deviation
// MAD = median(abs(Xi - median(X)))
function mad(dat) {
var med = d3.median(dat);
var vals = dat.map(function(d) { return Math.abs(d - med); })
return d3.median(vals);
}
// set cell color based on cell value
// depending on whether it should be normalized or not
function cellColor(d) {
var colorVal = normalize ? getNorm(d) : getCellValue(d);
return (cellsAreNumeric() && !isNaN(colorVal) || typeof colorVal !== 'undefined') ? colorScale(colorVal) : missingDataColor;
}
// return the domain of the color data
// if ordinal data is given for the cells, this will
// return all possible cells values; otherwise it
// returns the extent of the cell values
// will take into account normalization if specified
function getColorDomain() {
if (cellsAreNumeric()) { // if cell values are numeric
return normalize ? d3.extent(prepedData, function(d) { return getNorm(d); }) : d3.extent(uniqueColor);
} else if (!cellsAreNumeric()) { // if cell values are ordinal
return uniqueColor;
}
}
// return true if cells are numeric
// as opposed to categorical
function cellsAreNumeric() {
return typeof uniqueColor[0] === 'number';
}
/*
* Normalize input data
*
* normalize must be one of centerX, robustCenterX, centerScaleX, robustCenterScaleX, centerAll,
* robustCenterAll, centerScaleAll, robustCenterScaleAll where X is either 'Row' or 'Column'
*
* - centerX: subtract row/column mean from cell
* - centerAll: subtract mean of whole data set from cell
* - centerScaleX: scale so that row/column has mean 0 and variance 1 (Z-score)
* - centerScaleAll: scale by overall normalization factor so that the whole data set has mean 0 and variance 1 (Z-score)
* - robustCenterX: subtract row/column median from cell
* - robustCenterScaleX: subtract row/column median from cell and then scale row/column by median absolute deviation
* - robustCenterAll: subtract median of whole data set from cell
* - robustCenterScaleAll: subtract overall median from cell and scale by overall median absolute deviation
*/
function normalizeData(dat) {
var normTypes = ['centerRow',
'robustCenterRow',
'centerScaleRow',
'robustCenterScaleRow',
'centerColumn',
'robustCenterColumn',
'centerScaleColumn',
'robustCenterScaleColumn',
'centerAll',
'robustCenterAll',
'centerScaleAll',
'robustCenterScaleAll'];
if(normTypes.indexOf(normalize) != -1) {
var xVals = Object.keys(uniqueX), yVals = Object.keys(uniqueY);
// setup normalization options
var scale = normalize.includes('Scale') ? true: false,
agg = normalize.includes('robust') ? 'median': 'mean',
axis = normalize.includes('Row') ? 'row' : normalize.includes('Column') ? 'col' : null,
vals = getHeatmapValues(dat, axis);
// calculate mean or median
// calculate standard dev or median absolute deviation
var stat = {};
var dev = {};
for (var key in vals) {
stat[key] = agg == 'mean' ? d3.mean(vals[key]) : d3.median(vals[key]);
if (scale) dev[key] = agg == 'mean' ? d3.deviation(vals[key]) : mad(vals[key]);
}
// do the normalizing
dat.forEach(function(cell, i) {
if (cellsAreNumeric()) {
if (axis == 'row') {
var key = getIY(cell);
} else if (axis == 'col') {
var key = getIX(cell);
} else if (axis == null) { // if calculating stat over entire dataset
var key = 0;
}
var normVal = getCellValue(cell) - stat[key];
if (scale) {
cell._cellPos.norm = normVal / dev[key];
} else {
cell._cellPos.norm = normVal;
}
} else {
cell._cellPos.norm = getCellValue(cell); // if trying to normalize ordinal cells, just set norm to cell value
}
})
} else {
normalize = false; // proper normalize option was not provided, disable it so heatmap still shows colors
}
return dat;
}
/*
* Process incoming data for use with heatmap including:
* - adding a unique key indexer to each data point (idx)
* - getting a unique list of all x & y values
* - generating a position index (x & y) for each data point
* - sorting that data for correct traversal when generating rect
* - generating placeholders for missing data
*
* In order to allow for the flexibility of the user providing either
* categorical or quantitative data, we're going to position the cells
* through indices that we increment based on previously seen data
* this way we can use ordinal() axes even if the data is quantitative.
*
* When we generate the SVG elements, we assumes traversal occures from
* top to bottom and from left to right.
*
* @param data {list} - input data organize as a list of objects
*
* @return - copy of input data with additional '_cellPos' key
* formatted as {idx: XXX, ix, XXX, iy: XXX}
* where idx is a global identifier; ix is an identifier
* within each column, and iy is an identifier within
* each row.
*/
function prepData(data) {
// reinitialize
uniqueX = {}, // {cell x value: ix index}
uniqueY = {}, // {cell y value: iy index}
uniqueColor = [], // [cell color value]
uniqueXMeta = [], // [cell x metadata value]
uniqueYMeta = [], // [cell y metadata value]
uniqueCells = []; // [cell x,y values stored as array]
var warnings = [];
var sortedCells = {}; // {cell x values: {cell y value: cell data, ... }, ... }
var ix = 0, iy = 0; // use these indices to position cell in x & y direction
var combo, idx=0;
data.forEach(function(cell) {
var valX = getX(cell),
valY = getY(cell),
valColor = getCellValue(cell);
// assemble list of unique values for each dimension
if (!(valX in uniqueX)) {
uniqueX[valX] = ix;
ix++;
sortedCells[valX] = {}
if (typeof xMeta === 'function') uniqueXMeta.push(xMeta(cell));
}
if (!(valY in uniqueY)) {
uniqueY[valY] = iy;
iy++;
sortedCells[valX][valY] = {}
if (typeof yMeta === 'function') uniqueYMeta.push(yMeta(cell));
}
if (uniqueColor.indexOf(valColor) == -1) uniqueColor.push(valColor)
// for each data point, we generate an object of data
// needed to properly position each cell
cell._cellPos = {
idx: idx,
ix: uniqueX[valX],
iy: uniqueY[valY],
}
idx++;
// keep track of row & column combinations we've already seen
// this prevents the same cells from being generated when
// the user hasn't provided proper data (one value for each
// row & column).
// if properly formatted data is not provided, only the first
// row & column value is used (the rest are ignored)
combo = [valX, valY];
if (!isArrayInArray(uniqueCells, combo)) {
uniqueCells.push(combo)
sortedCells[valX][valY] = cell;
} else if (warnings.indexOf(valX + valY) == -1) {
warnings.push(valX + valY);
console.warn("The row/column position " + valX + "/" + valY + " has multiple values; ensure each cell has only a single value.");
}
});
uniqueColor = uniqueColor.sort()
// check in sortedCells that each x has all the y's
// if not, generate an empty placeholder
// this will also sort all cells from left to right
// and top to bottom
var reformatData = [];
Object.keys(uniqueY).forEach(function(j) {
Object.keys(uniqueX).forEach(function(i) {
var cellVal = sortedCells[i][j];
if (cellVal) {
reformatData.push(cellVal);
} else {
var cellPos = {
idx: idx,
ix: uniqueX[i],
iy: uniqueY[j],
}
idx++;
reformatData.push({_cellPos: cellPos}); // empty cell placeholder
}
})
})
// normalize data is needed
return normalize ? normalizeData(reformatData) : reformatData;
}
// https://stackoverflow.com/a/41661388/1153897
function isArrayInArray(arr, item){
var item_as_string = JSON.stringify(item);
var contains = arr.some(function(ele){
return JSON.stringify(ele) === item_as_string;
});
return contains;
}
function removeAllHoverClasses() {
// remove all hover classes
d3.selectAll('.cell-hover').classed('cell-hover', false);
d3.selectAll('.no-hover').classed('no-hover', false);
d3.selectAll('.row-hover').classed('row-hover', false);
d3.selectAll('.column-hover').classed('column-hover', false);
}
// return the formatted cell value if it is
// a number, otherwise return missingDataLabel
var cellValueLabel = function(d) {
var val = !normalize ? cellValueFormat(getCellValue(d)) : cellValueFormat(getNorm(d));
return (cellsAreNumeric() && !isNaN(val) || typeof val !== 'undefined') ? val : missingDataLabel;
}
// https://stackoverflow.com/a/16794116/1153897
// note this returns the obj keys
function sortObjByVals(obj) {
return Object.keys(obj).sort(function(a,b){return obj[a]-obj[b]})
}
// https://stackoverflow.com/a/28191966/1153897
function getKeyByValue(object, value) {
//return Object.keys(object).find(key => object[key] === value);
return Object.keys(object).filter(function(key) {return object[key] === value})[0];
}
//============================================================
// Private Variables
//------------------------------------------------------------
var prepedData, cellHeight, cellWidth;
var uniqueX = {}, uniqueY = {}, uniqueColor = [];
var uniqueXMeta = [], uniqueYMeta = [], uniqueCells = []
var renderWatch = nv.utils.renderWatch(dispatch, duration);
var RdYlBu = ["#a50026","#d73027","#f46d43","#fdae61","#fee090","#ffffbf","#e0f3f8","#abd9e9","#74add1","#4575b4","#313695"];
var getCellPos = function(d) { return d._cellPos; };
var getIX = function(d) { return getCellPos(d).ix; } // get the given cell's x index position
var getIY = function(d) { return getCellPos(d).iy; } // get the given cell's y index position
var getNorm = function(d) { return getCellPos(d).norm; }
var getIdx = function(d) { return getCellPos(d).idx; }
function chart(selection) {
renderWatch.reset();
selection.each(function(data) {
prepedData = prepData(data);
var availableWidth = width - margin.left - margin.right,
availableHeight = height - margin.top - margin.bottom;
// available width/height set the cell dimenions unless
// the aspect ratio is defined - in that case the cell
// height is adjusted and availableHeight updated
cellWidth = availableWidth / Object.keys(uniqueX).length;
cellHeight = cellAspectRatio ? cellWidth / cellAspectRatio : availableHeight / Object.keys(uniqueY).length;
if (cellAspectRatio) availableHeight = cellHeight * Object.keys(uniqueY).length - margin.top - margin.bottom;
container = d3.select(this);
nv.utils.initSVG(container);
// Setup Scales
xScale.domain(xDomain || sortObjByVals(uniqueX))
.rangeBands(xRange || [0, availableWidth-cellBorderWidth/2]);
yScale.domain(yDomain || sortObjByVals(uniqueY))
.rangeBands(yRange || [0, availableHeight-cellBorderWidth/2]);
colorScale = cellsAreNumeric() ? d3.scale.quantize() : d3.scale.ordinal();
colorScale.domain(colorDomain || getColorDomain())
.range(colorRange || RdYlBu);
// Setup containers and skeleton of chart
var wrap = container.selectAll('g.nv-heatMapWrap').data([prepedData]);
var wrapEnter = wrap.enter().append('g').attr('class', 'nvd3 nv-heatMapWrap');
wrapEnter
.append('g')
.attr('class','cellWrap')
wrap.watchTransition(renderWatch, 'nv-wrap: heatMapWrap')
.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')');
var gridWrap = wrapEnter
.append('g')
.attr('class','cellGrid')
.style('opacity',1e-6)
var gridLinesV = wrap.select('.cellGrid').selectAll('.gridLines.verticalGrid')
.data(Object.values(uniqueX).concat([Object.values(uniqueX).length]))
gridLinesV.enter()
.append('line')
.attr('class','gridLines verticalGrid')
gridLinesV.exit()
.remove()
var gridLinesH = wrap.select('.cellGrid').selectAll('.gridLines.horizontalGrid')
.data(Object.values(uniqueY).concat([Object.values(uniqueY).length]))
gridLinesH.enter()
.append('line')
.attr('class','gridLines horizontalGrid')
gridLinesH.exit()
.remove()
var cellWrap = wrap.select('.cellWrap')
.selectAll(".nv-cell")
.data(function(d) { return d; }, function(e) { return getIdx(e); })
var xMetaWrap = wrapEnter
.append('g')
.attr('class','xMetaWrap')
.attr("transform", function() { return "translate(0," + (-xMetaHeight()-cellBorderWidth-metaOffset) + ")" })
var xMetas = wrap.select('.xMetaWrap').selectAll('.x-meta')
.data(uniqueXMeta)
var xMetaEnter = xMetas
.enter()
.append('rect')
.attr('class','x-meta meta')
.attr("width", cellWidth-cellBorderWidth)
.attr("height", xMetaHeight())
.attr("transform", "translate(0,0)")
.attr("fill", function(d) { return xMetaColorScale(d); })
var yMetaWrap = wrapEnter
.append('g')
.attr('class','yMetaWrap')
.attr("transform", function(d,i) { return "translate(" + (-yMetaWidth()-cellBorderWidth-metaOffset) + ",0)" })
var yMetas = wrap.select('.yMetaWrap').selectAll('.y-meta')
.data(uniqueYMeta)
var yMetaEnter = yMetas
.enter()
.append('rect')
.attr('class','y-meta meta')
.attr("width", yMetaWidth())
.attr("height", cellHeight-cellBorderWidth)
.attr("transform", function(d,i) { return "translate(0,0)" })
.attr("fill", function(d,i) { return yMetaColorScale(d); })
xMetas.exit().remove()
yMetas.exit().remove()
// CELLS
var cellsEnter = cellWrap
.enter()
.append('g')
.style('opacity', 1e-6)
.attr("transform", function(d) { return "translate(0," + getIY(d) * cellHeight + ")" }) // enter all g's here for a sweep-right transition
.attr('data-row', function(d) { return getIY(d) })
.attr('data-column', function(d) { return getIX(d) });
cellsEnter
.append("rect")
cellsEnter
.append('text')
.attr('text-anchor', 'middle')
.attr("dy", 4)
.attr("class","cell-text")
// transition cell (rect) size
cellWrap.selectAll('rect')
.watchTransition(renderWatch, 'heatMap: rect')
.attr("width", cellWidth-cellBorderWidth)
.attr("height", cellHeight-cellBorderWidth)
.attr('rx', cellRadius)
.attr('ry', cellRadius)
.style('stroke', function(d) { return cellColor(d) })
// transition cell (g) position, opacity and fill
cellWrap
.attr("class",function(d) { return isNaN(getCellValue(d)) ? 'nv-cell cell-missing' : 'nv-cell'})
.watchTransition(renderWatch, 'heatMap: cells')
.style({
'opacity': 1,
'fill': function(d) { return cellColor(d) },
})
.attr("transform", function(d) { return "translate(" + getIX(d) * cellWidth + "," + getIY(d) * cellHeight + ")" })
.attr("class",function(d) { return isNaN(getCellValue(d)) ? 'nv-cell cell-missing' : 'nv-cell'})
cellWrap.exit().remove();
// transition text position and fill
cellWrap.selectAll('text')
.watchTransition(renderWatch, 'heatMap: cells text')
.text(function(d) { return cellValueLabel(d); })
.attr("x", function(d) { return (cellWidth-cellBorderWidth) / 2; })
.attr("y", function(d) { return (cellHeight-cellBorderWidth) / 2; })
.style("fill", function(d) { return cellTextColor(cellColor(d)) })
.style('opacity', function() { return showCellValues ? 1 : 0 })
// transition grid
wrap.selectAll('.verticalGrid')
.watchTransition(renderWatch, 'heatMap: gridLines')
.attr('y1',0)
.attr('y2',availableHeight-cellBorderWidth)
.attr('x1',function(d) { return d*cellWidth-cellBorderWidth/2; })
.attr('x2',function(d) { return d*cellWidth-cellBorderWidth/2; })
var numHLines = Object.keys(uniqueY).length;
wrap.selectAll('.horizontalGrid')
.watchTransition(renderWatch, 'heatMap: gridLines')
.attr('x1',function(d) { return (d == 0 || d == numHLines) ? -cellBorderWidth : 0 })
.attr('x2',function(d) { return (d == 0 || d == numHLines) ? availableWidth : availableWidth-cellBorderWidth})
.attr('y1',function(d) { return d*cellHeight-cellBorderWidth/2; })
.attr('y2',function(d) { return d*cellHeight-cellBorderWidth/2; })
wrap.select('.cellGrid')
.watchTransition(renderWatch, 'heatMap: gridLines')
.style({
'stroke-width': cellBorderWidth,
'opacity': function() { return showGrid ? 1 : 1e-6 },
})
var xMetaRect = wrap.selectAll('.x-meta')
var yMetaRect = wrap.selectAll('.y-meta')
var allMetaRect = wrap.selectAll('.meta')
// transition meta rect size
xMetas
.watchTransition(renderWatch, 'heatMap: xMetaRect')
.attr("width", cellWidth-cellBorderWidth)
.attr("height", xMetaHeight())
.attr("transform", function(d,i) { return "translate(" + (i * cellWidth) + ",0)" })
yMetas
.watchTransition(renderWatch, 'heatMap: yMetaRect')
.attr("width", yMetaWidth())
.attr("height", cellHeight-cellBorderWidth)
.attr("transform", function(d,i) { return "translate(0," + (i * cellHeight) + ")" })
// transition position of meta wrap g & opacity
wrap.select('.xMetaWrap')
.watchTransition(renderWatch, 'heatMap: xMetaWrap')
.attr("transform", function(d,i) { return "translate(0," + (-xMetaHeight()-cellBorderWidth-metaOffset) + ")" })
.style("opacity", function() { return xMeta !== false ? 1 : 0 })
wrap.select('.yMetaWrap')
.watchTransition(renderWatch, 'heatMap: yMetaWrap')
.attr("transform", function(d,i) { return "translate(" + (-yMetaWidth()-cellBorderWidth-metaOffset) + ",0)" })
.style("opacity", function() { return yMeta !== false ? 1 : 0 })
// TOOLTIPS
cellWrap
.on('mouseover', function(d,i) {
var idx = getIdx(d);
var ix = getIX(d);
var iy = getIY(d);
// set the proper classes for all cells
// hover row gets class .row-hover
// hover column gets class .column-hover
// hover cell gets class .cell-hover
// all remaining cells get class .no-hover
d3.selectAll('.nv-cell').each(function(e) {
if (idx == getIdx(e)) {
d3.select(this).classed('cell-hover', true);
d3.select(this).classed('no-hover', false);
} else {
d3.select(this).classed('no-hover', true);
d3.select(this).classed('cell-hover', false);
}
if (ix == getIX(e)) {
d3.select(this).classed('no-hover', false);
d3.select(this).classed('column-hover', true);
}
if (iy == getIY(e)) {
d3.select(this).classed('no-hover', false);
d3.select(this).classed('row-hover', true);
}
})
// set hover classes for column metadata
d3.selectAll('.x-meta').each(function(e, j) {
if (j == ix) {
d3.select(this).classed('cell-hover', true);
d3.select(this).classed('no-hover', false);
} else {
d3.select(this).classed('no-hover', true);
d3.select(this).classed('cell-hover', false);
}
});
// set hover class for row metadata
d3.selectAll('.y-meta').each(function(e, j) {
if (j == iy) {
d3.select(this).classed('cell-hover', true);
d3.select(this).classed('no-hover', false);
} else {
d3.select(this).classed('no-hover', true);
d3.select(this).classed('cell-hover', false);
}
});
dispatch.elementMouseover({
value: getKeyByValue(uniqueX, ix) + ' & ' + getKeyByValue(uniqueY, iy),
series: {
value: cellValueLabel(d),
color: d3.select(this).select('rect').style("fill")
},
e: d3.event,
});
})
.on('mouseout', function(d,i) {
// allow tooltip to remain even when mouse is over the
// space between the cell;
// this prevents cells from "flashing" when transitioning
// between cells
var bBox = d3.select(this).select('rect').node().getBBox();
var coordinates = d3.mouse(d3.select('.nv-heatMap').node());
var x = coordinates[0];
var y = coordinates[1];
// we only trigger mouseout when mouse moves outside of
// .nv-heatMap
if (x + cellBorderWidth >= availableWidth || y + cellBorderWidth >= availableHeight || x < 0 || y < 0) {
// remove all hover classes
removeAllHoverClasses();
dispatch.elementMouseout({e: d3.event});
}
})
.on('mousemove', function(d,i) {
dispatch.elementMousemove({e: d3.event});
})
allMetaRect
.on('mouseover', function(d,i) {
// true if hovering over a row metadata rect
var isColMeta = d3.select(this).attr('class').indexOf('x-meta') != -1 ? true : false;
// apply proper .row-hover & .column-hover
// classes to cells
d3.selectAll('.nv-cell').each(function(e) {
if (isColMeta && i == getIX(e)) {
d3.select(this).classed('column-hover', true);
d3.select(this).classed('no-hover', false);
} else if (!isColMeta && i-uniqueXMeta.length == getIY(e)) {
// since allMetaRect selects all the meta rects, the index for the y's will
// be offset by the number of x rects. TODO - write seperate tooltip sections
// for x meta rect & y meta rect
d3.select(this).classed('row-hover', true);
d3.select(this).classed('no-hover', false);
} else {
d3.select(this).classed('no-hover', true);
d3.select(this).classed('column-hover', false);
d3.select(this).classed('row-hover', false);
}
d3.select(this).classed('cell-hover', false);
})
// apply proper .row-hover & .column-hover
// classes to meta rects
d3.selectAll('.meta').classed('no-hover', true);
d3.select(this).classed('cell-hover', true);
d3.select(this).classed('no-hover', false);
dispatch.elementMouseover({
value: isColMeta ? 'Column meta' : 'Row meta',
series: { value: d, color: d3.select(this).style('fill'), }
});
})
.on('mouseout', function(d,i) {
// true if hovering over a row metadata rect
var isColMeta = d3.select(this).attr('class').indexOf('x-meta') != -1 ? true : false;
// allow tooltip to remain even when mouse is over the
// space between the cell;
// this prevents cells from "flashing" when transitioning
// between cells
var bBox = d3.select(this).node().getBBox();
var coordinates = d3.mouse(d3.select(isColMeta ? '.xMetaWrap' : '.yMetaWrap').node());
var x = coordinates[0];
var y = coordinates[1];
if ( y < 0 || x < 0 ||
(isColMeta && x + cellBorderWidth >= availableWidth) ||
(!isColMeta && y + cellBorderWidth >= availableHeight)
) {
// remove all hover classes
removeAllHoverClasses();
dispatch.elementMouseout({e: d3.event});
}
})
.on('mousemove', function(d,i) {
dispatch.elementMousemove({e: d3.event});
})
});
renderWatch.renderEnd('heatMap immediate');
return chart;
}
//============================================================
// Expose Public Variables
//------------------------------------------------------------
chart.dispatch = dispatch;
chart.options = nv.utils.optionsFunc.bind(chart);
chart._options = Object.create({}, {
// simple options, just get/set the necessary values
width: {get: function(){return width;}, set: function(_){width=_;}},
height: {get: function(){return height;}, set: function(_){height=_;}},
showCellValues: {get: function(){return showCellValues;}, set: function(_){showCellValues=_;}},
x: {get: function(){return getX;}, set: function(_){getX=_;}}, // data attribute for horizontal axis
y: {get: function(){return getY;}, set: function(_){getY=_;}}, // data attribute for vertical axis
cellValue: {get: function(){return getCellValue;}, set: function(_){getCellValue=_;}}, // data attribute that sets cell value and color
missingDataColor: {get: function(){return missingDataColor;}, set: function(_){missingDataColor=_;}},
missingDataLabel: {get: function(){return missingDataLabel;}, set: function(_){missingDataLabel=_;}},
xScale: {get: function(){return xScale;}, set: function(_){xScale=_;}},
yScale: {get: function(){return yScale;}, set: function(_){yScale=_;}},
colorScale: {get: function(){return colorScale;}, set: function(_){colorScale=_;}}, // scale to map cell values to colors
xDomain: {get: function(){return xDomain;}, set: function(_){xDomain=_;}},
yDomain: {get: function(){return yDomain;}, set: function(_){yDomain=_;}},
xRange: {get: function(){return xRange;}, set: function(_){xRange=_;}},
yRange: {get: function(){return yRange;}, set: function(_){yRange=_;}},
colorRange: {get: function(){return colorRange;}, set: function(_){colorRange=_;}},
colorDomain: {get: function(){return colorDomain;}, set: function(_){colorDomain=_;}},
xMeta: {get: function(){return xMeta;}, set: function(_){xMeta=_;}},
yMeta: {get: function(){return yMeta;}, set: function(_){yMeta=_;}},
xMetaColorScale: {get: function(){return color;}, set: function(_){color = nv.utils.getColor(_);}},
yMetaColorScale: {get: function(){return color;}, set: function(_){color = nv.utils.getColor(_);}},
cellAspectRatio: {get: function(){return cellAspectRatio;}, set: function(_){cellAspectRatio=_;}}, // cell width / height
cellRadius: {get: function(){return cellRadius;}, set: function(_){cellRadius=_;}}, // cell width / height
cellHeight: {get: function(){return cellHeight;}}, // TODO - should not be exposed since we don't want user setting this
cellWidth: {get: function(){return cellWidth;}}, // TODO - should not be exposed since we don't want user setting this
normalize: {get: function(){return normalize;}, set: function(_){normalize=_;}},
cellBorderWidth: {get: function(){return cellBorderWidth;}, set: function(_){cellBorderWidth=_;}},
highContrastText: {get: function(){return highContrastText;}, set: function(_){highContrastText=_;}},
cellValueFormat: {get: function(){return cellValueFormat;}, set: function(_){cellValueFormat=_;}},
id: {get: function(){return id;}, set: function(_){id=_;}},
metaOffset: {get: function(){return metaOffset;}, set: function(_){metaOffset=_;}},
xMetaHeight: {get: function(){return xMetaHeight;}, set: function(_){xMetaHeight=_;}},
yMetaWidth: {get: function(){return yMetaWidth;}, set: function(_){yMetaWidth=_;}},
showGrid: {get: function(){return showGrid;}, set: function(_){showGrid=_;}},
// options that require extra logic in the setter
margin: {get: function(){return margin;}, set: function(_){
margin.top = _.top !== undefined ? _.top : margin.top;
margin.right = _.right !== undefined ? _.right : margin.right;
margin.bottom = _.bottom !== undefined ? _.bottom : margin.bottom;
margin.left = _.left !== undefined ? _.left : margin.left;
}},
duration: {get: function(){return duration;}, set: function(_){
duration = _;
renderWatch.reset(duration);
}}
});
nv.utils.initOptions(chart);
return chart;
};