plotly.js
Version:
The open source javascript graphing library that powers plotly
363 lines (309 loc) • 12.8 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.slice(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.countDefiningCoords = function(shapeType, path, axLetter) {
// non-path shapes always have 2 defining coordinates
if(shapeType !== 'path') return 2;
if(!path) return 0;
const segments = path.match(constants.segmentRE);
if(!segments) return 0;
const paramIsAxis = axLetter === 'x' ? constants.paramIsX : constants.paramIsY;
return segments.reduce((coordCount, segment) => {
// for each path command, check if there is a drawn coordinate for this axis
const segmentType = segment.charAt(0);
const hasDrawn = paramIsAxis[segmentType].drawn !== undefined;
return coordCount + (hasDrawn ? 1 : 0);
}, 0);
};
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) {
const shapeType = options.type;
const xRefType = Axes.getRefType(options.xref);
const yRefType = Axes.getRefType(options.yref);
const gs = gd._fullLayout._size;
var xa, ya;
var xShiftStart, xShiftEnd, yShiftStart, yShiftEnd;
var x2p, y2p;
var x0, x1, y0, y1;
function getConverter(axis, refType, shapeType, isVertical) {
var converter;
if(axis) {
if(refType === 'domain') {
if(isVertical) {
converter = function(v) { return axis._offset + axis._length * (1 - v); };
} else {
converter = function(v) { return axis._offset + axis._length * v; };
}
} else {
const d2r = exports.shapePositionToRange(axis);
converter = function(v) { return axis._offset + axis.r2p(d2r(v, true)); };
if(shapeType === 'path' && axis.type === 'date') converter = exports.decodeDate(converter);
}
} else {
if(isVertical) {
converter = function(v) { return gs.t + gs.h * (1 - v); };
} else {
converter = function(v) { return gs.l + gs.w * v; };
}
}
return converter;
}
// Build function(s) to convert data to pixel
if(xRefType === 'array') {
x2p = [];
xa = options.xref.map(function(ref) { return Axes.getFromId(gd, ref); });
x2p = options.xref.map(function(ref, i) {
return getConverter(xa[i], Axes.getRefType(ref), shapeType, false);
});
} else {
xa = Axes.getFromId(gd, options.xref);
x2p = getConverter(xa, xRefType, shapeType, false);
}
if(yRefType === 'array') {
y2p = [];
ya = options.yref.map(function(ref) { return Axes.getFromId(gd, ref); });
y2p = options.yref.map(function(ref, i) {
return getConverter(ya[i], Axes.getRefType(ref), shapeType, true);
});
} else {
ya = Axes.getFromId(gd, options.yref);
y2p = getConverter(ya, yRefType, shapeType, true);
}
if(shapeType === 'path') { return convertPath(options, x2p, y2p); }
// Calculate pixel coordinates for non-path shapes
// Pixel sizemode for array refs is not supported for now
if(xRefType === 'array') {
xShiftStart = getPixelShift(xa[0], options.x0shift);
xShiftEnd = getPixelShift(xa[1], options.x1shift);
x0 = x2p[0](options.x0) + xShiftStart;
x1 = x2p[1](options.x1) + xShiftEnd;
} else {
xShiftStart = getPixelShift(xa, options.x0shift);
xShiftEnd = getPixelShift(xa, options.x1shift);
if(options.xsizemode === 'pixel') {
const 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(yRefType === 'array') {
yShiftStart = getPixelShift(ya[0], options.y0shift);
yShiftEnd = getPixelShift(ya[1], options.y1shift);
y0 = y2p[0](options.y0) + yShiftStart;
y1 = y2p[1](options.y1) + yShiftEnd;
} else {
yShiftStart = getPixelShift(ya, options.y0shift);
yShiftEnd = getPixelShift(ya, options.y1shift);
if(options.ysizemode === 'pixel') {
const 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(shapeType === 'line') return 'M' + x0 + ',' + y0 + 'L' + x1 + ',' + y1;
if(shapeType === '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) {
const pathIn = options.path;
const xSizemode = options.xsizemode;
const ySizemode = options.ysizemode;
const xAnchor = options.xanchor;
const yAnchor = options.yanchor;
const isArrayXref = Array.isArray(options.xref);
const isArrayYref = Array.isArray(options.yref);
var xVertexIndex = 0;
var yVertexIndex = 0;
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];
const hasDrawnX = xParams.drawn !== undefined;
const hasDrawnY = yParams.drawn !== undefined;
// Use vertex indices for array refs (same converter for all params in segment)
const segmentX2p = isArrayXref ? x2p[xVertexIndex] : x2p;
const segmentY2p = isArrayYref ? y2p[yVertexIndex] : y2p;
var paramString = segment.slice(1).replace(constants.paramRE, function(param) {
if(xParams[paramNumber]) {
if(xSizemode === 'pixel') param = segmentX2p(xAnchor) + Number(param);
else param = segmentX2p(param);
} else if(yParams[paramNumber]) {
if(ySizemode === 'pixel') param = segmentY2p(yAnchor) - Number(param);
else param = segmentY2p(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);
}
if(hasDrawnX) xVertexIndex++;
if(hasDrawnY) yVertexIndex++;
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;
}