877 lines
32 KiB
JavaScript
877 lines
32 KiB
JavaScript
|
/* *
|
||
|
*
|
||
|
* (c) 2010-2020 Torstein Honsi
|
||
|
*
|
||
|
* Extensions to the SVGRenderer class to enable 3D shapes
|
||
|
*
|
||
|
* License: www.highcharts.com/license
|
||
|
*
|
||
|
* !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!!
|
||
|
*
|
||
|
* */
|
||
|
'use strict';
|
||
|
import H from '../parts/Globals.js';
|
||
|
import U from '../parts/Utilities.js';
|
||
|
var animObject = U.animObject, defined = U.defined, extend = U.extend, merge = U.merge, objectEach = U.objectEach, pick = U.pick;
|
||
|
import '../parts/Color.js';
|
||
|
import '../parts/SvgRenderer.js';
|
||
|
var cos = Math.cos, PI = Math.PI, sin = Math.sin;
|
||
|
var charts = H.charts, color = H.color, deg2rad = H.deg2rad, perspective = H.perspective, SVGElement = H.SVGElement, SVGRenderer = H.SVGRenderer,
|
||
|
// internal:
|
||
|
dFactor, element3dMethods, cuboidMethods;
|
||
|
/*
|
||
|
EXTENSION TO THE SVG-RENDERER TO ENABLE 3D SHAPES
|
||
|
*/
|
||
|
// HELPER METHODS
|
||
|
dFactor = (4 * (Math.sqrt(2) - 1) / 3) / (PI / 2);
|
||
|
/* eslint-disable no-invalid-this, valid-jsdoc */
|
||
|
/**
|
||
|
* Method to construct a curved path. Can 'wrap' around more then 180 degrees.
|
||
|
* @private
|
||
|
*/
|
||
|
function curveTo(cx, cy, rx, ry, start, end, dx, dy) {
|
||
|
var result = [], arcAngle = end - start;
|
||
|
if ((end > start) && (end - start > Math.PI / 2 + 0.0001)) {
|
||
|
result = result.concat(curveTo(cx, cy, rx, ry, start, start + (Math.PI / 2), dx, dy));
|
||
|
result = result.concat(curveTo(cx, cy, rx, ry, start + (Math.PI / 2), end, dx, dy));
|
||
|
return result;
|
||
|
}
|
||
|
if ((end < start) && (start - end > Math.PI / 2 + 0.0001)) {
|
||
|
result = result.concat(curveTo(cx, cy, rx, ry, start, start - (Math.PI / 2), dx, dy));
|
||
|
result = result.concat(curveTo(cx, cy, rx, ry, start - (Math.PI / 2), end, dx, dy));
|
||
|
return result;
|
||
|
}
|
||
|
return [[
|
||
|
'C',
|
||
|
cx + (rx * Math.cos(start)) -
|
||
|
((rx * dFactor * arcAngle) * Math.sin(start)) + dx,
|
||
|
cy + (ry * Math.sin(start)) +
|
||
|
((ry * dFactor * arcAngle) * Math.cos(start)) + dy,
|
||
|
cx + (rx * Math.cos(end)) +
|
||
|
((rx * dFactor * arcAngle) * Math.sin(end)) + dx,
|
||
|
cy + (ry * Math.sin(end)) -
|
||
|
((ry * dFactor * arcAngle) * Math.cos(end)) + dy,
|
||
|
cx + (rx * Math.cos(end)) + dx,
|
||
|
cy + (ry * Math.sin(end)) + dy
|
||
|
]];
|
||
|
}
|
||
|
SVGRenderer.prototype.toLinePath = function (points, closed) {
|
||
|
var result = [];
|
||
|
// Put "L x y" for each point
|
||
|
points.forEach(function (point) {
|
||
|
result.push(['L', point.x, point.y]);
|
||
|
});
|
||
|
if (points.length) {
|
||
|
// Set the first element to M
|
||
|
result[0][0] = 'M';
|
||
|
// If it is a closed line, add Z
|
||
|
if (closed) {
|
||
|
result.push(['Z']);
|
||
|
}
|
||
|
}
|
||
|
return result;
|
||
|
};
|
||
|
SVGRenderer.prototype.toLineSegments = function (points) {
|
||
|
var result = [], m = true;
|
||
|
points.forEach(function (point) {
|
||
|
result.push(m ? ['M', point.x, point.y] : ['L', point.x, point.y]);
|
||
|
m = !m;
|
||
|
});
|
||
|
return result;
|
||
|
};
|
||
|
// A 3-D Face is defined by it's 3D vertexes, and is only visible if it's
|
||
|
// vertexes are counter-clockwise (Back-face culling). It is used as a
|
||
|
// polyhedron Element
|
||
|
SVGRenderer.prototype.face3d = function (args) {
|
||
|
var renderer = this, ret = this.createElement('path');
|
||
|
ret.vertexes = [];
|
||
|
ret.insidePlotArea = false;
|
||
|
ret.enabled = true;
|
||
|
ret.attr = function (hash) {
|
||
|
if (typeof hash === 'object' &&
|
||
|
(defined(hash.enabled) ||
|
||
|
defined(hash.vertexes) ||
|
||
|
defined(hash.insidePlotArea))) {
|
||
|
this.enabled = pick(hash.enabled, this.enabled);
|
||
|
this.vertexes = pick(hash.vertexes, this.vertexes);
|
||
|
this.insidePlotArea = pick(hash.insidePlotArea, this.insidePlotArea);
|
||
|
delete hash.enabled;
|
||
|
delete hash.vertexes;
|
||
|
delete hash.insidePlotArea;
|
||
|
var chart = charts[renderer.chartIndex], vertexes2d = perspective(this.vertexes, chart, this.insidePlotArea), path = renderer.toLinePath(vertexes2d, true), area = H.shapeArea(vertexes2d), visibility = (this.enabled && area > 0) ? 'visible' : 'hidden';
|
||
|
hash.d = path;
|
||
|
hash.visibility = visibility;
|
||
|
}
|
||
|
return SVGElement.prototype.attr.apply(this, arguments);
|
||
|
};
|
||
|
ret.animate = function (params) {
|
||
|
if (typeof params === 'object' &&
|
||
|
(defined(params.enabled) ||
|
||
|
defined(params.vertexes) ||
|
||
|
defined(params.insidePlotArea))) {
|
||
|
this.enabled = pick(params.enabled, this.enabled);
|
||
|
this.vertexes = pick(params.vertexes, this.vertexes);
|
||
|
this.insidePlotArea = pick(params.insidePlotArea, this.insidePlotArea);
|
||
|
delete params.enabled;
|
||
|
delete params.vertexes;
|
||
|
delete params.insidePlotArea;
|
||
|
var chart = charts[renderer.chartIndex], vertexes2d = perspective(this.vertexes, chart, this.insidePlotArea), path = renderer.toLinePath(vertexes2d, true), area = H.shapeArea(vertexes2d), visibility = (this.enabled && area > 0) ? 'visible' : 'hidden';
|
||
|
params.d = path;
|
||
|
this.attr('visibility', visibility);
|
||
|
}
|
||
|
return SVGElement.prototype.animate.apply(this, arguments);
|
||
|
};
|
||
|
return ret.attr(args);
|
||
|
};
|
||
|
// A Polyhedron is a handy way of defining a group of 3-D faces. It's only
|
||
|
// attribute is `faces`, an array of attributes of each one of it's Face3D
|
||
|
// instances.
|
||
|
SVGRenderer.prototype.polyhedron = function (args) {
|
||
|
var renderer = this, result = this.g(), destroy = result.destroy;
|
||
|
if (!this.styledMode) {
|
||
|
result.attr({
|
||
|
'stroke-linejoin': 'round'
|
||
|
});
|
||
|
}
|
||
|
result.faces = [];
|
||
|
// destroy all children
|
||
|
result.destroy = function () {
|
||
|
for (var i = 0; i < result.faces.length; i++) {
|
||
|
result.faces[i].destroy();
|
||
|
}
|
||
|
return destroy.call(this);
|
||
|
};
|
||
|
result.attr = function (hash, val, complete, continueAnimation) {
|
||
|
if (typeof hash === 'object' && defined(hash.faces)) {
|
||
|
while (result.faces.length > hash.faces.length) {
|
||
|
result.faces.pop().destroy();
|
||
|
}
|
||
|
while (result.faces.length < hash.faces.length) {
|
||
|
result.faces.push(renderer.face3d().add(result));
|
||
|
}
|
||
|
for (var i = 0; i < hash.faces.length; i++) {
|
||
|
if (renderer.styledMode) {
|
||
|
delete hash.faces[i].fill;
|
||
|
}
|
||
|
result.faces[i].attr(hash.faces[i], null, complete, continueAnimation);
|
||
|
}
|
||
|
delete hash.faces;
|
||
|
}
|
||
|
return SVGElement.prototype.attr.apply(this, arguments);
|
||
|
};
|
||
|
result.animate = function (params, duration, complete) {
|
||
|
if (params && params.faces) {
|
||
|
while (result.faces.length > params.faces.length) {
|
||
|
result.faces.pop().destroy();
|
||
|
}
|
||
|
while (result.faces.length < params.faces.length) {
|
||
|
result.faces.push(renderer.face3d().add(result));
|
||
|
}
|
||
|
for (var i = 0; i < params.faces.length; i++) {
|
||
|
result.faces[i].animate(params.faces[i], duration, complete);
|
||
|
}
|
||
|
delete params.faces;
|
||
|
}
|
||
|
return SVGElement.prototype.animate.apply(this, arguments);
|
||
|
};
|
||
|
return result.attr(args);
|
||
|
};
|
||
|
// Base, abstract prototype member for 3D elements
|
||
|
element3dMethods = {
|
||
|
/**
|
||
|
* The init is used by base - renderer.Element
|
||
|
* @private
|
||
|
*/
|
||
|
initArgs: function (args) {
|
||
|
var elem3d = this, renderer = elem3d.renderer, paths = renderer[elem3d.pathType + 'Path'](args), zIndexes = paths.zIndexes;
|
||
|
// build parts
|
||
|
elem3d.parts.forEach(function (part) {
|
||
|
elem3d[part] = renderer.path(paths[part]).attr({
|
||
|
'class': 'highcharts-3d-' + part,
|
||
|
zIndex: zIndexes[part] || 0
|
||
|
}).add(elem3d);
|
||
|
});
|
||
|
elem3d.attr({
|
||
|
'stroke-linejoin': 'round',
|
||
|
zIndex: zIndexes.group
|
||
|
});
|
||
|
// store original destroy
|
||
|
elem3d.originalDestroy = elem3d.destroy;
|
||
|
elem3d.destroy = elem3d.destroyParts;
|
||
|
// Store information if any side of element was rendered by force.
|
||
|
elem3d.forcedSides = paths.forcedSides;
|
||
|
},
|
||
|
/**
|
||
|
* Single property setter that applies options to each part
|
||
|
* @private
|
||
|
*/
|
||
|
singleSetterForParts: function (prop, val, values, verb, duration, complete) {
|
||
|
var elem3d = this, newAttr = {}, optionsToApply = [null, null, (verb || 'attr'), duration, complete], hasZIndexes = values && values.zIndexes;
|
||
|
if (!values) {
|
||
|
newAttr[prop] = val;
|
||
|
optionsToApply[0] = newAttr;
|
||
|
}
|
||
|
else {
|
||
|
// It is needed to deal with the whole group zIndexing
|
||
|
// in case of graph rotation
|
||
|
if (hasZIndexes && hasZIndexes.group) {
|
||
|
this.attr({
|
||
|
zIndex: hasZIndexes.group
|
||
|
});
|
||
|
}
|
||
|
objectEach(values, function (partVal, part) {
|
||
|
newAttr[part] = {};
|
||
|
newAttr[part][prop] = partVal;
|
||
|
// include zIndexes if provided
|
||
|
if (hasZIndexes) {
|
||
|
newAttr[part].zIndex = values.zIndexes[part] || 0;
|
||
|
}
|
||
|
});
|
||
|
optionsToApply[1] = newAttr;
|
||
|
}
|
||
|
return elem3d.processParts.apply(elem3d, optionsToApply);
|
||
|
},
|
||
|
/**
|
||
|
* Calls function for each part. Used for attr, animate and destroy.
|
||
|
* @private
|
||
|
*/
|
||
|
processParts: function (props, partsProps, verb, duration, complete) {
|
||
|
var elem3d = this;
|
||
|
elem3d.parts.forEach(function (part) {
|
||
|
// if different props for different parts
|
||
|
if (partsProps) {
|
||
|
props = pick(partsProps[part], false);
|
||
|
}
|
||
|
// only if something to set, but allow undefined
|
||
|
if (props !== false) {
|
||
|
elem3d[part][verb](props, duration, complete);
|
||
|
}
|
||
|
});
|
||
|
return elem3d;
|
||
|
},
|
||
|
/**
|
||
|
* Destroy all parts
|
||
|
* @private
|
||
|
*/
|
||
|
destroyParts: function () {
|
||
|
this.processParts(null, null, 'destroy');
|
||
|
return this.originalDestroy();
|
||
|
}
|
||
|
};
|
||
|
// CUBOID
|
||
|
cuboidMethods = merge(element3dMethods, {
|
||
|
parts: ['front', 'top', 'side'],
|
||
|
pathType: 'cuboid',
|
||
|
attr: function (args, val, complete, continueAnimation) {
|
||
|
// Resolve setting attributes by string name
|
||
|
if (typeof args === 'string' && typeof val !== 'undefined') {
|
||
|
var key = args;
|
||
|
args = {};
|
||
|
args[key] = val;
|
||
|
}
|
||
|
if (args.shapeArgs || defined(args.x)) {
|
||
|
return this.singleSetterForParts('d', null, this.renderer[this.pathType + 'Path'](args.shapeArgs || args));
|
||
|
}
|
||
|
return SVGElement.prototype.attr.call(this, args, void 0, complete, continueAnimation);
|
||
|
},
|
||
|
animate: function (args, duration, complete) {
|
||
|
if (defined(args.x) && defined(args.y)) {
|
||
|
var paths = this.renderer[this.pathType + 'Path'](args), forcedSides = paths.forcedSides;
|
||
|
this.singleSetterForParts('d', null, paths, 'animate', duration, complete);
|
||
|
this.attr({
|
||
|
zIndex: paths.zIndexes.group
|
||
|
});
|
||
|
// If sides that are forced to render changed, recalculate colors.
|
||
|
if (forcedSides !== this.forcedSides) {
|
||
|
this.forcedSides = forcedSides;
|
||
|
cuboidMethods.fillSetter.call(this, this.fill);
|
||
|
}
|
||
|
}
|
||
|
else {
|
||
|
SVGElement.prototype.animate.call(this, args, duration, complete);
|
||
|
}
|
||
|
return this;
|
||
|
},
|
||
|
fillSetter: function (fill) {
|
||
|
var elem3d = this;
|
||
|
elem3d.forcedSides = elem3d.forcedSides || [];
|
||
|
elem3d.singleSetterForParts('fill', null, {
|
||
|
front: fill,
|
||
|
// Do not change color if side was forced to render.
|
||
|
top: color(fill).brighten(elem3d.forcedSides.indexOf('top') >= 0 ? 0 : 0.1).get(),
|
||
|
side: color(fill).brighten(elem3d.forcedSides.indexOf('side') >= 0 ? 0 : -0.1).get()
|
||
|
});
|
||
|
// fill for animation getter (#6776)
|
||
|
elem3d.color = elem3d.fill = fill;
|
||
|
return elem3d;
|
||
|
}
|
||
|
});
|
||
|
// set them up
|
||
|
SVGRenderer.prototype.elements3d = {
|
||
|
base: element3dMethods,
|
||
|
cuboid: cuboidMethods
|
||
|
};
|
||
|
/**
|
||
|
* return result, generalization
|
||
|
* @private
|
||
|
* @requires highcharts-3d
|
||
|
*/
|
||
|
SVGRenderer.prototype.element3d = function (type, shapeArgs) {
|
||
|
// base
|
||
|
var ret = this.g();
|
||
|
// extend
|
||
|
extend(ret, this.elements3d[type]);
|
||
|
// init
|
||
|
ret.initArgs(shapeArgs);
|
||
|
// return
|
||
|
return ret;
|
||
|
};
|
||
|
// generelized, so now use simply
|
||
|
SVGRenderer.prototype.cuboid = function (shapeArgs) {
|
||
|
return this.element3d('cuboid', shapeArgs);
|
||
|
};
|
||
|
// Generates a cuboid path and zIndexes
|
||
|
H.SVGRenderer.prototype.cuboidPath = function (shapeArgs) {
|
||
|
var x = shapeArgs.x, y = shapeArgs.y, z = shapeArgs.z || 0,
|
||
|
// For side calculation (right/left)
|
||
|
// there is a need for height (and other shapeArgs arguments)
|
||
|
// to be at least 1px
|
||
|
h = shapeArgs.height, w = shapeArgs.width, d = shapeArgs.depth, chart = charts[this.chartIndex], front, back, top, bottom, left, right, shape, path1, path2, path3, isFront, isTop, isRight, options3d = chart.options.chart.options3d, alpha = options3d.alpha,
|
||
|
// Priority for x axis is the biggest,
|
||
|
// because of x direction has biggest influence on zIndex
|
||
|
incrementX = 1000000,
|
||
|
// y axis has the smallest priority in case of our charts
|
||
|
// (needs to be set because of stacking)
|
||
|
incrementY = 10, incrementZ = 100, zIndex = 0,
|
||
|
// The 8 corners of the cube
|
||
|
pArr = [{
|
||
|
x: x,
|
||
|
y: y,
|
||
|
z: z
|
||
|
}, {
|
||
|
x: x + w,
|
||
|
y: y,
|
||
|
z: z
|
||
|
}, {
|
||
|
x: x + w,
|
||
|
y: y + h,
|
||
|
z: z
|
||
|
}, {
|
||
|
x: x,
|
||
|
y: y + h,
|
||
|
z: z
|
||
|
}, {
|
||
|
x: x,
|
||
|
y: y + h,
|
||
|
z: z + d
|
||
|
}, {
|
||
|
x: x + w,
|
||
|
y: y + h,
|
||
|
z: z + d
|
||
|
}, {
|
||
|
x: x + w,
|
||
|
y: y,
|
||
|
z: z + d
|
||
|
}, {
|
||
|
x: x,
|
||
|
y: y,
|
||
|
z: z + d
|
||
|
}], forcedSides = [], pickShape;
|
||
|
// apply perspective
|
||
|
pArr = perspective(pArr, chart, shapeArgs.insidePlotArea);
|
||
|
/**
|
||
|
* helper method to decide which side is visible
|
||
|
* @private
|
||
|
*/
|
||
|
function mapSidePath(i) {
|
||
|
// Added support for 0 value in columns, where height is 0
|
||
|
// but the shape is rendered.
|
||
|
// Height is used from 1st to 6th element of pArr
|
||
|
if (h === 0 && i > 1 && i < 6) { // [2, 3, 4, 5]
|
||
|
return {
|
||
|
x: pArr[i].x,
|
||
|
// when height is 0 instead of cuboid we render plane
|
||
|
// so it is needed to add fake 10 height to imitate cuboid
|
||
|
// for side calculation
|
||
|
y: pArr[i].y + 10,
|
||
|
z: pArr[i].z
|
||
|
};
|
||
|
}
|
||
|
// It is needed to calculate dummy sides (front/back) for breaking
|
||
|
// points in case of x and depth values. If column has side,
|
||
|
// it means that x values of front and back side are different.
|
||
|
if (pArr[0].x === pArr[7].x && i >= 4) { // [4, 5, 6, 7]
|
||
|
return {
|
||
|
x: pArr[i].x + 10,
|
||
|
// when height is 0 instead of cuboid we render plane
|
||
|
// so it is needed to add fake 10 height to imitate cuboid
|
||
|
// for side calculation
|
||
|
y: pArr[i].y,
|
||
|
z: pArr[i].z
|
||
|
};
|
||
|
}
|
||
|
// Added dummy depth
|
||
|
if (d === 0 && i < 2 || i > 5) { // [0, 1, 6, 7]
|
||
|
return {
|
||
|
x: pArr[i].x,
|
||
|
// when height is 0 instead of cuboid we render plane
|
||
|
// so it is needed to add fake 10 height to imitate cuboid
|
||
|
// for side calculation
|
||
|
y: pArr[i].y,
|
||
|
z: pArr[i].z + 10
|
||
|
};
|
||
|
}
|
||
|
return pArr[i];
|
||
|
}
|
||
|
/**
|
||
|
* method creating the final side
|
||
|
* @private
|
||
|
*/
|
||
|
function mapPath(i) {
|
||
|
return pArr[i];
|
||
|
}
|
||
|
/**
|
||
|
* First value - path with specific face
|
||
|
* Second value - added information about side for later calculations.
|
||
|
* Possible second values are 0 for path1, 1 for path2 and -1 for no path
|
||
|
* chosen.
|
||
|
* Third value - string containing information about current side
|
||
|
* of cuboid for forcing side rendering.
|
||
|
* @private
|
||
|
*/
|
||
|
pickShape = function (verticesIndex1, verticesIndex2, side) {
|
||
|
var ret = [[], -1],
|
||
|
// An array of vertices for cuboid face
|
||
|
face1 = verticesIndex1.map(mapPath), face2 = verticesIndex2.map(mapPath),
|
||
|
// dummy face is calculated the same way as standard face,
|
||
|
// but if cuboid height is 0 additional height is added so it is
|
||
|
// possible to use this vertices array for visible face calculation
|
||
|
dummyFace1 = verticesIndex1.map(mapSidePath), dummyFace2 = verticesIndex2.map(mapSidePath);
|
||
|
if (H.shapeArea(face1) < 0) {
|
||
|
ret = [face1, 0];
|
||
|
}
|
||
|
else if (H.shapeArea(face2) < 0) {
|
||
|
ret = [face2, 1];
|
||
|
}
|
||
|
else if (side) {
|
||
|
forcedSides.push(side);
|
||
|
if (H.shapeArea(dummyFace1) < 0) {
|
||
|
ret = [face1, 0];
|
||
|
}
|
||
|
else if (H.shapeArea(dummyFace2) < 0) {
|
||
|
ret = [face2, 1];
|
||
|
}
|
||
|
else {
|
||
|
ret = [face1, 0]; // force side calculation.
|
||
|
}
|
||
|
}
|
||
|
return ret;
|
||
|
};
|
||
|
// front or back
|
||
|
front = [3, 2, 1, 0];
|
||
|
back = [7, 6, 5, 4];
|
||
|
shape = pickShape(front, back, 'front');
|
||
|
path1 = shape[0];
|
||
|
isFront = shape[1];
|
||
|
// top or bottom
|
||
|
top = [1, 6, 7, 0];
|
||
|
bottom = [4, 5, 2, 3];
|
||
|
shape = pickShape(top, bottom, 'top');
|
||
|
path2 = shape[0];
|
||
|
isTop = shape[1];
|
||
|
// side
|
||
|
right = [1, 2, 5, 6];
|
||
|
left = [0, 7, 4, 3];
|
||
|
shape = pickShape(right, left, 'side');
|
||
|
path3 = shape[0];
|
||
|
isRight = shape[1];
|
||
|
/* New block used for calculating zIndex. It is basing on X, Y and Z
|
||
|
position of specific columns. All zIndexes (for X, Y and Z values) are
|
||
|
added to the final zIndex, where every value has different priority. The
|
||
|
biggest priority is in X and Z directions, the lowest index is for
|
||
|
stacked columns (Y direction and the same X and Z positions). Big
|
||
|
differences between priorities is made because we need to ensure that
|
||
|
even for big changes in Y and Z parameters all columns will be drawn
|
||
|
correctly. */
|
||
|
if (isRight === 1) {
|
||
|
// It is needed to connect value with current chart width
|
||
|
// for big chart size.
|
||
|
zIndex += incrementX * (chart.plotWidth - x);
|
||
|
}
|
||
|
else if (!isRight) {
|
||
|
zIndex += incrementX * x;
|
||
|
}
|
||
|
zIndex += incrementY * (!isTop ||
|
||
|
// Numbers checked empirically
|
||
|
(alpha >= 0 && alpha <= 180 || alpha < 360 && alpha > 357.5) ?
|
||
|
chart.plotHeight - y : 10 + y);
|
||
|
if (isFront === 1) {
|
||
|
zIndex += incrementZ * (z);
|
||
|
}
|
||
|
else if (!isFront) {
|
||
|
zIndex += incrementZ * (1000 - z);
|
||
|
}
|
||
|
return {
|
||
|
front: this.toLinePath(path1, true),
|
||
|
top: this.toLinePath(path2, true),
|
||
|
side: this.toLinePath(path3, true),
|
||
|
zIndexes: {
|
||
|
group: Math.round(zIndex)
|
||
|
},
|
||
|
forcedSides: forcedSides,
|
||
|
// additional info about zIndexes
|
||
|
isFront: isFront,
|
||
|
isTop: isTop
|
||
|
}; // #4774
|
||
|
};
|
||
|
// SECTORS //
|
||
|
H.SVGRenderer.prototype.arc3d = function (attribs) {
|
||
|
var wrapper = this.g(), renderer = wrapper.renderer, customAttribs = ['x', 'y', 'r', 'innerR', 'start', 'end', 'depth'];
|
||
|
/**
|
||
|
* Get custom attributes. Don't mutate the original object and return an
|
||
|
* object with only custom attr.
|
||
|
* @private
|
||
|
*/
|
||
|
function suckOutCustom(params) {
|
||
|
var hasCA = false, ca = {}, key;
|
||
|
params = merge(params); // Don't mutate the original object
|
||
|
for (key in params) {
|
||
|
if (customAttribs.indexOf(key) !== -1) {
|
||
|
ca[key] = params[key];
|
||
|
delete params[key];
|
||
|
hasCA = true;
|
||
|
}
|
||
|
}
|
||
|
return hasCA ? ca : false;
|
||
|
}
|
||
|
attribs = merge(attribs);
|
||
|
attribs.alpha = (attribs.alpha || 0) * deg2rad;
|
||
|
attribs.beta = (attribs.beta || 0) * deg2rad;
|
||
|
// Create the different sub sections of the shape
|
||
|
wrapper.top = renderer.path();
|
||
|
wrapper.side1 = renderer.path();
|
||
|
wrapper.side2 = renderer.path();
|
||
|
wrapper.inn = renderer.path();
|
||
|
wrapper.out = renderer.path();
|
||
|
// Add all faces
|
||
|
wrapper.onAdd = function () {
|
||
|
var parent = wrapper.parentGroup, className = wrapper.attr('class');
|
||
|
wrapper.top.add(wrapper);
|
||
|
// These faces are added outside the wrapper group because the z index
|
||
|
// relates to neighbour elements as well
|
||
|
['out', 'inn', 'side1', 'side2'].forEach(function (face) {
|
||
|
wrapper[face]
|
||
|
.attr({
|
||
|
'class': className + ' highcharts-3d-side'
|
||
|
})
|
||
|
.add(parent);
|
||
|
});
|
||
|
};
|
||
|
// Cascade to faces
|
||
|
['addClass', 'removeClass'].forEach(function (fn) {
|
||
|
wrapper[fn] = function () {
|
||
|
var args = arguments;
|
||
|
['top', 'out', 'inn', 'side1', 'side2'].forEach(function (face) {
|
||
|
wrapper[face][fn].apply(wrapper[face], args);
|
||
|
});
|
||
|
};
|
||
|
});
|
||
|
/**
|
||
|
* Compute the transformed paths and set them to the composite shapes
|
||
|
* @private
|
||
|
*/
|
||
|
wrapper.setPaths = function (attribs) {
|
||
|
var paths = wrapper.renderer.arc3dPath(attribs), zIndex = paths.zTop * 100;
|
||
|
wrapper.attribs = attribs;
|
||
|
wrapper.top.attr({ d: paths.top, zIndex: paths.zTop });
|
||
|
wrapper.inn.attr({ d: paths.inn, zIndex: paths.zInn });
|
||
|
wrapper.out.attr({ d: paths.out, zIndex: paths.zOut });
|
||
|
wrapper.side1.attr({ d: paths.side1, zIndex: paths.zSide1 });
|
||
|
wrapper.side2.attr({ d: paths.side2, zIndex: paths.zSide2 });
|
||
|
// show all children
|
||
|
wrapper.zIndex = zIndex;
|
||
|
wrapper.attr({ zIndex: zIndex });
|
||
|
// Set the radial gradient center the first time
|
||
|
if (attribs.center) {
|
||
|
wrapper.top.setRadialReference(attribs.center);
|
||
|
delete attribs.center;
|
||
|
}
|
||
|
};
|
||
|
wrapper.setPaths(attribs);
|
||
|
/**
|
||
|
* Apply the fill to the top and a darker shade to the sides
|
||
|
* @private
|
||
|
*/
|
||
|
wrapper.fillSetter = function (value) {
|
||
|
var darker = color(value).brighten(-0.1).get();
|
||
|
this.fill = value;
|
||
|
this.side1.attr({ fill: darker });
|
||
|
this.side2.attr({ fill: darker });
|
||
|
this.inn.attr({ fill: darker });
|
||
|
this.out.attr({ fill: darker });
|
||
|
this.top.attr({ fill: value });
|
||
|
return this;
|
||
|
};
|
||
|
// Apply the same value to all. These properties cascade down to the
|
||
|
// children when set to the composite arc3d.
|
||
|
['opacity', 'translateX', 'translateY', 'visibility'].forEach(function (setter) {
|
||
|
wrapper[setter + 'Setter'] = function (value, key) {
|
||
|
wrapper[key] = value;
|
||
|
['out', 'inn', 'side1', 'side2', 'top'].forEach(function (el) {
|
||
|
wrapper[el].attr(key, value);
|
||
|
});
|
||
|
};
|
||
|
});
|
||
|
// Override attr to remove shape attributes and use those to set child paths
|
||
|
wrapper.attr = function (params) {
|
||
|
var ca;
|
||
|
if (typeof params === 'object') {
|
||
|
ca = suckOutCustom(params);
|
||
|
if (ca) {
|
||
|
extend(wrapper.attribs, ca);
|
||
|
wrapper.setPaths(wrapper.attribs);
|
||
|
}
|
||
|
}
|
||
|
return SVGElement.prototype.attr.apply(wrapper, arguments);
|
||
|
};
|
||
|
// Override the animate function by sucking out custom parameters related to
|
||
|
// the shapes directly, and update the shapes from the animation step.
|
||
|
wrapper.animate = function (params, animation, complete) {
|
||
|
var ca, from = this.attribs, to, anim, randomProp = 'data-' + Math.random().toString(26).substring(2, 9);
|
||
|
// Attribute-line properties connected to 3D. These shouldn't have been
|
||
|
// in the attribs collection in the first place.
|
||
|
delete params.center;
|
||
|
delete params.z;
|
||
|
delete params.alpha;
|
||
|
delete params.beta;
|
||
|
anim = animObject(pick(animation, this.renderer.globalAnimation));
|
||
|
if (anim.duration) {
|
||
|
ca = suckOutCustom(params);
|
||
|
// Params need to have a property in order for the step to run
|
||
|
// (#5765, #7097, #7437)
|
||
|
wrapper[randomProp] = 0;
|
||
|
params[randomProp] = 1;
|
||
|
wrapper[randomProp + 'Setter'] = H.noop;
|
||
|
if (ca) {
|
||
|
to = ca;
|
||
|
anim.step = function (a, fx) {
|
||
|
/**
|
||
|
* @private
|
||
|
*/
|
||
|
function interpolate(key) {
|
||
|
return from[key] + (pick(to[key], from[key]) -
|
||
|
from[key]) * fx.pos;
|
||
|
}
|
||
|
if (fx.prop === randomProp) {
|
||
|
fx.elem.setPaths(merge(from, {
|
||
|
x: interpolate('x'),
|
||
|
y: interpolate('y'),
|
||
|
r: interpolate('r'),
|
||
|
innerR: interpolate('innerR'),
|
||
|
start: interpolate('start'),
|
||
|
end: interpolate('end'),
|
||
|
depth: interpolate('depth')
|
||
|
}));
|
||
|
}
|
||
|
};
|
||
|
}
|
||
|
animation = anim; // Only when duration (#5572)
|
||
|
}
|
||
|
return SVGElement.prototype.animate.call(this, params, animation, complete);
|
||
|
};
|
||
|
// destroy all children
|
||
|
wrapper.destroy = function () {
|
||
|
this.top.destroy();
|
||
|
this.out.destroy();
|
||
|
this.inn.destroy();
|
||
|
this.side1.destroy();
|
||
|
this.side2.destroy();
|
||
|
return SVGElement.prototype.destroy.call(this);
|
||
|
};
|
||
|
// hide all children
|
||
|
wrapper.hide = function () {
|
||
|
this.top.hide();
|
||
|
this.out.hide();
|
||
|
this.inn.hide();
|
||
|
this.side1.hide();
|
||
|
this.side2.hide();
|
||
|
};
|
||
|
wrapper.show = function (inherit) {
|
||
|
this.top.show(inherit);
|
||
|
this.out.show(inherit);
|
||
|
this.inn.show(inherit);
|
||
|
this.side1.show(inherit);
|
||
|
this.side2.show(inherit);
|
||
|
};
|
||
|
return wrapper;
|
||
|
};
|
||
|
// Generate the paths required to draw a 3D arc
|
||
|
SVGRenderer.prototype.arc3dPath = function (shapeArgs) {
|
||
|
var cx = shapeArgs.x, // x coordinate of the center
|
||
|
cy = shapeArgs.y, // y coordinate of the center
|
||
|
start = shapeArgs.start, // start angle
|
||
|
end = shapeArgs.end - 0.00001, // end angle
|
||
|
r = shapeArgs.r, // radius
|
||
|
ir = shapeArgs.innerR || 0, // inner radius
|
||
|
d = shapeArgs.depth || 0, // depth
|
||
|
alpha = shapeArgs.alpha, // alpha rotation of the chart
|
||
|
beta = shapeArgs.beta; // beta rotation of the chart
|
||
|
// Derived Variables
|
||
|
var cs = Math.cos(start), // cosinus of the start angle
|
||
|
ss = Math.sin(start), // sinus of the start angle
|
||
|
ce = Math.cos(end), // cosinus of the end angle
|
||
|
se = Math.sin(end), // sinus of the end angle
|
||
|
rx = r * Math.cos(beta), // x-radius
|
||
|
ry = r * Math.cos(alpha), // y-radius
|
||
|
irx = ir * Math.cos(beta), // x-radius (inner)
|
||
|
iry = ir * Math.cos(alpha), // y-radius (inner)
|
||
|
dx = d * Math.sin(beta), // distance between top and bottom in x
|
||
|
dy = d * Math.sin(alpha); // distance between top and bottom in y
|
||
|
// TOP
|
||
|
var top = [
|
||
|
['M', cx + (rx * cs), cy + (ry * ss)]
|
||
|
];
|
||
|
top = top.concat(curveTo(cx, cy, rx, ry, start, end, 0, 0));
|
||
|
top.push([
|
||
|
'L', cx + (irx * ce), cy + (iry * se)
|
||
|
]);
|
||
|
top = top.concat(curveTo(cx, cy, irx, iry, end, start, 0, 0));
|
||
|
top.push(['Z']);
|
||
|
// OUTSIDE
|
||
|
var b = (beta > 0 ? Math.PI / 2 : 0), a = (alpha > 0 ? 0 : Math.PI / 2);
|
||
|
var start2 = start > -b ? start : (end > -b ? -b : start), end2 = end < PI - a ? end : (start < PI - a ? PI - a : end), midEnd = 2 * PI - a;
|
||
|
// When slice goes over bottom middle, need to add both, left and right
|
||
|
// outer side. Additionally, when we cross right hand edge, create sharp
|
||
|
// edge. Outer shape/wall:
|
||
|
//
|
||
|
// -------
|
||
|
// / ^ \
|
||
|
// 4) / / \ \ 1)
|
||
|
// / / \ \
|
||
|
// / / \ \
|
||
|
// (c)=> ==== ==== <=(d)
|
||
|
// \ \ / /
|
||
|
// \ \<=(a)/ /
|
||
|
// \ \ / / <=(b)
|
||
|
// 3) \ v / 2)
|
||
|
// -------
|
||
|
//
|
||
|
// (a) - inner side
|
||
|
// (b) - outer side
|
||
|
// (c) - left edge (sharp)
|
||
|
// (d) - right edge (sharp)
|
||
|
// 1..n - rendering order for startAngle = 0, when set to e.g 90, order
|
||
|
// changes clockwise (1->2, 2->3, n->1) and counterclockwise for negative
|
||
|
// startAngle
|
||
|
var out = [
|
||
|
['M', cx + (rx * cos(start2)), cy + (ry * sin(start2))]
|
||
|
];
|
||
|
out = out.concat(curveTo(cx, cy, rx, ry, start2, end2, 0, 0));
|
||
|
// When shape is wide, it can cross both, (c) and (d) edges, when using
|
||
|
// startAngle
|
||
|
if (end > midEnd && start < midEnd) {
|
||
|
// Go to outer side
|
||
|
out.push([
|
||
|
'L', cx + (rx * cos(end2)) + dx, cy + (ry * sin(end2)) + dy
|
||
|
]);
|
||
|
// Curve to the right edge of the slice (d)
|
||
|
out = out.concat(curveTo(cx, cy, rx, ry, end2, midEnd, dx, dy));
|
||
|
// Go to the inner side
|
||
|
out.push([
|
||
|
'L', cx + (rx * cos(midEnd)), cy + (ry * sin(midEnd))
|
||
|
]);
|
||
|
// Curve to the true end of the slice
|
||
|
out = out.concat(curveTo(cx, cy, rx, ry, midEnd, end, 0, 0));
|
||
|
// Go to the outer side
|
||
|
out.push([
|
||
|
'L', cx + (rx * cos(end)) + dx, cy + (ry * sin(end)) + dy
|
||
|
]);
|
||
|
// Go back to middle (d)
|
||
|
out = out.concat(curveTo(cx, cy, rx, ry, end, midEnd, dx, dy));
|
||
|
out.push([
|
||
|
'L', cx + (rx * cos(midEnd)), cy + (ry * sin(midEnd))
|
||
|
]);
|
||
|
// Go back to the left edge
|
||
|
out = out.concat(curveTo(cx, cy, rx, ry, midEnd, end2, 0, 0));
|
||
|
// But shape can cross also only (c) edge:
|
||
|
}
|
||
|
else if (end > PI - a && start < PI - a) {
|
||
|
// Go to outer side
|
||
|
out.push([
|
||
|
'L',
|
||
|
cx + (rx * Math.cos(end2)) + dx,
|
||
|
cy + (ry * Math.sin(end2)) + dy
|
||
|
]);
|
||
|
// Curve to the true end of the slice
|
||
|
out = out.concat(curveTo(cx, cy, rx, ry, end2, end, dx, dy));
|
||
|
// Go to the inner side
|
||
|
out.push([
|
||
|
'L', cx + (rx * Math.cos(end)), cy + (ry * Math.sin(end))
|
||
|
]);
|
||
|
// Go back to the artifical end2
|
||
|
out = out.concat(curveTo(cx, cy, rx, ry, end, end2, 0, 0));
|
||
|
}
|
||
|
out.push([
|
||
|
'L', cx + (rx * Math.cos(end2)) + dx, cy + (ry * Math.sin(end2)) + dy
|
||
|
]);
|
||
|
out = out.concat(curveTo(cx, cy, rx, ry, end2, start2, dx, dy));
|
||
|
out.push(['Z']);
|
||
|
// INSIDE
|
||
|
var inn = [
|
||
|
['M', cx + (irx * cs), cy + (iry * ss)]
|
||
|
];
|
||
|
inn = inn.concat(curveTo(cx, cy, irx, iry, start, end, 0, 0));
|
||
|
inn.push([
|
||
|
'L', cx + (irx * Math.cos(end)) + dx, cy + (iry * Math.sin(end)) + dy
|
||
|
]);
|
||
|
inn = inn.concat(curveTo(cx, cy, irx, iry, end, start, dx, dy));
|
||
|
inn.push(['Z']);
|
||
|
// SIDES
|
||
|
var side1 = [
|
||
|
['M', cx + (rx * cs), cy + (ry * ss)],
|
||
|
['L', cx + (rx * cs) + dx, cy + (ry * ss) + dy],
|
||
|
['L', cx + (irx * cs) + dx, cy + (iry * ss) + dy],
|
||
|
['L', cx + (irx * cs), cy + (iry * ss)],
|
||
|
['Z']
|
||
|
];
|
||
|
var side2 = [
|
||
|
['M', cx + (rx * ce), cy + (ry * se)],
|
||
|
['L', cx + (rx * ce) + dx, cy + (ry * se) + dy],
|
||
|
['L', cx + (irx * ce) + dx, cy + (iry * se) + dy],
|
||
|
['L', cx + (irx * ce), cy + (iry * se)],
|
||
|
['Z']
|
||
|
];
|
||
|
// correction for changed position of vanishing point caused by alpha and
|
||
|
// beta rotations
|
||
|
var angleCorr = Math.atan2(dy, -dx), angleEnd = Math.abs(end + angleCorr), angleStart = Math.abs(start + angleCorr), angleMid = Math.abs((start + end) / 2 + angleCorr);
|
||
|
/**
|
||
|
* set to 0-PI range
|
||
|
* @private
|
||
|
*/
|
||
|
function toZeroPIRange(angle) {
|
||
|
angle = angle % (2 * Math.PI);
|
||
|
if (angle > Math.PI) {
|
||
|
angle = 2 * Math.PI - angle;
|
||
|
}
|
||
|
return angle;
|
||
|
}
|
||
|
angleEnd = toZeroPIRange(angleEnd);
|
||
|
angleStart = toZeroPIRange(angleStart);
|
||
|
angleMid = toZeroPIRange(angleMid);
|
||
|
// *1e5 is to compensate pInt in zIndexSetter
|
||
|
var incPrecision = 1e5, a1 = angleMid * incPrecision, a2 = angleStart * incPrecision, a3 = angleEnd * incPrecision;
|
||
|
return {
|
||
|
top: top,
|
||
|
// max angle is PI, so this is always higher
|
||
|
zTop: Math.PI * incPrecision + 1,
|
||
|
out: out,
|
||
|
zOut: Math.max(a1, a2, a3),
|
||
|
inn: inn,
|
||
|
zInn: Math.max(a1, a2, a3),
|
||
|
side1: side1,
|
||
|
zSide1: a3 * 0.99,
|
||
|
side2: side2,
|
||
|
zSide2: a2 * 0.99
|
||
|
};
|
||
|
};
|