initial commit
This commit is contained in:
@@ -0,0 +1,669 @@
|
||||
/* *
|
||||
* (c) 2010-2019 Torstein Honsi
|
||||
*
|
||||
* Extenstion for 3d axes
|
||||
*
|
||||
* License: www.highcharts.com/license
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import H from '../parts/Globals.js';
|
||||
import '../parts/Utilities.js';
|
||||
import '../parts/Axis.js';
|
||||
import '../parts/Chart.js';
|
||||
import '../parts/Tick.js';
|
||||
|
||||
var ZAxis,
|
||||
addEvent = H.addEvent,
|
||||
Axis = H.Axis,
|
||||
Chart = H.Chart,
|
||||
deg2rad = H.deg2rad,
|
||||
extend = H.extend,
|
||||
merge = H.merge,
|
||||
perspective = H.perspective,
|
||||
perspective3D = H.perspective3D,
|
||||
pick = H.pick,
|
||||
shapeArea = H.shapeArea,
|
||||
splat = H.splat,
|
||||
Tick = H.Tick,
|
||||
wrap = H.wrap;
|
||||
|
||||
/**
|
||||
* @optionparent xAxis
|
||||
*/
|
||||
var extendedOptions = {
|
||||
labels: {
|
||||
/**
|
||||
* Defines how the labels are be repositioned according to the 3D chart
|
||||
* orientation.
|
||||
*
|
||||
* - `'offset'`: Maintain a fixed horizontal/vertical distance from the
|
||||
* tick marks, despite the chart orientation. This is the backwards
|
||||
* compatible behavior, and causes skewing of X and Z axes.
|
||||
*
|
||||
* - `'chart'`: Preserve 3D position relative to the chart.
|
||||
* This looks nice, but hard to read if the text isn't
|
||||
* forward-facing.
|
||||
*
|
||||
* - `'flap'`: Rotated text along the axis to compensate for the chart
|
||||
* orientation. This tries to maintain text as legible as possible
|
||||
* on all orientations.
|
||||
*
|
||||
* - `'ortho'`: Rotated text along the axis direction so that the labels
|
||||
* are orthogonal to the axis. This is very similar to `'flap'`,
|
||||
* but prevents skewing the labels (X and Y scaling are still
|
||||
* present).
|
||||
*
|
||||
* @sample highcharts/3d/skewed-labels/
|
||||
* Skewed labels
|
||||
*
|
||||
* @since 5.0.15
|
||||
* @validvalue ['offset', 'chart', 'flap', 'ortho']
|
||||
* @product highcharts
|
||||
*/
|
||||
position3d: 'offset',
|
||||
|
||||
/**
|
||||
* If enabled, the axis labels will skewed to follow the perspective.
|
||||
*
|
||||
* This will fix overlapping labels and titles, but texts become less
|
||||
* legible due to the distortion.
|
||||
*
|
||||
* The final appearance depends heavily on `labels.position3d`.
|
||||
*
|
||||
* @sample highcharts/3d/skewed-labels/
|
||||
* Skewed labels
|
||||
*
|
||||
* @since 5.0.15
|
||||
* @product highcharts
|
||||
*/
|
||||
skew3d: false
|
||||
},
|
||||
title: {
|
||||
/**
|
||||
* Defines how the title is repositioned according to the 3D chart
|
||||
* orientation.
|
||||
*
|
||||
* - `'offset'`: Maintain a fixed horizontal/vertical distance from the
|
||||
* tick marks, despite the chart orientation. This is the backwards
|
||||
* compatible behavior, and causes skewing of X and Z axes.
|
||||
*
|
||||
* - `'chart'`: Preserve 3D position relative to the chart.
|
||||
* This looks nice, but hard to read if the text isn't
|
||||
* forward-facing.
|
||||
*
|
||||
* - `'flap'`: Rotated text along the axis to compensate for the chart
|
||||
* orientation. This tries to maintain text as legible as possible on
|
||||
* all orientations.
|
||||
*
|
||||
* - `'ortho'`: Rotated text along the axis direction so that the labels
|
||||
* are orthogonal to the axis. This is very similar to `'flap'`, but
|
||||
* prevents skewing the labels (X and Y scaling are still present).
|
||||
*
|
||||
* - `undefined`: Will use the config from `labels.position3d`
|
||||
*
|
||||
* @sample highcharts/3d/skewed-labels/
|
||||
* Skewed labels
|
||||
*
|
||||
* @type {"offset"|"chart"|"flap"|"ortho"|null}
|
||||
* @since 5.0.15
|
||||
* @product highcharts
|
||||
*/
|
||||
position3d: null,
|
||||
|
||||
/**
|
||||
* If enabled, the axis title will skewed to follow the perspective.
|
||||
*
|
||||
* This will fix overlapping labels and titles, but texts become less
|
||||
* legible due to the distortion.
|
||||
*
|
||||
* The final appearance depends heavily on `title.position3d`.
|
||||
*
|
||||
* A `null` value will use the config from `labels.skew3d`.
|
||||
*
|
||||
* @sample highcharts/3d/skewed-labels/
|
||||
* Skewed labels
|
||||
*
|
||||
* @type {boolean|null}
|
||||
* @since 5.0.15
|
||||
* @product highcharts
|
||||
*/
|
||||
skew3d: null
|
||||
}
|
||||
};
|
||||
|
||||
merge(true, Axis.prototype.defaultOptions, extendedOptions);
|
||||
|
||||
|
||||
addEvent(Axis, 'afterSetOptions', function () {
|
||||
var options;
|
||||
|
||||
if (this.chart.is3d && this.chart.is3d() && this.coll !== 'colorAxis') {
|
||||
options = this.options;
|
||||
options.tickWidth = pick(options.tickWidth, 0);
|
||||
options.gridLineWidth = pick(options.gridLineWidth, 1);
|
||||
}
|
||||
});
|
||||
|
||||
wrap(Axis.prototype, 'getPlotLinePath', function (proceed) {
|
||||
var path = proceed.apply(this, [].slice.call(arguments, 1));
|
||||
|
||||
// Do not do this if the chart is not 3D
|
||||
if (!this.chart.is3d() || this.coll === 'colorAxis') {
|
||||
return path;
|
||||
}
|
||||
|
||||
if (path === null) {
|
||||
return path;
|
||||
}
|
||||
|
||||
var chart = this.chart,
|
||||
options3d = chart.options.chart.options3d,
|
||||
d = this.isZAxis ? chart.plotWidth : options3d.depth,
|
||||
frame = chart.frame3d;
|
||||
|
||||
var pArr = [
|
||||
this.swapZ({ x: path[1], y: path[2], z: 0 }),
|
||||
this.swapZ({ x: path[1], y: path[2], z: d }),
|
||||
this.swapZ({ x: path[4], y: path[5], z: 0 }),
|
||||
this.swapZ({ x: path[4], y: path[5], z: d })
|
||||
];
|
||||
|
||||
var pathSegments = [];
|
||||
|
||||
if (!this.horiz) { // Y-Axis
|
||||
if (frame.front.visible) {
|
||||
pathSegments.push(pArr[0], pArr[2]);
|
||||
}
|
||||
if (frame.back.visible) {
|
||||
pathSegments.push(pArr[1], pArr[3]);
|
||||
}
|
||||
if (frame.left.visible) {
|
||||
pathSegments.push(pArr[0], pArr[1]);
|
||||
}
|
||||
if (frame.right.visible) {
|
||||
pathSegments.push(pArr[2], pArr[3]);
|
||||
}
|
||||
} else if (this.isZAxis) { // Z-Axis
|
||||
if (frame.left.visible) {
|
||||
pathSegments.push(pArr[0], pArr[2]);
|
||||
}
|
||||
if (frame.right.visible) {
|
||||
pathSegments.push(pArr[1], pArr[3]);
|
||||
}
|
||||
if (frame.top.visible) {
|
||||
pathSegments.push(pArr[0], pArr[1]);
|
||||
}
|
||||
if (frame.bottom.visible) {
|
||||
pathSegments.push(pArr[2], pArr[3]);
|
||||
}
|
||||
} else { // X-Axis
|
||||
if (frame.front.visible) {
|
||||
pathSegments.push(pArr[0], pArr[2]);
|
||||
}
|
||||
if (frame.back.visible) {
|
||||
pathSegments.push(pArr[1], pArr[3]);
|
||||
}
|
||||
if (frame.top.visible) {
|
||||
pathSegments.push(pArr[0], pArr[1]);
|
||||
}
|
||||
if (frame.bottom.visible) {
|
||||
pathSegments.push(pArr[2], pArr[3]);
|
||||
}
|
||||
}
|
||||
|
||||
pathSegments = perspective(pathSegments, this.chart, false);
|
||||
|
||||
return this.chart.renderer.toLineSegments(pathSegments);
|
||||
});
|
||||
|
||||
// Do not draw axislines in 3D
|
||||
wrap(Axis.prototype, 'getLinePath', function (proceed) {
|
||||
// Do not do this if the chart is not 3D
|
||||
if (!this.chart.is3d() || this.coll === 'colorAxis') {
|
||||
return proceed.apply(this, [].slice.call(arguments, 1));
|
||||
}
|
||||
|
||||
return [];
|
||||
});
|
||||
|
||||
wrap(Axis.prototype, 'getPlotBandPath', function (proceed) {
|
||||
// Do not do this if the chart is not 3D
|
||||
if (!this.chart.is3d() || this.coll === 'colorAxis') {
|
||||
return proceed.apply(this, [].slice.call(arguments, 1));
|
||||
}
|
||||
|
||||
var args = arguments,
|
||||
from = args[1],
|
||||
to = args[2],
|
||||
path = [],
|
||||
fromPath = this.getPlotLinePath(from),
|
||||
toPath = this.getPlotLinePath(to);
|
||||
|
||||
if (fromPath && toPath) {
|
||||
for (var i = 0; i < fromPath.length; i += 6) {
|
||||
path.push(
|
||||
'M', fromPath[i + 1], fromPath[i + 2],
|
||||
'L', fromPath[i + 4], fromPath[i + 5],
|
||||
'L', toPath[i + 4], toPath[i + 5],
|
||||
'L', toPath[i + 1], toPath[i + 2],
|
||||
'Z'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return path;
|
||||
});
|
||||
|
||||
|
||||
function fix3dPosition(axis, pos, isTitle) {
|
||||
// Do not do this if the chart is not 3D
|
||||
if (!axis.chart.is3d() || axis.coll === 'colorAxis') {
|
||||
return pos;
|
||||
}
|
||||
|
||||
var chart = axis.chart,
|
||||
alpha = deg2rad * chart.options.chart.options3d.alpha,
|
||||
beta = deg2rad * chart.options.chart.options3d.beta,
|
||||
positionMode = pick(
|
||||
isTitle && axis.options.title.position3d,
|
||||
axis.options.labels.position3d
|
||||
),
|
||||
skew = pick(
|
||||
isTitle && axis.options.title.skew3d,
|
||||
axis.options.labels.skew3d
|
||||
),
|
||||
frame = chart.frame3d,
|
||||
plotLeft = chart.plotLeft,
|
||||
plotRight = chart.plotWidth + plotLeft,
|
||||
plotTop = chart.plotTop,
|
||||
plotBottom = chart.plotHeight + plotTop,
|
||||
// Indicates we are labelling an X or Z axis on the "back" of the chart
|
||||
reverseFlap = false,
|
||||
offsetX = 0,
|
||||
offsetY = 0,
|
||||
vecX,
|
||||
vecY = { x: 0, y: 1, z: 0 };
|
||||
|
||||
pos = axis.swapZ({ x: pos.x, y: pos.y, z: 0 });
|
||||
|
||||
|
||||
if (axis.isZAxis) { // Z Axis
|
||||
if (axis.opposite) {
|
||||
if (frame.axes.z.top === null) {
|
||||
return {};
|
||||
}
|
||||
offsetY = pos.y - plotTop;
|
||||
pos.x = frame.axes.z.top.x;
|
||||
pos.y = frame.axes.z.top.y;
|
||||
vecX = frame.axes.z.top.xDir;
|
||||
reverseFlap = !frame.top.frontFacing;
|
||||
} else {
|
||||
if (frame.axes.z.bottom === null) {
|
||||
return {};
|
||||
}
|
||||
offsetY = pos.y - plotBottom;
|
||||
pos.x = frame.axes.z.bottom.x;
|
||||
pos.y = frame.axes.z.bottom.y;
|
||||
vecX = frame.axes.z.bottom.xDir;
|
||||
reverseFlap = !frame.bottom.frontFacing;
|
||||
}
|
||||
} else if (axis.horiz) { // X Axis
|
||||
if (axis.opposite) {
|
||||
if (frame.axes.x.top === null) {
|
||||
return {};
|
||||
}
|
||||
offsetY = pos.y - plotTop;
|
||||
pos.y = frame.axes.x.top.y;
|
||||
pos.z = frame.axes.x.top.z;
|
||||
vecX = frame.axes.x.top.xDir;
|
||||
reverseFlap = !frame.top.frontFacing;
|
||||
} else {
|
||||
if (frame.axes.x.bottom === null) {
|
||||
return {};
|
||||
}
|
||||
offsetY = pos.y - plotBottom;
|
||||
pos.y = frame.axes.x.bottom.y;
|
||||
pos.z = frame.axes.x.bottom.z;
|
||||
vecX = frame.axes.x.bottom.xDir;
|
||||
reverseFlap = !frame.bottom.frontFacing;
|
||||
}
|
||||
} else { // Y Axis
|
||||
if (axis.opposite) {
|
||||
if (frame.axes.y.right === null) {
|
||||
return {};
|
||||
}
|
||||
offsetX = pos.x - plotRight;
|
||||
pos.x = frame.axes.y.right.x;
|
||||
pos.z = frame.axes.y.right.z;
|
||||
vecX = frame.axes.y.right.xDir;
|
||||
// Rotate 90º on opposite edge
|
||||
vecX = { x: vecX.z, y: vecX.y, z: -vecX.x };
|
||||
} else {
|
||||
if (frame.axes.y.left === null) {
|
||||
return {};
|
||||
}
|
||||
offsetX = pos.x - plotLeft;
|
||||
pos.x = frame.axes.y.left.x;
|
||||
pos.z = frame.axes.y.left.z;
|
||||
vecX = frame.axes.y.left.xDir;
|
||||
}
|
||||
}
|
||||
|
||||
if (positionMode === 'chart') {
|
||||
// Labels preserve their direction relative to the chart
|
||||
// nothing to do
|
||||
|
||||
} else if (positionMode === 'flap') {
|
||||
// Labels are be rotated around the axis direction to face the screen
|
||||
if (!axis.horiz) { // Y Axis
|
||||
vecX = { x: Math.cos(beta), y: 0, z: Math.sin(beta) };
|
||||
} else { // X and Z Axis
|
||||
var sin = Math.sin(alpha);
|
||||
var cos = Math.cos(alpha);
|
||||
|
||||
if (axis.opposite) {
|
||||
sin = -sin;
|
||||
}
|
||||
if (reverseFlap) {
|
||||
sin = -sin;
|
||||
}
|
||||
vecY = { x: vecX.z * sin, y: cos, z: -vecX.x * sin };
|
||||
}
|
||||
} else if (positionMode === 'ortho') {
|
||||
// Labels will be rotated to be ortogonal to the axis
|
||||
if (!axis.horiz) { // Y Axis
|
||||
vecX = { x: Math.cos(beta), y: 0, z: Math.sin(beta) };
|
||||
} else { // X and Z Axis
|
||||
var sina = Math.sin(alpha);
|
||||
var cosa = Math.cos(alpha);
|
||||
var sinb = Math.sin(beta);
|
||||
var cosb = Math.cos(beta);
|
||||
var vecZ = { x: sinb * cosa, y: -sina, z: -cosa * cosb };
|
||||
|
||||
vecY = {
|
||||
x: vecX.y * vecZ.z - vecX.z * vecZ.y,
|
||||
y: vecX.z * vecZ.x - vecX.x * vecZ.z,
|
||||
z: vecX.x * vecZ.y - vecX.y * vecZ.x
|
||||
};
|
||||
var scale = 1 / Math.sqrt(
|
||||
vecY.x * vecY.x + vecY.y * vecY.y + vecY.z * vecY.z
|
||||
);
|
||||
|
||||
if (reverseFlap) {
|
||||
scale = -scale;
|
||||
}
|
||||
vecY = { x: scale * vecY.x, y: scale * vecY.y, z: scale * vecY.z };
|
||||
}
|
||||
} else { // positionMode == 'offset'
|
||||
// Labels will be skewd to maintain vertical / horizontal offsets from
|
||||
// axis
|
||||
if (!axis.horiz) { // Y Axis
|
||||
vecX = { x: Math.cos(beta), y: 0, z: Math.sin(beta) };
|
||||
} else { // X and Z Axis
|
||||
vecY = {
|
||||
x: Math.sin(beta) * Math.sin(alpha),
|
||||
y: Math.cos(alpha),
|
||||
z: -Math.cos(beta) * Math.sin(alpha)
|
||||
};
|
||||
}
|
||||
}
|
||||
pos.x += offsetX * vecX.x + offsetY * vecY.x;
|
||||
pos.y += offsetX * vecX.y + offsetY * vecY.y;
|
||||
pos.z += offsetX * vecX.z + offsetY * vecY.z;
|
||||
|
||||
var projected = perspective([pos], axis.chart)[0];
|
||||
|
||||
if (skew) {
|
||||
// Check if the label text would be mirrored
|
||||
var isMirrored = shapeArea(perspective([
|
||||
pos,
|
||||
{ x: pos.x + vecX.x, y: pos.y + vecX.y, z: pos.z + vecX.z },
|
||||
{ x: pos.x + vecY.x, y: pos.y + vecY.y, z: pos.z + vecY.z }
|
||||
], axis.chart)) < 0;
|
||||
|
||||
if (isMirrored) {
|
||||
vecX = { x: -vecX.x, y: -vecX.y, z: -vecX.z };
|
||||
}
|
||||
|
||||
var pointsProjected = perspective([
|
||||
{ x: pos.x, y: pos.y, z: pos.z },
|
||||
{ x: pos.x + vecX.x, y: pos.y + vecX.y, z: pos.z + vecX.z },
|
||||
{ x: pos.x + vecY.x, y: pos.y + vecY.y, z: pos.z + vecY.z }
|
||||
], axis.chart);
|
||||
|
||||
projected.matrix = [
|
||||
pointsProjected[1].x - pointsProjected[0].x,
|
||||
pointsProjected[1].y - pointsProjected[0].y,
|
||||
pointsProjected[2].x - pointsProjected[0].x,
|
||||
pointsProjected[2].y - pointsProjected[0].y,
|
||||
projected.x,
|
||||
projected.y
|
||||
];
|
||||
projected.matrix[4] -= projected.x * projected.matrix[0] +
|
||||
projected.y * projected.matrix[2];
|
||||
projected.matrix[5] -= projected.x * projected.matrix[1] +
|
||||
projected.y * projected.matrix[3];
|
||||
}
|
||||
|
||||
return projected;
|
||||
}
|
||||
|
||||
/*
|
||||
Tick extensions
|
||||
*/
|
||||
|
||||
wrap(Tick.prototype, 'getMarkPath', function (proceed) {
|
||||
var path = proceed.apply(this, [].slice.call(arguments, 1));
|
||||
|
||||
var pArr = [
|
||||
fix3dPosition(this.axis, { x: path[1], y: path[2], z: 0 }),
|
||||
fix3dPosition(this.axis, { x: path[4], y: path[5], z: 0 })
|
||||
];
|
||||
|
||||
return this.axis.chart.renderer.toLineSegments(pArr);
|
||||
});
|
||||
|
||||
addEvent(Tick, 'afterGetLabelPosition', function (e) {
|
||||
extend(e.pos, fix3dPosition(this.axis, e.pos));
|
||||
});
|
||||
|
||||
wrap(Axis.prototype, 'getTitlePosition', function (proceed) {
|
||||
var pos = proceed.apply(this, [].slice.call(arguments, 1));
|
||||
|
||||
return fix3dPosition(this, pos, true);
|
||||
});
|
||||
|
||||
addEvent(Axis, 'drawCrosshair', function (e) {
|
||||
if (this.chart.is3d() && this.coll !== 'colorAxis') {
|
||||
if (e.point) {
|
||||
e.point.crosshairPos = this.isXAxis ?
|
||||
e.point.axisXpos :
|
||||
this.len - (e.point.axisYpos);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
addEvent(Axis, 'destroy', function () {
|
||||
['backFrame', 'bottomFrame', 'sideFrame'].forEach(function (prop) {
|
||||
if (this[prop]) {
|
||||
this[prop] = this[prop].destroy();
|
||||
}
|
||||
}, this);
|
||||
});
|
||||
|
||||
/*
|
||||
Z-AXIS
|
||||
*/
|
||||
|
||||
Axis.prototype.swapZ = function (p, insidePlotArea) {
|
||||
if (this.isZAxis) {
|
||||
var plotLeft = insidePlotArea ? 0 : this.chart.plotLeft;
|
||||
|
||||
return {
|
||||
x: plotLeft + p.z,
|
||||
y: p.y,
|
||||
z: p.x - plotLeft
|
||||
};
|
||||
}
|
||||
return p;
|
||||
};
|
||||
|
||||
ZAxis = H.ZAxis = function () {
|
||||
this.init.apply(this, arguments);
|
||||
};
|
||||
extend(ZAxis.prototype, Axis.prototype);
|
||||
extend(ZAxis.prototype, {
|
||||
isZAxis: true,
|
||||
setOptions: function (userOptions) {
|
||||
userOptions = merge({
|
||||
offset: 0,
|
||||
lineWidth: 0
|
||||
}, userOptions);
|
||||
Axis.prototype.setOptions.call(this, userOptions);
|
||||
this.coll = 'zAxis';
|
||||
},
|
||||
setAxisSize: function () {
|
||||
Axis.prototype.setAxisSize.call(this);
|
||||
this.width = this.len = this.chart.options.chart.options3d.depth;
|
||||
this.right = this.chart.chartWidth - this.width - this.left;
|
||||
},
|
||||
getSeriesExtremes: function () {
|
||||
var axis = this,
|
||||
chart = axis.chart;
|
||||
|
||||
axis.hasVisibleSeries = false;
|
||||
|
||||
// Reset properties in case we're redrawing (#3353)
|
||||
axis.dataMin =
|
||||
axis.dataMax =
|
||||
axis.ignoreMinPadding =
|
||||
axis.ignoreMaxPadding = null;
|
||||
|
||||
if (axis.buildStacks) {
|
||||
axis.buildStacks();
|
||||
}
|
||||
|
||||
// loop through this axis' series
|
||||
axis.series.forEach(function (series) {
|
||||
|
||||
if (series.visible || !chart.options.chart.ignoreHiddenSeries) {
|
||||
|
||||
var seriesOptions = series.options,
|
||||
zData,
|
||||
threshold = seriesOptions.threshold;
|
||||
|
||||
axis.hasVisibleSeries = true;
|
||||
|
||||
// Validate threshold in logarithmic axes
|
||||
if (axis.positiveValuesOnly && threshold <= 0) {
|
||||
threshold = null;
|
||||
}
|
||||
|
||||
zData = series.zData;
|
||||
if (zData.length) {
|
||||
axis.dataMin = Math.min(
|
||||
pick(axis.dataMin, zData[0]),
|
||||
Math.min.apply(null, zData)
|
||||
);
|
||||
axis.dataMax = Math.max(
|
||||
pick(axis.dataMax, zData[0]),
|
||||
Math.max.apply(null, zData)
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// Get the Z axis in addition to the default X and Y.
|
||||
addEvent(Chart, 'afterGetAxes', function () {
|
||||
var chart = this,
|
||||
options = this.options,
|
||||
zAxisOptions = options.zAxis = splat(options.zAxis || {});
|
||||
|
||||
if (!chart.is3d()) {
|
||||
return;
|
||||
}
|
||||
this.zAxis = [];
|
||||
zAxisOptions.forEach(function (axisOptions, i) {
|
||||
axisOptions.index = i;
|
||||
// Z-Axis is shown horizontally, so it's kind of a X-Axis
|
||||
axisOptions.isX = true;
|
||||
var zAxis = new ZAxis(chart, axisOptions);
|
||||
|
||||
zAxis.setScale();
|
||||
});
|
||||
});
|
||||
|
||||
// Wrap getSlotWidth function to calculate individual width value for each slot
|
||||
// (#8042).
|
||||
wrap(Axis.prototype, 'getSlotWidth', function (proceed, tick) {
|
||||
if (this.chart.is3d() &&
|
||||
tick &&
|
||||
tick.label &&
|
||||
this.categories &&
|
||||
this.chart.frameShapes
|
||||
) {
|
||||
var chart = this.chart,
|
||||
ticks = this.ticks,
|
||||
gridGroup = this.gridGroup.element.childNodes,
|
||||
firstGridLine = gridGroup[0].getBBox(),
|
||||
frame3DLeft = chart.frameShapes.left.getBBox(),
|
||||
options3d = chart.options.chart.options3d,
|
||||
origin = {
|
||||
x: chart.plotWidth / 2,
|
||||
y: chart.plotHeight / 2,
|
||||
z: options3d.depth / 2,
|
||||
vd: pick(options3d.depth, 1) * pick(options3d.viewDistance, 0)
|
||||
},
|
||||
labelPos,
|
||||
prevLabelPos,
|
||||
nextLabelPos,
|
||||
slotWidth,
|
||||
tickId = tick.pos,
|
||||
prevTick = ticks[tickId - 1],
|
||||
nextTick = ticks[tickId + 1];
|
||||
|
||||
// Check whether the tick is not the first one and previous tick exists,
|
||||
// then calculate position of previous label.
|
||||
if (tickId !== 0 && prevTick && prevTick.label.xy) { // #8621
|
||||
prevLabelPos = perspective3D({
|
||||
x: prevTick.label.xy.x,
|
||||
y: prevTick.label.xy.y,
|
||||
z: null
|
||||
}, origin, origin.vd);
|
||||
}
|
||||
// If next label position is defined, then recalculate its position
|
||||
// basing on the perspective.
|
||||
if (nextTick && nextTick.label.xy) {
|
||||
nextLabelPos = perspective3D({
|
||||
x: nextTick.label.xy.x,
|
||||
y: nextTick.label.xy.y,
|
||||
z: null
|
||||
}, origin, origin.vd);
|
||||
}
|
||||
labelPos = {
|
||||
x: tick.label.xy.x,
|
||||
y: tick.label.xy.y,
|
||||
z: null
|
||||
};
|
||||
|
||||
labelPos = perspective3D(labelPos, origin, origin.vd);
|
||||
|
||||
// If tick is first one, check whether next label position is already
|
||||
// calculated, then return difference between the first and the second
|
||||
// label. If there is no next label position calculated, return the
|
||||
// difference between the first grid line and left 3d frame.
|
||||
slotWidth = Math.abs(
|
||||
prevLabelPos ?
|
||||
labelPos.x - prevLabelPos.x : nextLabelPos ?
|
||||
nextLabelPos.x - labelPos.x :
|
||||
firstGridLine.x - frame3DLeft.x
|
||||
);
|
||||
return slotWidth;
|
||||
}
|
||||
return proceed.apply(this, [].slice.call(arguments, 1));
|
||||
});
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,437 @@
|
||||
/* *
|
||||
* (c) 2010-2019 Torstein Honsi
|
||||
*
|
||||
* License: www.highcharts.com/license
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import H from '../parts/Globals.js';
|
||||
import '../parts/Utilities.js';
|
||||
import '../parts/Series.js';
|
||||
|
||||
var addEvent = H.addEvent,
|
||||
perspective = H.perspective,
|
||||
pick = H.pick,
|
||||
Series = H.Series,
|
||||
seriesTypes = H.seriesTypes,
|
||||
svg = H.svg,
|
||||
wrap = H.wrap;
|
||||
|
||||
/**
|
||||
* Depth of the columns in a 3D column chart. Requires `highcharts-3d.js`.
|
||||
*
|
||||
* @type {number}
|
||||
* @default 25
|
||||
* @since 4.0
|
||||
* @product highcharts
|
||||
* @apioption plotOptions.column.depth
|
||||
*/
|
||||
|
||||
/**
|
||||
* 3D columns only. The color of the edges. Similar to `borderColor`,
|
||||
* except it defaults to the same color as the column.
|
||||
*
|
||||
* @type {Highcharts.ColorString}
|
||||
* @product highcharts
|
||||
* @apioption plotOptions.column.edgeColor
|
||||
*/
|
||||
|
||||
/**
|
||||
* 3D columns only. The width of the colored edges.
|
||||
*
|
||||
* @type {number}
|
||||
* @default 1
|
||||
* @product highcharts
|
||||
* @apioption plotOptions.column.edgeWidth
|
||||
*/
|
||||
|
||||
/**
|
||||
* The spacing between columns on the Z Axis in a 3D chart. Requires
|
||||
* `highcharts-3d.js`.
|
||||
*
|
||||
* @type {number}
|
||||
* @default 1
|
||||
* @since 4.0
|
||||
* @product highcharts
|
||||
* @apioption plotOptions.column.groupZPadding
|
||||
*/
|
||||
|
||||
wrap(seriesTypes.column.prototype, 'translate', function (proceed) {
|
||||
proceed.apply(this, [].slice.call(arguments, 1));
|
||||
|
||||
// Do not do this if the chart is not 3D
|
||||
if (this.chart.is3d()) {
|
||||
this.translate3dShapes();
|
||||
}
|
||||
});
|
||||
|
||||
// In 3D we need to pass point.outsidePlot option to the justifyDataLabel
|
||||
// method for disabling justifying dataLabels in columns outside plot
|
||||
wrap(H.Series.prototype, 'alignDataLabel', function (proceed) {
|
||||
arguments[3].outside3dPlot = arguments[1].outside3dPlot;
|
||||
proceed.apply(this, [].slice.call(arguments, 1));
|
||||
});
|
||||
|
||||
// Don't use justifyDataLabel when point is outsidePlot
|
||||
wrap(H.Series.prototype, 'justifyDataLabel', function (proceed) {
|
||||
return !(arguments[2].outside3dPlot) ?
|
||||
proceed.apply(this, [].slice.call(arguments, 1)) :
|
||||
false;
|
||||
});
|
||||
|
||||
seriesTypes.column.prototype.translate3dPoints = function () {};
|
||||
seriesTypes.column.prototype.translate3dShapes = function () {
|
||||
|
||||
var series = this,
|
||||
chart = series.chart,
|
||||
seriesOptions = series.options,
|
||||
depth = seriesOptions.depth || 25,
|
||||
stack = seriesOptions.stacking ?
|
||||
(seriesOptions.stack || 0) :
|
||||
series.index, // #4743
|
||||
z = stack * (depth + (seriesOptions.groupZPadding || 1)),
|
||||
borderCrisp = series.borderWidth % 2 ? 0.5 : 0;
|
||||
|
||||
if (chart.inverted && !series.yAxis.reversed) {
|
||||
borderCrisp *= -1;
|
||||
}
|
||||
|
||||
if (seriesOptions.grouping !== false) {
|
||||
z = 0;
|
||||
}
|
||||
|
||||
z += (seriesOptions.groupZPadding || 1);
|
||||
series.data.forEach(function (point) {
|
||||
// #7103 Reset outside3dPlot flag
|
||||
point.outside3dPlot = null;
|
||||
if (point.y !== null) {
|
||||
var shapeArgs = point.shapeArgs,
|
||||
tooltipPos = point.tooltipPos,
|
||||
// Array for final shapeArgs calculation.
|
||||
// We are checking two dimensions (x and y).
|
||||
dimensions = [['x', 'width'], ['y', 'height']],
|
||||
borderlessBase; // Crisped rects can have +/- 0.5 pixels offset.
|
||||
|
||||
// #3131 We need to check if column is inside plotArea.
|
||||
dimensions.forEach(function (d) {
|
||||
borderlessBase = shapeArgs[d[0]] - borderCrisp;
|
||||
if (borderlessBase < 0) {
|
||||
// If borderLessBase is smaller than 0, it is needed to set
|
||||
// its value to 0 or 0.5 depending on borderWidth
|
||||
// borderWidth may be even or odd.
|
||||
shapeArgs[d[1]] += shapeArgs[d[0]] + borderCrisp;
|
||||
shapeArgs[d[0]] = -borderCrisp;
|
||||
borderlessBase = 0;
|
||||
}
|
||||
if (
|
||||
(
|
||||
borderlessBase + shapeArgs[d[1]] >
|
||||
series[d[0] + 'Axis'].len
|
||||
) &&
|
||||
// Do not change height/width of column if 0 (#6708)
|
||||
shapeArgs[d[1]] !== 0
|
||||
) {
|
||||
shapeArgs[d[1]] =
|
||||
series[d[0] + 'Axis'].len - shapeArgs[d[0]];
|
||||
}
|
||||
if (
|
||||
// Do not remove columns with zero height/width.
|
||||
(shapeArgs[d[1]] !== 0) &&
|
||||
(
|
||||
shapeArgs[d[0]] >= series[d[0] + 'Axis'].len ||
|
||||
shapeArgs[d[0]] + shapeArgs[d[1]] <= borderCrisp
|
||||
)
|
||||
) {
|
||||
// Set args to 0 if column is outside the chart.
|
||||
for (var key in shapeArgs) {
|
||||
shapeArgs[key] = 0;
|
||||
}
|
||||
// #7103 outside3dPlot flag is set on Points which are
|
||||
// currently outside of plot.
|
||||
point.outside3dPlot = true;
|
||||
}
|
||||
});
|
||||
|
||||
// Change from 2d to 3d
|
||||
if (point.shapeType === 'rect') {
|
||||
point.shapeType = 'cuboid';
|
||||
}
|
||||
|
||||
shapeArgs.z = z;
|
||||
shapeArgs.depth = depth;
|
||||
shapeArgs.insidePlotArea = true;
|
||||
|
||||
// Translate the tooltip position in 3d space
|
||||
tooltipPos = perspective(
|
||||
[{ x: tooltipPos[0], y: tooltipPos[1], z: z }],
|
||||
chart,
|
||||
true
|
||||
)[0];
|
||||
point.tooltipPos = [tooltipPos.x, tooltipPos.y];
|
||||
}
|
||||
});
|
||||
// store for later use #4067
|
||||
series.z = z;
|
||||
};
|
||||
|
||||
wrap(seriesTypes.column.prototype, 'animate', function (proceed) {
|
||||
if (!this.chart.is3d()) {
|
||||
proceed.apply(this, [].slice.call(arguments, 1));
|
||||
} else {
|
||||
var args = arguments,
|
||||
init = args[1],
|
||||
yAxis = this.yAxis,
|
||||
series = this,
|
||||
reversed = this.yAxis.reversed;
|
||||
|
||||
if (svg) { // VML is too slow anyway
|
||||
if (init) {
|
||||
series.data.forEach(function (point) {
|
||||
if (point.y !== null) {
|
||||
point.height = point.shapeArgs.height;
|
||||
point.shapey = point.shapeArgs.y; // #2968
|
||||
point.shapeArgs.height = 1;
|
||||
if (!reversed) {
|
||||
if (point.stackY) {
|
||||
point.shapeArgs.y =
|
||||
point.plotY + yAxis.translate(point.stackY);
|
||||
} else {
|
||||
point.shapeArgs.y =
|
||||
point.plotY +
|
||||
(
|
||||
point.negative ?
|
||||
-point.height :
|
||||
point.height
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
} else { // run the animation
|
||||
series.data.forEach(function (point) {
|
||||
if (point.y !== null) {
|
||||
point.shapeArgs.height = point.height;
|
||||
point.shapeArgs.y = point.shapey; // #2968
|
||||
// null value do not have a graphic
|
||||
if (point.graphic) {
|
||||
point.graphic.animate(
|
||||
point.shapeArgs,
|
||||
series.options.animation
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// redraw datalabels to the correct position
|
||||
this.drawDataLabels();
|
||||
|
||||
// delete this function to allow it only once
|
||||
series.animate = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// In case of 3d columns there is no sense to add this columns to a specific
|
||||
// series group - if series is added to a group all columns will have the same
|
||||
// zIndex in comparison with different series.
|
||||
wrap(
|
||||
seriesTypes.column.prototype,
|
||||
'plotGroup',
|
||||
function (proceed, prop, name, visibility, zIndex, parent) {
|
||||
if (this.chart.is3d() && parent && !this[prop]) {
|
||||
if (!this.chart.columnGroup) {
|
||||
this.chart.columnGroup =
|
||||
this.chart.renderer.g('columnGroup').add(parent);
|
||||
}
|
||||
this[prop] = this.chart.columnGroup;
|
||||
this.chart.columnGroup.attr(this.getPlotBox());
|
||||
this[prop].survive = true;
|
||||
}
|
||||
return proceed.apply(this, Array.prototype.slice.call(arguments, 1));
|
||||
}
|
||||
);
|
||||
|
||||
// When series is not added to group it is needed to change setVisible method to
|
||||
// allow correct Legend funcionality. This wrap is basing on pie chart series.
|
||||
wrap(
|
||||
seriesTypes.column.prototype,
|
||||
'setVisible',
|
||||
function (proceed, vis) {
|
||||
var series = this,
|
||||
pointVis;
|
||||
|
||||
if (series.chart.is3d()) {
|
||||
series.data.forEach(function (point) {
|
||||
point.visible = point.options.visible = vis =
|
||||
vis === undefined ? !point.visible : vis;
|
||||
pointVis = vis ? 'visible' : 'hidden';
|
||||
series.options.data[series.data.indexOf(point)] =
|
||||
point.options;
|
||||
if (point.graphic) {
|
||||
point.graphic.attr({
|
||||
visibility: pointVis
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
proceed.apply(this, Array.prototype.slice.call(arguments, 1));
|
||||
}
|
||||
);
|
||||
|
||||
seriesTypes.column.prototype.handle3dGrouping = true;
|
||||
addEvent(Series, 'afterInit', function () {
|
||||
if (this.chart.is3d() && this.handle3dGrouping) {
|
||||
var seriesOptions = this.options,
|
||||
grouping = seriesOptions.grouping,
|
||||
stacking = seriesOptions.stacking,
|
||||
reversedStacks = pick(this.yAxis.options.reversedStacks, true),
|
||||
z = 0;
|
||||
|
||||
if (!(grouping !== undefined && !grouping)) {
|
||||
var stacks = this.chart.retrieveStacks(stacking),
|
||||
stack = seriesOptions.stack || 0,
|
||||
i; // position within the stack
|
||||
|
||||
for (i = 0; i < stacks[stack].series.length; i++) {
|
||||
if (stacks[stack].series[i] === this) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
z = (10 * (stacks.totalStacks - stacks[stack].position)) +
|
||||
(reversedStacks ? i : -i); // #4369
|
||||
|
||||
// In case when axis is reversed, columns are also reversed inside
|
||||
// the group (#3737)
|
||||
if (!this.xAxis.reversed) {
|
||||
z = (stacks.totalStacks * 10) - z;
|
||||
}
|
||||
}
|
||||
|
||||
seriesOptions.zIndex = z;
|
||||
}
|
||||
});
|
||||
|
||||
function pointAttribs(proceed) {
|
||||
var attr = proceed.apply(this, [].slice.call(arguments, 1));
|
||||
|
||||
if (this.chart.is3d && this.chart.is3d()) {
|
||||
// Set the fill color to the fill color to provide a smooth edge
|
||||
attr.stroke = this.options.edgeColor || attr.fill;
|
||||
attr['stroke-width'] = pick(this.options.edgeWidth, 1); // #4055
|
||||
}
|
||||
|
||||
return attr;
|
||||
}
|
||||
|
||||
wrap(seriesTypes.column.prototype, 'pointAttribs', pointAttribs);
|
||||
if (seriesTypes.columnrange) {
|
||||
wrap(seriesTypes.columnrange.prototype, 'pointAttribs', pointAttribs);
|
||||
seriesTypes.columnrange.prototype.plotGroup =
|
||||
seriesTypes.column.prototype.plotGroup;
|
||||
seriesTypes.columnrange.prototype.setVisible =
|
||||
seriesTypes.column.prototype.setVisible;
|
||||
}
|
||||
|
||||
wrap(Series.prototype, 'alignDataLabel', function (proceed) {
|
||||
|
||||
// Only do this for 3D columns and it's derived series
|
||||
if (
|
||||
this.chart.is3d() &&
|
||||
this instanceof seriesTypes.column
|
||||
) {
|
||||
var series = this,
|
||||
chart = series.chart;
|
||||
|
||||
var args = arguments,
|
||||
alignTo = args[4],
|
||||
point = args[1];
|
||||
|
||||
var pos = ({ x: alignTo.x, y: alignTo.y, z: series.z });
|
||||
|
||||
pos = perspective([pos], chart, true)[0];
|
||||
alignTo.x = pos.x;
|
||||
// #7103 If point is outside of plotArea, hide data label.
|
||||
alignTo.y = point.outside3dPlot ? -9e9 : pos.y;
|
||||
}
|
||||
|
||||
proceed.apply(this, [].slice.call(arguments, 1));
|
||||
});
|
||||
|
||||
// Added stackLabels position calculation for 3D charts.
|
||||
wrap(H.StackItem.prototype, 'getStackBox', function (proceed, chart) { // #3946
|
||||
var stackBox = proceed.apply(this, [].slice.call(arguments, 1));
|
||||
|
||||
// Only do this for 3D chart.
|
||||
if (chart.is3d()) {
|
||||
var pos = ({
|
||||
x: stackBox.x,
|
||||
y: stackBox.y,
|
||||
z: 0
|
||||
});
|
||||
|
||||
pos = H.perspective([pos], chart, true)[0];
|
||||
stackBox.x = pos.x;
|
||||
stackBox.y = pos.y;
|
||||
}
|
||||
|
||||
return stackBox;
|
||||
});
|
||||
|
||||
/*
|
||||
@merge v6.2
|
||||
@todo
|
||||
EXTENSION FOR 3D CYLINDRICAL COLUMNS
|
||||
Not supported
|
||||
*/
|
||||
/*
|
||||
var defaultOptions = H.getOptions();
|
||||
defaultOptions.plotOptions.cylinder =
|
||||
H.merge(defaultOptions.plotOptions.column);
|
||||
var CylinderSeries = H.extendClass(seriesTypes.column, {
|
||||
type: 'cylinder'
|
||||
});
|
||||
seriesTypes.cylinder = CylinderSeries;
|
||||
|
||||
wrap(seriesTypes.cylinder.prototype, 'translate', function (proceed) {
|
||||
proceed.apply(this, [].slice.call(arguments, 1));
|
||||
|
||||
// Do not do this if the chart is not 3D
|
||||
if (!this.chart.is3d()) {
|
||||
return;
|
||||
}
|
||||
|
||||
var series = this,
|
||||
chart = series.chart,
|
||||
options = chart.options,
|
||||
cylOptions = options.plotOptions.cylinder,
|
||||
options3d = options.chart.options3d,
|
||||
depth = cylOptions.depth || 0,
|
||||
alpha = chart.alpha3d;
|
||||
|
||||
var z = cylOptions.stacking ?
|
||||
(this.options.stack || 0) * depth :
|
||||
series._i * depth;
|
||||
z += depth / 2;
|
||||
|
||||
if (cylOptions.grouping !== false) { z = 0; }
|
||||
|
||||
each(series.data, function (point) {
|
||||
var shapeArgs = point.shapeArgs,
|
||||
deg2rad = H.deg2rad;
|
||||
point.shapeType = 'arc3d';
|
||||
shapeArgs.x += depth / 2;
|
||||
shapeArgs.z = z;
|
||||
shapeArgs.start = 0;
|
||||
shapeArgs.end = 2 * PI;
|
||||
shapeArgs.r = depth * 0.95;
|
||||
shapeArgs.innerR = 0;
|
||||
shapeArgs.depth =
|
||||
shapeArgs.height * (1 / sin((90 - alpha) * deg2rad)) - z;
|
||||
shapeArgs.alpha = 90 - alpha;
|
||||
shapeArgs.beta = 0;
|
||||
});
|
||||
});
|
||||
*/
|
||||
@@ -0,0 +1,206 @@
|
||||
/* *
|
||||
* (c) 2010-2019 Torstein Honsi
|
||||
*
|
||||
* License: www.highcharts.com/license
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import H from '../parts/Globals.js';
|
||||
import '../parts/Utilities.js';
|
||||
|
||||
// Mathematical Functionility
|
||||
var deg2rad = H.deg2rad,
|
||||
pick = H.pick;
|
||||
|
||||
/* eslint-disable max-len */
|
||||
/**
|
||||
* Apply 3-D rotation
|
||||
* Euler Angles (XYZ):
|
||||
* cosA = cos(Alfa|Roll)
|
||||
* cosB = cos(Beta|Pitch)
|
||||
* cosG = cos(Gamma|Yaw)
|
||||
*
|
||||
* Composite rotation:
|
||||
* | cosB * cosG | cosB * sinG | -sinB |
|
||||
* | sinA * sinB * cosG - cosA * sinG | sinA * sinB * sinG + cosA * cosG | sinA * cosB |
|
||||
* | cosA * sinB * cosG + sinA * sinG | cosA * sinB * sinG - sinA * cosG | cosA * cosB |
|
||||
*
|
||||
* Now, Gamma/Yaw is not used (angle=0), so we assume cosG = 1 and sinG = 0, so
|
||||
* we get:
|
||||
* | cosB | 0 | - sinB |
|
||||
* | sinA * sinB | cosA | sinA * cosB |
|
||||
* | cosA * sinB | - sinA | cosA * cosB |
|
||||
*
|
||||
* But in browsers, y is reversed, so we get sinA => -sinA. The general result
|
||||
* is:
|
||||
* | cosB | 0 | - sinB | | x | | px |
|
||||
* | - sinA * sinB | cosA | - sinA * cosB | x | y | = | py |
|
||||
* | cosA * sinB | sinA | cosA * cosB | | z | | pz |
|
||||
*
|
||||
* @private
|
||||
* @function rotate3D
|
||||
*/
|
||||
/* eslint-enable max-len */
|
||||
function rotate3D(x, y, z, angles) {
|
||||
return {
|
||||
x: angles.cosB * x - angles.sinB * z,
|
||||
y: -angles.sinA * angles.sinB * x + angles.cosA * y -
|
||||
angles.cosB * angles.sinA * z,
|
||||
z: angles.cosA * angles.sinB * x + angles.sinA * y +
|
||||
angles.cosA * angles.cosB * z
|
||||
};
|
||||
}
|
||||
|
||||
// Perspective3D function is available in global Highcharts scope because is
|
||||
// needed also outside of perspective() function (#8042).
|
||||
H.perspective3D = function (coordinate, origin, distance) {
|
||||
var projection = ((distance > 0) && (distance < Number.POSITIVE_INFINITY)) ?
|
||||
distance / (coordinate.z + origin.z + distance) :
|
||||
1;
|
||||
|
||||
return {
|
||||
x: coordinate.x * projection,
|
||||
y: coordinate.y * projection
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Transforms a given array of points according to the angles in chart.options.
|
||||
*
|
||||
* @private
|
||||
* @function Highcharts.perspective
|
||||
*
|
||||
* @param {Array<Highcharts.Point>} points
|
||||
* The array of points
|
||||
*
|
||||
* @param {Highcharts.Chart} chart
|
||||
* The chart
|
||||
*
|
||||
* @param {boolean} [insidePlotArea]
|
||||
* Wether to verifiy the points are inside the plotArea
|
||||
*
|
||||
* @return {Array<Highcharts.Point>}
|
||||
* An array of transformed points
|
||||
*/
|
||||
H.perspective = function (points, chart, insidePlotArea) {
|
||||
var options3d = chart.options.chart.options3d,
|
||||
inverted = insidePlotArea ? chart.inverted : false,
|
||||
origin = {
|
||||
x: chart.plotWidth / 2,
|
||||
y: chart.plotHeight / 2,
|
||||
z: options3d.depth / 2,
|
||||
vd: pick(options3d.depth, 1) * pick(options3d.viewDistance, 0)
|
||||
},
|
||||
scale = chart.scale3d || 1,
|
||||
beta = deg2rad * options3d.beta * (inverted ? -1 : 1),
|
||||
alpha = deg2rad * options3d.alpha * (inverted ? -1 : 1),
|
||||
angles = {
|
||||
cosA: Math.cos(alpha),
|
||||
cosB: Math.cos(-beta),
|
||||
sinA: Math.sin(alpha),
|
||||
sinB: Math.sin(-beta)
|
||||
};
|
||||
|
||||
if (!insidePlotArea) {
|
||||
origin.x += chart.plotLeft;
|
||||
origin.y += chart.plotTop;
|
||||
}
|
||||
|
||||
// Transform each point
|
||||
return points.map(function (point) {
|
||||
var rotated = rotate3D(
|
||||
(inverted ? point.y : point.x) - origin.x,
|
||||
(inverted ? point.x : point.y) - origin.y,
|
||||
(point.z || 0) - origin.z,
|
||||
angles
|
||||
),
|
||||
// Apply perspective
|
||||
coordinate = H.perspective3D(rotated, origin, origin.vd);
|
||||
|
||||
// Apply translation
|
||||
coordinate.x = coordinate.x * scale + origin.x;
|
||||
coordinate.y = coordinate.y * scale + origin.y;
|
||||
coordinate.z = rotated.z * scale + origin.z;
|
||||
|
||||
return {
|
||||
x: (inverted ? coordinate.y : coordinate.x),
|
||||
y: (inverted ? coordinate.x : coordinate.y),
|
||||
z: coordinate.z
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Calculate a distance from camera to points - made for calculating zIndex of
|
||||
* scatter points.
|
||||
*
|
||||
* @private
|
||||
* @function Highcharts.pointCameraDistance
|
||||
*
|
||||
* @param {object} coordinates
|
||||
* The coordinates of the specific point
|
||||
*
|
||||
* @param {Highcharts.Chart} chart
|
||||
* The chart
|
||||
*
|
||||
* @return {number}
|
||||
* A distance from camera to point
|
||||
*/
|
||||
H.pointCameraDistance = function (coordinates, chart) {
|
||||
var options3d = chart.options.chart.options3d,
|
||||
cameraPosition = {
|
||||
x: chart.plotWidth / 2,
|
||||
y: chart.plotHeight / 2,
|
||||
z: pick(options3d.depth, 1) * pick(options3d.viewDistance, 0) +
|
||||
options3d.depth
|
||||
},
|
||||
distance = Math.sqrt(
|
||||
Math.pow(cameraPosition.x - coordinates.plotX, 2) +
|
||||
Math.pow(cameraPosition.y - coordinates.plotY, 2) +
|
||||
Math.pow(cameraPosition.z - coordinates.plotZ, 2)
|
||||
);
|
||||
|
||||
return distance;
|
||||
};
|
||||
|
||||
/**
|
||||
* Calculate area of a 2D polygon using Shoelace algorithm
|
||||
* http://en.wikipedia.org/wiki/Shoelace_formula
|
||||
*
|
||||
* @private
|
||||
* @function Highcharts.shapeArea
|
||||
*
|
||||
* @param {Array<object>} vertexes
|
||||
*
|
||||
* @return {number}
|
||||
*/
|
||||
H.shapeArea = function (vertexes) {
|
||||
var area = 0,
|
||||
i,
|
||||
j;
|
||||
|
||||
for (i = 0; i < vertexes.length; i++) {
|
||||
j = (i + 1) % vertexes.length;
|
||||
area += vertexes[i].x * vertexes[j].y - vertexes[j].x * vertexes[i].y;
|
||||
}
|
||||
return area / 2;
|
||||
};
|
||||
|
||||
/**
|
||||
* Calculate area of a 3D polygon after perspective projection
|
||||
*
|
||||
* @private
|
||||
* @function Highcharts.shapeArea3d
|
||||
*
|
||||
* @param {Array<object>} vertexes
|
||||
*
|
||||
* @param {Highcharts.Chart} chart
|
||||
*
|
||||
* @param {boolean} [insidePlotArea]
|
||||
*
|
||||
* @return {number}
|
||||
*/
|
||||
H.shapeArea3d = function (vertexes, chart, insidePlotArea) {
|
||||
return H.shapeArea(H.perspective(vertexes, chart, insidePlotArea));
|
||||
};
|
||||
@@ -0,0 +1,207 @@
|
||||
/* *
|
||||
* (c) 2010-2019 Torstein Honsi
|
||||
*
|
||||
* 3D pie series
|
||||
*
|
||||
* License: www.highcharts.com/license
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import H from '../parts/Globals.js';
|
||||
import '../parts/Utilities.js';
|
||||
|
||||
var deg2rad = H.deg2rad,
|
||||
pick = H.pick,
|
||||
seriesTypes = H.seriesTypes,
|
||||
svg = H.svg,
|
||||
wrap = H.wrap;
|
||||
|
||||
/**
|
||||
* The thickness of a 3D pie. Requires `highcharts-3d.js`
|
||||
*
|
||||
* @type {number}
|
||||
* @default 0
|
||||
* @since 4.0
|
||||
* @product highcharts
|
||||
* @apioption plotOptions.pie.depth
|
||||
*/
|
||||
|
||||
wrap(seriesTypes.pie.prototype, 'translate', function (proceed) {
|
||||
proceed.apply(this, [].slice.call(arguments, 1));
|
||||
|
||||
// Do not do this if the chart is not 3D
|
||||
if (!this.chart.is3d()) {
|
||||
return;
|
||||
}
|
||||
|
||||
var series = this,
|
||||
seriesOptions = series.options,
|
||||
depth = seriesOptions.depth || 0,
|
||||
options3d = series.chart.options.chart.options3d,
|
||||
alpha = options3d.alpha,
|
||||
beta = options3d.beta,
|
||||
z = seriesOptions.stacking ?
|
||||
(seriesOptions.stack || 0) * depth :
|
||||
series._i * depth;
|
||||
|
||||
z += depth / 2;
|
||||
|
||||
if (seriesOptions.grouping !== false) {
|
||||
z = 0;
|
||||
}
|
||||
|
||||
series.data.forEach(function (point) {
|
||||
|
||||
var shapeArgs = point.shapeArgs,
|
||||
angle;
|
||||
|
||||
point.shapeType = 'arc3d';
|
||||
|
||||
shapeArgs.z = z;
|
||||
shapeArgs.depth = depth * 0.75;
|
||||
shapeArgs.alpha = alpha;
|
||||
shapeArgs.beta = beta;
|
||||
shapeArgs.center = series.center;
|
||||
|
||||
angle = (shapeArgs.end + shapeArgs.start) / 2;
|
||||
|
||||
point.slicedTranslation = {
|
||||
translateX: Math.round(
|
||||
Math.cos(angle) *
|
||||
seriesOptions.slicedOffset *
|
||||
Math.cos(alpha * deg2rad)
|
||||
),
|
||||
translateY: Math.round(
|
||||
Math.sin(angle) *
|
||||
seriesOptions.slicedOffset *
|
||||
Math.cos(alpha * deg2rad)
|
||||
)
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
wrap(
|
||||
seriesTypes.pie.prototype.pointClass.prototype,
|
||||
'haloPath',
|
||||
function (proceed) {
|
||||
var args = arguments;
|
||||
|
||||
return this.series.chart.is3d() ? [] : proceed.call(this, args[1]);
|
||||
}
|
||||
);
|
||||
|
||||
wrap(
|
||||
seriesTypes.pie.prototype,
|
||||
'pointAttribs',
|
||||
function (proceed, point, state) {
|
||||
var attr = proceed.call(this, point, state),
|
||||
options = this.options;
|
||||
|
||||
if (this.chart.is3d() && !this.chart.styledMode) {
|
||||
attr.stroke = options.edgeColor || point.color || this.color;
|
||||
attr['stroke-width'] = pick(options.edgeWidth, 1);
|
||||
}
|
||||
|
||||
return attr;
|
||||
}
|
||||
);
|
||||
|
||||
wrap(seriesTypes.pie.prototype, 'drawDataLabels', function (proceed) {
|
||||
if (this.chart.is3d()) {
|
||||
var series = this,
|
||||
chart = series.chart,
|
||||
options3d = chart.options.chart.options3d;
|
||||
|
||||
series.data.forEach(function (point) {
|
||||
var shapeArgs = point.shapeArgs,
|
||||
r = shapeArgs.r,
|
||||
// #3240 issue with datalabels for 0 and null values
|
||||
a1 = (shapeArgs.alpha || options3d.alpha) * deg2rad,
|
||||
b1 = (shapeArgs.beta || options3d.beta) * deg2rad,
|
||||
a2 = (shapeArgs.start + shapeArgs.end) / 2,
|
||||
labelPosition = point.labelPosition,
|
||||
connectorPosition = labelPosition.connectorPosition,
|
||||
yOffset = (-r * (1 - Math.cos(a1)) * Math.sin(a2)),
|
||||
xOffset = r * (Math.cos(b1) - 1) * Math.cos(a2);
|
||||
|
||||
// Apply perspective on label positions
|
||||
[
|
||||
labelPosition.natural,
|
||||
connectorPosition.breakAt,
|
||||
connectorPosition.touchingSliceAt
|
||||
].forEach(function (coordinates) {
|
||||
coordinates.x += xOffset;
|
||||
coordinates.y += yOffset;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
proceed.apply(this, [].slice.call(arguments, 1));
|
||||
});
|
||||
|
||||
wrap(seriesTypes.pie.prototype, 'addPoint', function (proceed) {
|
||||
proceed.apply(this, [].slice.call(arguments, 1));
|
||||
if (this.chart.is3d()) {
|
||||
// destroy (and rebuild) everything!!!
|
||||
this.update(this.userOptions, true); // #3845 pass the old options
|
||||
}
|
||||
});
|
||||
|
||||
wrap(seriesTypes.pie.prototype, 'animate', function (proceed) {
|
||||
if (!this.chart.is3d()) {
|
||||
proceed.apply(this, [].slice.call(arguments, 1));
|
||||
} else {
|
||||
var args = arguments,
|
||||
init = args[1],
|
||||
animation = this.options.animation,
|
||||
attribs,
|
||||
center = this.center,
|
||||
group = this.group,
|
||||
markerGroup = this.markerGroup;
|
||||
|
||||
if (svg) { // VML is too slow anyway
|
||||
|
||||
if (animation === true) {
|
||||
animation = {};
|
||||
}
|
||||
// Initialize the animation
|
||||
if (init) {
|
||||
|
||||
// Scale down the group and place it in the center
|
||||
group.oldtranslateX = group.translateX;
|
||||
group.oldtranslateY = group.translateY;
|
||||
attribs = {
|
||||
translateX: center[0],
|
||||
translateY: center[1],
|
||||
scaleX: 0.001, // #1499
|
||||
scaleY: 0.001
|
||||
};
|
||||
|
||||
group.attr(attribs);
|
||||
if (markerGroup) {
|
||||
markerGroup.attrSetters = group.attrSetters;
|
||||
markerGroup.attr(attribs);
|
||||
}
|
||||
|
||||
// Run the animation
|
||||
} else {
|
||||
attribs = {
|
||||
translateX: group.oldtranslateX,
|
||||
translateY: group.oldtranslateY,
|
||||
scaleX: 1,
|
||||
scaleY: 1
|
||||
};
|
||||
group.animate(attribs, animation);
|
||||
|
||||
if (markerGroup) {
|
||||
markerGroup.animate(attribs, animation);
|
||||
}
|
||||
|
||||
// Delete this function to allow it only once
|
||||
this.animate = null;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
});
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,154 @@
|
||||
/* *
|
||||
* (c) 2010-2019 Torstein Honsi
|
||||
*
|
||||
* Scatter 3D series.
|
||||
*
|
||||
* License: www.highcharts.com/license
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import H from '../parts/Globals.js';
|
||||
import '../parts/Utilities.js';
|
||||
|
||||
var Point = H.Point,
|
||||
seriesType = H.seriesType,
|
||||
seriesTypes = H.seriesTypes;
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @class
|
||||
* @name Highcharts.seriesTypes.scatter3d
|
||||
*
|
||||
* @augments Highcharts.Series
|
||||
*/
|
||||
seriesType(
|
||||
'scatter3d',
|
||||
'scatter',
|
||||
/**
|
||||
* A 3D scatter plot uses x, y and z coordinates to display values for three
|
||||
* variables for a set of data.
|
||||
*
|
||||
* @sample {highcharts} highcharts/3d/scatter/
|
||||
* Simple 3D scatter
|
||||
* @sample {highcharts} highcharts/demo/3d-scatter-draggable
|
||||
* Draggable 3d scatter
|
||||
*
|
||||
* @extends plotOptions.scatter
|
||||
* @product highcharts
|
||||
* @optionparent plotOptions.scatter3d
|
||||
*/
|
||||
{
|
||||
tooltip: {
|
||||
pointFormat: 'x: <b>{point.x}</b><br/>y: <b>{point.y}</b><br/>z: <b>{point.z}</b><br/>'
|
||||
}
|
||||
|
||||
// Series class
|
||||
}, {
|
||||
pointAttribs: function (point) {
|
||||
var attribs = seriesTypes.scatter.prototype.pointAttribs
|
||||
.apply(this, arguments);
|
||||
|
||||
if (this.chart.is3d() && point) {
|
||||
attribs.zIndex = H.pointCameraDistance(point, this.chart);
|
||||
}
|
||||
|
||||
return attribs;
|
||||
},
|
||||
axisTypes: ['xAxis', 'yAxis', 'zAxis'],
|
||||
pointArrayMap: ['x', 'y', 'z'],
|
||||
parallelArrays: ['x', 'y', 'z'],
|
||||
|
||||
// Require direct touch rather than using the k-d-tree, because the
|
||||
// k-d-tree currently doesn't take the xyz coordinate system into
|
||||
// account (#4552)
|
||||
directTouch: true
|
||||
|
||||
// Point class
|
||||
}, {
|
||||
applyOptions: function () {
|
||||
Point.prototype.applyOptions.apply(this, arguments);
|
||||
if (this.z === undefined) {
|
||||
this.z = 0;
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
/**
|
||||
* A `scatter3d` series. If the [type](#series.scatter3d.type) option is
|
||||
* not specified, it is inherited from [chart.type](#chart.type).
|
||||
*
|
||||
* scatter3d](#plotOptions.scatter3d).
|
||||
*
|
||||
* @extends series,plotOptions.scatter3d
|
||||
* @product highcharts
|
||||
* @apioption series.scatter3d
|
||||
*/
|
||||
|
||||
/**
|
||||
* An array of data points for the series. For the `scatter3d` series
|
||||
* type, points can be given in the following ways:
|
||||
*
|
||||
* 1. An array of arrays with 3 values. In this case, the values correspond
|
||||
* to `x,y,z`. If the first value is a string, it is applied as the name
|
||||
* of the point, and the `x` value is inferred.
|
||||
*
|
||||
* ```js
|
||||
* data: [
|
||||
* [0, 0, 1],
|
||||
* [1, 8, 7],
|
||||
* [2, 9, 2]
|
||||
* ]
|
||||
* ```
|
||||
*
|
||||
* 3. An array of objects with named values. The following snippet shows only a
|
||||
* few settings, see the complete options set below. If the total number of data
|
||||
* points exceeds the series'
|
||||
* [turboThreshold](#series.scatter3d.turboThreshold), this option is not
|
||||
* available.
|
||||
*
|
||||
* ```js
|
||||
* data: [{
|
||||
* x: 1,
|
||||
* y: 2,
|
||||
* z: 24,
|
||||
* name: "Point2",
|
||||
* color: "#00FF00"
|
||||
* }, {
|
||||
* x: 1,
|
||||
* y: 4,
|
||||
* z: 12,
|
||||
* name: "Point1",
|
||||
* color: "#FF00FF"
|
||||
* }]
|
||||
* ```
|
||||
*
|
||||
* @sample {highcharts} highcharts/chart/reflow-true/
|
||||
* Numerical values
|
||||
* @sample {highcharts} highcharts/series/data-array-of-arrays/
|
||||
* Arrays of numeric x and y
|
||||
* @sample {highcharts} highcharts/series/data-array-of-arrays-datetime/
|
||||
* Arrays of datetime x and y
|
||||
* @sample {highcharts} highcharts/series/data-array-of-name-value/
|
||||
* Arrays of point.name and y
|
||||
* @sample {highcharts} highcharts/series/data-array-of-objects/
|
||||
* Config objects
|
||||
*
|
||||
* @type {Array<Array<number>|*>}
|
||||
* @extends series.scatter.data
|
||||
* @product highcharts
|
||||
* @apioption series.scatter3d.data
|
||||
*/
|
||||
|
||||
/**
|
||||
* The z value for each data point.
|
||||
*
|
||||
* @type {number}
|
||||
* @product highcharts
|
||||
* @apioption series.scatter3d.data.z
|
||||
*/
|
||||
@@ -0,0 +1,73 @@
|
||||
/* *
|
||||
* (c) 2010-2019 Torstein Honsi
|
||||
*
|
||||
* Extension to the Series object in 3D charts.
|
||||
*
|
||||
* License: www.highcharts.com/license
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import H from '../parts/Globals.js';
|
||||
import '../parts/Utilities.js';
|
||||
|
||||
var addEvent = H.addEvent,
|
||||
perspective = H.perspective,
|
||||
pick = H.pick;
|
||||
|
||||
// Wrap the translate method to post-translate points into 3D perspective
|
||||
addEvent(H.Series, 'afterTranslate', function () {
|
||||
if (this.chart.is3d()) {
|
||||
this.translate3dPoints();
|
||||
}
|
||||
});
|
||||
|
||||
// Translate the plotX, plotY properties and add plotZ.
|
||||
H.Series.prototype.translate3dPoints = function () {
|
||||
var series = this,
|
||||
chart = series.chart,
|
||||
zAxis = pick(series.zAxis, chart.options.zAxis[0]),
|
||||
rawPoints = [],
|
||||
rawPoint,
|
||||
projectedPoints,
|
||||
projectedPoint,
|
||||
zValue,
|
||||
i;
|
||||
|
||||
for (i = 0; i < series.data.length; i++) {
|
||||
rawPoint = series.data[i];
|
||||
|
||||
if (zAxis && zAxis.translate) {
|
||||
zValue = zAxis.isLog && zAxis.val2lin ?
|
||||
zAxis.val2lin(rawPoint.z) :
|
||||
rawPoint.z; // #4562
|
||||
rawPoint.plotZ = zAxis.translate(zValue);
|
||||
rawPoint.isInside = rawPoint.isInside ?
|
||||
(zValue >= zAxis.min && zValue <= zAxis.max) :
|
||||
false;
|
||||
} else {
|
||||
rawPoint.plotZ = 0;
|
||||
}
|
||||
|
||||
rawPoint.axisXpos = rawPoint.plotX;
|
||||
rawPoint.axisYpos = rawPoint.plotY;
|
||||
rawPoint.axisZpos = rawPoint.plotZ;
|
||||
|
||||
rawPoints.push({
|
||||
x: rawPoint.plotX,
|
||||
y: rawPoint.plotY,
|
||||
z: rawPoint.plotZ
|
||||
});
|
||||
}
|
||||
|
||||
projectedPoints = perspective(rawPoints, chart, true);
|
||||
|
||||
for (i = 0; i < series.data.length; i++) {
|
||||
rawPoint = series.data[i];
|
||||
projectedPoint = projectedPoints[i];
|
||||
|
||||
rawPoint.plotX = projectedPoint.x;
|
||||
rawPoint.plotY = projectedPoint.y;
|
||||
rawPoint.plotZ = projectedPoint.z;
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,62 @@
|
||||
/* *
|
||||
* (c) 2010-2019 Torstein Honsi
|
||||
*
|
||||
* Extension to the VML Renderer
|
||||
*
|
||||
* License: www.highcharts.com/license
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import H from '../parts/Globals.js';
|
||||
import '../parts/Utilities.js';
|
||||
import '../parts/Axis.js';
|
||||
import '../parts/SvgRenderer.js';
|
||||
|
||||
var addEvent = H.addEvent,
|
||||
Axis = H.Axis,
|
||||
SVGRenderer = H.SVGRenderer,
|
||||
VMLRenderer = H.VMLRenderer;
|
||||
|
||||
if (VMLRenderer) {
|
||||
|
||||
H.setOptions({ animate: false });
|
||||
|
||||
VMLRenderer.prototype.face3d = SVGRenderer.prototype.face3d;
|
||||
VMLRenderer.prototype.polyhedron = SVGRenderer.prototype.polyhedron;
|
||||
|
||||
VMLRenderer.prototype.elements3d = SVGRenderer.prototype.elements3d;
|
||||
VMLRenderer.prototype.element3d = SVGRenderer.prototype.element3d;
|
||||
VMLRenderer.prototype.cuboid = SVGRenderer.prototype.cuboid;
|
||||
VMLRenderer.prototype.cuboidPath = SVGRenderer.prototype.cuboidPath;
|
||||
|
||||
VMLRenderer.prototype.toLinePath = SVGRenderer.prototype.toLinePath;
|
||||
VMLRenderer.prototype.toLineSegments = SVGRenderer.prototype.toLineSegments;
|
||||
|
||||
VMLRenderer.prototype.arc3d = function (shapeArgs) {
|
||||
var result = SVGRenderer.prototype.arc3d.call(this, shapeArgs);
|
||||
|
||||
result.css({ zIndex: result.zIndex });
|
||||
return result;
|
||||
};
|
||||
|
||||
H.VMLRenderer.prototype.arc3dPath = H.SVGRenderer.prototype.arc3dPath;
|
||||
|
||||
addEvent(Axis, 'render', function () {
|
||||
|
||||
// VML doesn't support a negative z-index
|
||||
if (this.sideFrame) {
|
||||
this.sideFrame.css({ zIndex: 0 });
|
||||
this.sideFrame.front.attr({ fill: this.sideFrame.color });
|
||||
}
|
||||
if (this.bottomFrame) {
|
||||
this.bottomFrame.css({ zIndex: 1 });
|
||||
this.bottomFrame.front.attr({ fill: this.bottomFrame.color });
|
||||
}
|
||||
if (this.backFrame) {
|
||||
this.backFrame.css({ zIndex: 0 });
|
||||
this.backFrame.front.attr({ fill: this.backFrame.color });
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user