123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669 |
- /* *
- * (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));
- });
|