/* * * * (c) 2010-2020 Torstein Honsi * * Extenstion for 3d axes * * License: www.highcharts.com/license * * !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!! * * */ 'use strict'; import H from '../parts/Globals.js'; import Tick from '../parts/Tick.js'; import Tick3D from './Tick3D.js'; import U from '../parts/Utilities.js'; var addEvent = U.addEvent, merge = U.merge, pick = U.pick, wrap = U.wrap; var deg2rad = H.deg2rad, perspective = H.perspective, perspective3D = H.perspective3D, shapeArea = H.shapeArea; /* eslint-disable valid-jsdoc */ /** * Adds 3D support to axes. * @private * @class */ var Axis3DAdditions = /** @class */ (function () { /* * * * Constructors * * */ /** * @private */ function Axis3DAdditions(axis) { this.axis = axis; } /* * * * Functions * * */ /** * @private * @param {Highcharts.Axis} axis * Related axis. * @param {Highcharts.Position3dObject} pos * Position to fix. * @param {boolean} [isTitle] * Whether this is a title position. * @return {Highcharts.Position3dObject} * Fixed position. */ Axis3DAdditions.prototype.fix3dPosition = function (pos, isTitle) { var axis3D = this; var axis = axis3D.axis; var chart = axis.chart; // Do not do this if the chart is not 3D if (axis.coll === 'colorAxis' || !chart.is3d()) { return pos; } var 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 that 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.axis3D.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 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; }; /** * @private */ Axis3DAdditions.prototype.swapZ = function (p, insidePlotArea) { var axis = this.axis; if (axis.isZAxis) { var plotLeft = insidePlotArea ? 0 : axis.chart.plotLeft; return { x: plotLeft + p.z, y: p.y, z: p.x - plotLeft }; } return p; }; return Axis3DAdditions; }()); /** * Axis with 3D support. * @private * @class */ var Axis3D = /** @class */ (function () { function Axis3D() { } /* * * * Static Functions * * */ /** * Extends axis class with 3D support. * @private */ Axis3D.compose = function (AxisClass) { merge(true, AxisClass.defaultOptions, Axis3D.defaultOptions); AxisClass.keepProps.push('axis3D'); addEvent(AxisClass, 'init', Axis3D.onInit); addEvent(AxisClass, 'afterSetOptions', Axis3D.onAfterSetOptions); addEvent(AxisClass, 'drawCrosshair', Axis3D.onDrawCrosshair); addEvent(AxisClass, 'destroy', Axis3D.onDestroy); var axisProto = AxisClass.prototype; wrap(axisProto, 'getLinePath', Axis3D.wrapGetLinePath); wrap(axisProto, 'getPlotBandPath', Axis3D.wrapGetPlotBandPath); wrap(axisProto, 'getPlotLinePath', Axis3D.wrapGetPlotLinePath); wrap(axisProto, 'getSlotWidth', Axis3D.wrapGetSlotWidth); wrap(axisProto, 'getTitlePosition', Axis3D.wrapGetTitlePosition); Tick3D.compose(Tick); }; /** * @private */ Axis3D.onAfterSetOptions = function () { var axis = this; var chart = axis.chart; var options = axis.options; if (chart.is3d && chart.is3d() && axis.coll !== 'colorAxis') { options.tickWidth = pick(options.tickWidth, 0); options.gridLineWidth = pick(options.gridLineWidth, 1); } }; /** * @private */ Axis3D.onDestroy = function () { ['backFrame', 'bottomFrame', 'sideFrame'].forEach(function (prop) { if (this[prop]) { this[prop] = this[prop].destroy(); } }, this); }; /** * @private */ Axis3D.onDrawCrosshair = function (e) { var axis = this; if (axis.chart.is3d() && axis.coll !== 'colorAxis') { if (e.point) { e.point.crosshairPos = axis.isXAxis ? e.point.axisXpos : axis.len - e.point.axisYpos; } } }; /** * @private */ Axis3D.onInit = function () { var axis = this; if (!axis.axis3D) { axis.axis3D = new Axis3DAdditions(axis); } }; /** * Do not draw axislines in 3D. * @private */ Axis3D.wrapGetLinePath = function (proceed) { var axis = this; // Do not do this if the chart is not 3D if (!axis.chart.is3d() || axis.coll === 'colorAxis') { return proceed.apply(axis, [].slice.call(arguments, 1)); } return []; }; /** * @private */ Axis3D.wrapGetPlotBandPath = 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({ value: from }), toPath = this.getPlotLinePath({ value: to }); if (fromPath && toPath) { for (var i = 0; i < fromPath.length; i += 2) { var fromStartSeg = fromPath[i], fromEndSeg = fromPath[i + 1], toStartSeg = toPath[i], toEndSeg = toPath[i + 1]; if (fromStartSeg[0] === 'M' && fromEndSeg[0] === 'L' && toStartSeg[0] === 'M' && toEndSeg[0] === 'L') { path.push(fromStartSeg, fromEndSeg, toEndSeg, // lineTo instead of moveTo ['L', toStartSeg[1], toStartSeg[2]], ['Z']); } } } return path; }; /** * @private */ Axis3D.wrapGetPlotLinePath = function (proceed) { var axis = this; var axis3D = axis.axis3D; var chart = axis.chart; var path = proceed.apply(axis, [].slice.call(arguments, 1)); // Do not do this if the chart is not 3D if (!chart.is3d() || axis.coll === 'colorAxis') { return path; } if (path === null) { return path; } var options3d = chart.options.chart.options3d, d = axis.isZAxis ? chart.plotWidth : options3d.depth, frame = chart.frame3d, startSegment = path[0], endSegment = path[1], pArr, pathSegments = []; if (startSegment[0] === 'M' && endSegment[0] === 'L') { pArr = [ axis3D.swapZ({ x: startSegment[1], y: startSegment[2], z: 0 }), axis3D.swapZ({ x: startSegment[1], y: startSegment[2], z: d }), axis3D.swapZ({ x: endSegment[1], y: endSegment[2], z: 0 }), axis3D.swapZ({ x: endSegment[1], y: endSegment[2], z: d }) ]; 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 chart.renderer.toLineSegments(pathSegments); }; /** * Wrap getSlotWidth function to calculate individual width value for each * slot (#8042). * @private */ Axis3D.wrapGetSlotWidth = function (proceed, tick) { var axis = this; var chart = axis.chart; var ticks = axis.ticks; var gridGroup = axis.gridGroup; if (axis.categories && chart.frameShapes && chart.is3d() && gridGroup && tick && tick.label) { var firstGridLine = gridGroup.element.childNodes[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) { 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(axis, [].slice.call(arguments, 1)); }; /** * @private */ Axis3D.wrapGetTitlePosition = function (proceed) { var pos = proceed.apply(this, [].slice.call(arguments, 1)); return this.axis3D ? this.axis3D.fix3dPosition(pos, true) : pos; }; /* * * * Static Properties * * */ /** * @optionparent xAxis */ Axis3D.defaultOptions = { 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 * @requires highcharts-3d */ 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 * @requires highcharts-3d */ 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 * @requires highcharts-3d */ 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 * @requires highcharts-3d */ skew3d: null } }; return Axis3D; }()); export default Axis3D;