plotly.js
Version:
The open source javascript graphing library that powers plotly
295 lines (248 loc) • 9.9 kB
JavaScript
;
var constants = require('./constants');
var Lib = require('../../lib');
var Axes = require('../../plots/cartesian/axes');
// special position conversion functions... category axis positions can't be
// specified by their data values, because they don't make a continuous mapping.
// so these have to be specified in terms of the category serial numbers,
// but can take fractional values. Other axis types we specify position based on
// the actual data values.
// TODO: in V3.0 (when log axis ranges are in data units) range and shape position
// will be identical, so rangeToShapePosition and shapePositionToRange can be
// removed entirely.
exports.rangeToShapePosition = function(ax) {
return (ax.type === 'log') ? ax.r2d : function(v) { return v; };
};
exports.shapePositionToRange = function(ax) {
return (ax.type === 'log') ? ax.d2r : function(v) { return v; };
};
exports.decodeDate = function(convertToPx) {
return function(v) {
if(v.replace) v = v.replace('_', ' ');
return convertToPx(v);
};
};
exports.encodeDate = function(convertToDate) {
return function(v) { return convertToDate(v).replace(' ', '_'); };
};
exports.extractPathCoords = function(path, paramsToUse, isRaw) {
var extractedCoordinates = [];
var segments = path.match(constants.segmentRE);
segments.forEach(function(segment) {
var relevantParamIdx = paramsToUse[segment.charAt(0)].drawn;
if(relevantParamIdx === undefined) return;
var params = segment.substr(1).match(constants.paramRE);
if(!params || params.length < relevantParamIdx) return;
var str = params[relevantParamIdx];
var pos = isRaw ? str : Lib.cleanNumber(str);
extractedCoordinates.push(pos);
});
return extractedCoordinates;
};
exports.getDataToPixel = function(gd, axis, shift, isVertical, refType) {
var gs = gd._fullLayout._size;
var dataToPixel;
if(axis) {
if(refType === 'domain') {
dataToPixel = function(v) {
return axis._length * (isVertical ? (1 - v) : v) + axis._offset;
};
} else {
var d2r = exports.shapePositionToRange(axis);
dataToPixel = function(v) {
var shiftPixels = getPixelShift(axis, shift);
return axis._offset + axis.r2p(d2r(v, true)) + shiftPixels;
};
if(axis.type === 'date') dataToPixel = exports.decodeDate(dataToPixel);
}
} else if(isVertical) {
dataToPixel = function(v) { return gs.t + gs.h * (1 - v); };
} else {
dataToPixel = function(v) { return gs.l + gs.w * v; };
}
return dataToPixel;
};
exports.getPixelToData = function(gd, axis, isVertical, opt) {
var gs = gd._fullLayout._size;
var pixelToData;
if(axis) {
if(opt === 'domain') {
pixelToData = function(p) {
var q = (p - axis._offset) / axis._length;
return isVertical ? 1 - q : q;
};
} else {
var r2d = exports.rangeToShapePosition(axis);
pixelToData = function(p) { return r2d(axis.p2r(p - axis._offset)); };
}
} else if(isVertical) {
pixelToData = function(p) { return 1 - (p - gs.t) / gs.h; };
} else {
pixelToData = function(p) { return (p - gs.l) / gs.w; };
}
return pixelToData;
};
/**
* Based on the given stroke width, rounds the passed
* position value to represent either a full or half pixel.
*
* In case of an odd stroke width (e.g. 1), this measure ensures
* that a stroke positioned at the returned position isn't rendered
* blurry due to anti-aliasing.
*
* In case of an even stroke width (e.g. 2), this measure ensures
* that the position value is transformed to a full pixel value
* so that anti-aliasing doesn't take effect either.
*
* @param {number} pos The raw position value to be transformed
* @param {number} strokeWidth The stroke width
* @returns {number} either an integer or a .5 decimal number
*/
exports.roundPositionForSharpStrokeRendering = function(pos, strokeWidth) {
var strokeWidthIsOdd = Math.round(strokeWidth % 2) === 1;
var posValAsInt = Math.round(pos);
return strokeWidthIsOdd ? posValAsInt + 0.5 : posValAsInt;
};
exports.makeShapesOptionsAndPlotinfo = function(gd, index) {
var options = gd._fullLayout.shapes[index] || {};
var plotinfo = gd._fullLayout._plots[options.xref + options.yref];
var hasPlotinfo = !!plotinfo;
if(hasPlotinfo) {
plotinfo._hadPlotinfo = true;
} else {
plotinfo = {};
if(options.xref && options.xref !== 'paper') plotinfo.xaxis = gd._fullLayout[options.xref + 'axis'];
if(options.yref && options.yref !== 'paper') plotinfo.yaxis = gd._fullLayout[options.yref + 'axis'];
}
plotinfo.xsizemode = options.xsizemode;
plotinfo.ysizemode = options.ysizemode;
plotinfo.xanchor = options.xanchor;
plotinfo.yanchor = options.yanchor;
return {
options: options,
plotinfo: plotinfo
};
};
// TODO: move to selections helpers?
exports.makeSelectionsOptionsAndPlotinfo = function(gd, index) {
var options = gd._fullLayout.selections[index] || {};
var plotinfo = gd._fullLayout._plots[options.xref + options.yref];
var hasPlotinfo = !!plotinfo;
if(hasPlotinfo) {
plotinfo._hadPlotinfo = true;
} else {
plotinfo = {};
if(options.xref) plotinfo.xaxis = gd._fullLayout[options.xref + 'axis'];
if(options.yref) plotinfo.yaxis = gd._fullLayout[options.yref + 'axis'];
}
return {
options: options,
plotinfo: plotinfo
};
};
exports.getPathString = function(gd, options) {
var type = options.type;
var xRefType = Axes.getRefType(options.xref);
var yRefType = Axes.getRefType(options.yref);
var xa = Axes.getFromId(gd, options.xref);
var ya = Axes.getFromId(gd, options.yref);
var gs = gd._fullLayout._size;
var x2r, x2p, y2r, y2p;
var xShiftStart = getPixelShift(xa, options.x0shift);
var xShiftEnd = getPixelShift(xa, options.x1shift);
var yShiftStart = getPixelShift(ya, options.y0shift);
var yShiftEnd = getPixelShift(ya, options.y1shift);
var x0, x1, y0, y1;
if(xa) {
if(xRefType === 'domain') {
x2p = function(v) { return xa._offset + xa._length * v; };
} else {
x2r = exports.shapePositionToRange(xa);
x2p = function(v) { return xa._offset + xa.r2p(x2r(v, true)); };
}
} else {
x2p = function(v) { return gs.l + gs.w * v; };
}
if(ya) {
if(yRefType === 'domain') {
y2p = function(v) { return ya._offset + ya._length * (1 - v); };
} else {
y2r = exports.shapePositionToRange(ya);
y2p = function(v) { return ya._offset + ya.r2p(y2r(v, true)); };
}
} else {
y2p = function(v) { return gs.t + gs.h * (1 - v); };
}
if(type === 'path') {
if(xa && xa.type === 'date') x2p = exports.decodeDate(x2p);
if(ya && ya.type === 'date') y2p = exports.decodeDate(y2p);
return convertPath(options, x2p, y2p);
}
if(options.xsizemode === 'pixel') {
var xAnchorPos = x2p(options.xanchor);
x0 = xAnchorPos + options.x0 + xShiftStart;
x1 = xAnchorPos + options.x1 + xShiftEnd;
} else {
x0 = x2p(options.x0) + xShiftStart;
x1 = x2p(options.x1) + xShiftEnd;
}
if(options.ysizemode === 'pixel') {
var yAnchorPos = y2p(options.yanchor);
y0 = yAnchorPos - options.y0 + yShiftStart;
y1 = yAnchorPos - options.y1 + yShiftEnd;
} else {
y0 = y2p(options.y0) + yShiftStart;
y1 = y2p(options.y1) + yShiftEnd;
}
if(type === 'line') return 'M' + x0 + ',' + y0 + 'L' + x1 + ',' + y1;
if(type === 'rect') return 'M' + x0 + ',' + y0 + 'H' + x1 + 'V' + y1 + 'H' + x0 + 'Z';
// circle
var cx = (x0 + x1) / 2;
var cy = (y0 + y1) / 2;
var rx = Math.abs(cx - x0);
var ry = Math.abs(cy - y0);
var rArc = 'A' + rx + ',' + ry;
var rightPt = (cx + rx) + ',' + cy;
var topPt = cx + ',' + (cy - ry);
return 'M' + rightPt + rArc + ' 0 1,1 ' + topPt +
rArc + ' 0 0,1 ' + rightPt + 'Z';
};
function convertPath(options, x2p, y2p) {
var pathIn = options.path;
var xSizemode = options.xsizemode;
var ySizemode = options.ysizemode;
var xAnchor = options.xanchor;
var yAnchor = options.yanchor;
return pathIn.replace(constants.segmentRE, function(segment) {
var paramNumber = 0;
var segmentType = segment.charAt(0);
var xParams = constants.paramIsX[segmentType];
var yParams = constants.paramIsY[segmentType];
var nParams = constants.numParams[segmentType];
var paramString = segment.substr(1).replace(constants.paramRE, function(param) {
if(xParams[paramNumber]) {
if(xSizemode === 'pixel') param = x2p(xAnchor) + Number(param);
else param = x2p(param);
} else if(yParams[paramNumber]) {
if(ySizemode === 'pixel') param = y2p(yAnchor) - Number(param);
else param = y2p(param);
}
paramNumber++;
if(paramNumber > nParams) param = 'X';
return param;
});
if(paramNumber > nParams) {
paramString = paramString.replace(/[\s,]*X.*/, '');
Lib.log('Ignoring extra params in segment ' + segment);
}
return segmentType + paramString;
});
}
function getPixelShift(axis, shift) {
shift = shift || 0;
var shiftPixels = 0;
if(shift && axis && (axis.type === 'category' || axis.type === 'multicategory')) {
shiftPixels = (axis.r2p(1) - axis.r2p(0)) * shift;
}
return shiftPixels;
}