initial commit

This commit is contained in:
2021-07-10 16:25:16 +02:00
parent bff57cbf45
commit 20fb7267a8
2426 changed files with 792090 additions and 0 deletions
@@ -0,0 +1,198 @@
/* *
* (c) 2017 Highsoft AS
* Authors: Lars A. V. Cabrera
*
* License: www.highcharts.com/license
*/
'use strict';
import H from '../parts/Globals.js';
/**
* Creates an arrow symbol. Like a triangle, except not filled.
* ```
* o
* o
* o
* o
* o
* o
* o
* ```
*
* @private
* @function
*
* @param {number} x
* x position of the arrow
*
* @param {number} y
* y position of the arrow
*
* @param {number} w
* width of the arrow
*
* @param {number} h
* height of the arrow
*
* @return {Highcharts.SVGPathArray}
* Path array
*/
H.SVGRenderer.prototype.symbols.arrow = function (x, y, w, h) {
return [
'M', x, y + h / 2,
'L', x + w, y,
'L', x, y + h / 2,
'L', x + w, y + h
];
};
/**
* Creates a half-width arrow symbol. Like a triangle, except not filled.
* ```
* o
* o
* o
* o
* o
* ```
*
* @private
* @function
*
* @param {number} x
* x position of the arrow
*
* @param {number} y
* y position of the arrow
*
* @param {number} w
* width of the arrow
*
* @param {number} h
* height of the arrow
*
* @return {Highcharts.SVGPathArray}
* Path array
*/
H.SVGRenderer.prototype.symbols['arrow-half'] = function (x, y, w, h) {
return H.SVGRenderer.prototype.symbols.arrow(x, y, w / 2, h);
};
/**
* Creates a left-oriented triangle.
* ```
* o
* ooooooo
* ooooooooooooo
* ooooooo
* o
* ```
*
* @private
* @function
*
* @param {number} x
* x position of the triangle
*
* @param {number} y
* y position of the triangle
*
* @param {number} w
* width of the triangle
*
* @param {number} h
* height of the triangle
*
* @return {Highcharts.SVGPathArray}
* Path array
*/
H.SVGRenderer.prototype.symbols['triangle-left'] = function (x, y, w, h) {
return [
'M', x + w, y,
'L', x, y + h / 2,
'L', x + w, y + h,
'Z'
];
};
/**
* Alias function for triangle-left.
*
* @private
* @function
*
* @param {number} x
* x position of the arrow
*
* @param {number} y
* y position of the arrow
*
* @param {number} w
* width of the arrow
*
* @param {number} h
* height of the arrow
*
* @return {Highcharts.SVGPathArray}
* Path array
*/
H.SVGRenderer.prototype.symbols['arrow-filled'] =
H.SVGRenderer.prototype.symbols['triangle-left'];
/**
* Creates a half-width, left-oriented triangle.
* ```
* o
* oooo
* ooooooo
* oooo
* o
* ```
*
* @private
* @function
*
* @param {number} x
* x position of the triangle
*
* @param {number} y
* y position of the triangle
*
* @param {number} w
* width of the triangle
*
* @param {number} h
* height of the triangle
*
* @return {Highcharts.SVGPathArray}
* Path array
*/
H.SVGRenderer.prototype.symbols['triangle-left-half'] = function (x, y, w, h) {
return H.SVGRenderer.prototype.symbols['triangle-left'](x, y, w / 2, h);
};
/**
* Alias function for triangle-left-half.
*
* @private
* @function
*
* @param {number} x
* x position of the arrow
*
* @param {number} y
* y position of the arrow
*
* @param {number} w
* width of the arrow
*
* @param {number} h
* height of the arrow
*
* @return {Highcharts.SVGPathArray}
* Path array
*/
H.SVGRenderer.prototype.symbols['arrow-filled-half'] =
H.SVGRenderer.prototype.symbols['triangle-left-half'];
@@ -0,0 +1,100 @@
/* *
*
* (c) 2016-2019 Highsoft AS
*
* Author: Lars A. V. Cabrera
*
* License: www.highcharts.com/license
*
* */
'use strict';
import H from '../parts/Globals.js';
var addEvent = H.addEvent,
Axis = H.Axis,
PlotLineOrBand = H.PlotLineOrBand,
merge = H.merge;
var defaultConfig = {
/**
* Show an indicator on the axis for the current date and time. Can be a
* boolean or a configuration object similar to
* [xAxis.plotLines](#xAxis.plotLines).
*
* @sample gantt/current-date-indicator/demo
* Current date indicator enabled
* @sample gantt/current-date-indicator/object-config
* Current date indicator with custom options
*
* @type {boolean|*}
* @default true
* @extends xAxis.plotLines
* @excluding value
* @product gantt
* @apioption xAxis.currentDateIndicator
*/
currentDateIndicator: true,
color: '#ccd6eb',
width: 2,
label: {
format: '%a, %b %d %Y, %H:%M',
formatter: undefined,
rotation: 0,
style: {
fontSize: '10px'
}
}
};
addEvent(Axis, 'afterSetOptions', function () {
var options = this.options,
cdiOptions = options.currentDateIndicator;
if (cdiOptions) {
if (typeof cdiOptions === 'object') {
// Ignore formatter if custom format is defined
if (cdiOptions.label && cdiOptions.label.format) {
cdiOptions.label.formatter = undefined;
}
cdiOptions = merge(defaultConfig, cdiOptions);
} else {
cdiOptions = merge(defaultConfig);
}
cdiOptions.value = new Date();
if (!options.plotLines) {
options.plotLines = [];
}
options.plotLines.push(cdiOptions);
}
});
addEvent(PlotLineOrBand, 'render', function () {
var options = this.options,
format,
formatter;
if (options.currentDateIndicator && options.label) {
format = options.label.format;
formatter = options.label.formatter;
options.value = new Date();
if (typeof formatter === 'function') {
options.label.text = formatter(this);
} else {
options.label.text = H.dateFormat(format, new Date());
}
// If the label already exists, update its text
if (this.label) {
this.label.attr({
text: options.label.text
});
}
}
});
@@ -0,0 +1,140 @@
/* *
* (c) 2016 Highsoft AS
* Authors: Lars A. V. Cabrera
*
* License: www.highcharts.com/license
*/
'use strict';
import H from '../parts/Globals.js';
import 'GanttSeries.js';
var merge = H.merge,
splat = H.splat,
Chart = H.Chart;
/**
* Factory function for Gantt charts.
*
* @example
* // Render a chart in to div#container
* var chart = Highcharts.ganttChart('container', {
* title: {
* text: 'My chart'
* },
* series: [{
* data: ...
* }]
* });
*
* @function Highcharts.ganttChart
*
* @param {string|Highcharts.HTMLDOMElement} [renderTo]
* The DOM element to render to, or its id.
*
* @param {Highcharts.Options} options
* The chart options structure.
*
* @param {Highcharts.ChartCallbackFunction} [callback]
* Function to run when the chart has loaded and and all external images
* are loaded. Defining a
* [chart.events.load](https://api.highcharts.com/highcharts/chart.events.load)
* handler is equivalent.
*
* @return {Highcharts.Chart}
* Returns the Chart object.
*/
H.ganttChart = function (renderTo, options, callback) {
var hasRenderToArg = typeof renderTo === 'string' || renderTo.nodeName,
seriesOptions = options.series,
defaultOptions = H.getOptions(),
defaultLinkedTo,
userOptions = options;
options = arguments[hasRenderToArg ? 1 : 0];
// If user hasn't defined axes as array, make it into an array and add a
// second axis by default.
if (!H.isArray(options.xAxis)) {
options.xAxis = [options.xAxis || {}, {}];
}
// apply X axis options to both single and multi x axes
options.xAxis = options.xAxis.map(function (xAxisOptions, i) {
if (i === 1) { // Second xAxis
defaultLinkedTo = 0;
}
return merge(
defaultOptions.xAxis,
{ // defaults
grid: {
enabled: true
},
opposite: true,
linkedTo: defaultLinkedTo
},
xAxisOptions, // user options
{ // forced options
type: 'datetime'
}
);
});
// apply Y axis options to both single and multi y axes
options.yAxis = (splat(options.yAxis || {})).map(function (yAxisOptions) {
return merge(
defaultOptions.yAxis, // #3802
{ // defaults
grid: {
enabled: true
},
staticScale: 50,
reversed: true,
// Set default type treegrid, but only if 'categories' is
// undefined
type: yAxisOptions.categories ? yAxisOptions.type : 'treegrid'
},
yAxisOptions // user options
);
});
options.series = null;
options = merge(
true,
{
chart: {
type: 'gantt'
},
title: {
text: null
},
legend: {
enabled: false
}
},
options, // user's options
// forced options
{
isGantt: true
}
);
options.series = userOptions.series = seriesOptions;
options.series.forEach(function (series) {
series.data.forEach(function (point) {
H.seriesTypes.gantt.prototype.setGanttPointAliases(point);
});
});
return hasRenderToArg ?
new Chart(renderTo, options, callback) :
new Chart(options, options);
};
@@ -0,0 +1,415 @@
/* *
*
* (c) 2016-2019 Highsoft AS
*
* Author: Lars A. V. Cabrera
*
* License: www.highcharts.com/license
*
* */
'use strict';
import H from '../parts/Globals.js';
import 'CurrentDateIndicator.js';
import 'GridAxis.js';
import '../modules/static-scale.src.js';
import 'TreeGrid.js';
import 'Pathfinder.js';
import '../modules/xrange.src.js';
var dateFormat = H.dateFormat,
isObject = H.isObject,
isNumber = H.isNumber,
merge = H.merge,
pick = H.pick,
seriesType = H.seriesType,
seriesTypes = H.seriesTypes,
stop = H.stop,
Series = H.Series,
parent = seriesTypes.xrange;
/**
* @private
* @class
* @name Highcharts.seriesTypes.gantt
*
* @augments Highcharts.Series
*/
seriesType('gantt', 'xrange'
/**
* A `gantt` series. If the [type](#series.gantt.type) option is not specified,
* it is inherited from [chart.type](#chart.type).
*
* @extends plotOptions.xrange
* @product gantt
* @optionparent plotOptions.gantt
*/
, {
// options - default options merged with parent
grouping: false,
dataLabels: {
enabled: true,
formatter: function () {
var point = this,
amount = point.point.partialFill;
if (isObject(amount)) {
amount = amount.amount;
}
if (isNumber(amount) && amount > 0) {
return (amount * 100) + '%';
}
}
},
tooltip: {
headerFormat: '<span style="font-size: 10px">{series.name}</span><br/>',
pointFormat: null,
pointFormatter: function () {
var point = this,
series = point.series,
tooltip = series.chart.tooltip,
xAxis = series.xAxis,
options = xAxis.options,
formats = options.dateTimeLabelFormats,
startOfWeek = xAxis.options.startOfWeek,
ttOptions = series.tooltipOptions,
format = ttOptions.xDateFormat,
range = point.end ? point.end - point.start : 0,
start,
end,
milestone = point.options.milestone,
retVal = '<b>' + (point.name || point.yCategory) + '</b>';
if (ttOptions.pointFormat) {
return point.tooltipFormatter(ttOptions.pointFormat);
}
if (!format) {
format = H.splat(
tooltip.getDateFormat(
range,
point.start,
startOfWeek,
formats
)
)[0];
}
start = dateFormat(format, point.start);
end = dateFormat(format, point.end);
retVal += '<br/>';
if (!milestone) {
retVal += 'Start: ' + start + '<br/>';
retVal += 'End: ' + end + '<br/>';
} else {
retVal += 'Date ' + start + '<br/>';
}
return retVal;
}
},
connectors: {
type: 'simpleConnect',
animation: {
reversed: true // Dependencies go from child to parent
},
startMarker: {
enabled: true,
symbol: 'arrow-filled',
radius: 4,
fill: '#fa0',
align: 'left'
},
endMarker: {
enabled: false, // Only show arrow on the dependent task
align: 'right'
}
}
}, {
// props - series member overrides
pointArrayMap: ['start', 'end', 'y'],
// Keyboard navigation, don't use nearest vertical mode
keyboardMoveVertical: false,
// Handle milestones, as they have no x2
translatePoint: function (point) {
var series = this,
shapeArgs,
size;
parent.prototype.translatePoint.call(series, point);
if (point.options.milestone) {
shapeArgs = point.shapeArgs;
size = shapeArgs.height;
point.shapeArgs = {
x: shapeArgs.x - (size / 2),
y: shapeArgs.y,
width: size,
height: size
};
}
},
/**
* Draws a single point in the series.
*
* This override draws the point as a diamond if point.options.milestone is
* true, and uses the original drawPoint() if it is false or not set.
*
* @requires module:highcharts-gantt
*
* @private
* @function Highcharts.seriesTypes.gantt#drawPoint
*
* @param {Highcharts.Point} point
* An instance of Point in the series
*
* @param {"animate"|"attr"} verb
* 'animate' (animates changes) or 'attr' (sets options)
*/
drawPoint: function (point, verb) {
var series = this,
seriesOpts = series.options,
renderer = series.chart.renderer,
shapeArgs = point.shapeArgs,
plotY = point.plotY,
graphic = point.graphic,
state = point.selected && 'select',
cutOff = seriesOpts.stacking && !seriesOpts.borderRadius,
diamondShape;
if (point.options.milestone) {
if (isNumber(plotY) && point.y !== null) {
diamondShape = renderer.symbols.diamond(
shapeArgs.x,
shapeArgs.y,
shapeArgs.width,
shapeArgs.height
);
if (graphic) {
stop(graphic);
graphic[verb]({
d: diamondShape
});
} else {
point.graphic = graphic = renderer.path(diamondShape)
.addClass(point.getClassName(), true)
.add(point.group || series.group);
}
// Presentational
if (!series.chart.styledMode) {
point.graphic
.attr(series.pointAttribs(point, state))
.shadow(seriesOpts.shadow, null, cutOff);
}
} else if (graphic) {
point.graphic = graphic.destroy(); // #1269
}
} else {
parent.prototype.drawPoint.call(series, point, verb);
}
},
setData: Series.prototype.setData,
setGanttPointAliases: function (options) {
// Add a value to options if the value exists
function addIfExists(prop, val) {
if (val !== undefined) {
options[prop] = val;
}
}
addIfExists('x', pick(options.start, options.x));
addIfExists('x2', pick(options.end, options.x2));
addIfExists(
'partialFill', pick(options.completed, options.partialFill)
);
addIfExists('connect', pick(options.dependency, options.connect));
}
}, merge(parent.prototype.pointClass.prototype, {
// pointProps - point member overrides. We inherit from parent as well.
/**
* Applies the options containing the x and y data and possible some extra
* properties. This is called on point init or from point.update.
*
* @private
* @function Highcharts.Point#applyOptions
*
* @param {object} options
* The point options
*
* @param {number} x
* The x value
*
* @return {Highcharts.Point}
* The Point instance
*/
applyOptions: function (options, x) {
var point = this,
retVal = merge(options);
H.seriesTypes.gantt.prototype.setGanttPointAliases(retVal);
retVal = parent.prototype.pointClass.prototype.applyOptions
.call(point, retVal, x);
return retVal;
}
}));
/**
* A `gantt` series.
*
* @extends series,plotOptions.gantt
* @excluding boostThreshold, connectors, dashStyle, findNearestPointBy,
* getExtremesFromAll, marker, negativeColor, pointInterval,
* pointIntervalUnit, pointPlacement, pointStart
* @product gantt
* @apioption series.gantt
*/
/**
* Data for a Gantt series.
*
* @type {Array<*>}
* @extends series.xrange.data
* @excluding className, color, colorIndex, connect, dataLabels, events, id,
* partialFill, selected, x, x2
* @product gantt
* @apioption series.gantt.data
*/
/**
* Whether the grid node belonging to this point should start as collapsed. Used
* in axes of type treegrid.
*
* @sample {gantt} gantt/treegrid-axis/collapsed/
* Start as collapsed
*
* @type {boolean}
* @default false
* @product gantt
* @apioption series.gantt.data.collapsed
*/
/**
* The start time of a task.
*
* @type {number}
* @product gantt
* @apioption series.gantt.data.start
*/
/**
* The end time of a task.
*
* @type {number}
* @product gantt
* @apioption series.gantt.data.end
*/
/**
* The Y value of a task.
*
* @type {number}
* @product gantt
* @apioption series.gantt.data.y
*/
/**
* The name of a task. If a `treegrid` y-axis is used (default in Gantt charts),
* this will be picked up automatically, and used to calculate the y-value.
*
* @type {string}
* @product gantt
* @apioption series.gantt.data.name
*/
/**
* Progress indicator, how much of the task completed. If it is a number, the
* `fill` will be applied automatically.
*
* @sample {gantt} gantt/demo/progress-indicator
* Progress indicator
*
* @type {number|*}
* @extends series.xrange.data.partialFill
* @product gantt
* @apioption series.gantt.data.completed
*/
/**
* The amount of the progress indicator, ranging from 0 (not started) to 1
* (finished).
*
* @type {number}
* @default 0
* @apioption series.gantt.data.completed.amount
*/
/**
* The fill of the progress indicator. Defaults to a darkened variety of the
* main color.
*
* @type {Highcharts.ColorString|Highcharts.GradientColorObject|Highcharts.PatternObject}
* @apioption series.gantt.data.completed.fill
*/
/**
* The ID of the point (task) that this point depends on in Gantt charts.
* Aliases [connect](series.xrange.data.connect). Can also be an object,
* specifying further connecting [options](series.gantt.connectors) between the
* points. Multiple connections can be specified by providing an array.
*
* @sample gantt/demo/project-management
* Dependencies
* @sample gantt/pathfinder/demo
* Different connection types
*
* @type {string|Array<string|*>|*}
* @extends series.xrange.data.connect
* @since 6.2.0
* @product gantt
* @apioption series.gantt.data.dependency
*/
/**
* Whether this point is a milestone. If so, only the `start` option is handled,
* while `end` is ignored.
*
* @sample gantt/gantt/milestones
* Milestones
*
* @type {boolean}
* @since 6.2.0
* @product gantt
* @apioption series.gantt.data.milestone
*/
/**
* The ID of the parent point (task) of this point in Gantt charts.
*
* @sample gantt/demo/subtasks
* Gantt chart with subtasks
*
* @type {string}
* @since 6.2.0
* @product gantt
* @apioption series.gantt.data.parent
*/
/**
* @excluding afterAnimate
* @apioption series.gantt.events
*/
@@ -0,0 +1,961 @@
/* *
* (c) 2016 Highsoft AS
* Authors: Lars A. V. Cabrera
*
* License: www.highcharts.com/license
*/
'use strict';
import H from '../parts/Globals.js';
var addEvent = H.addEvent,
argsToArray = function (args) {
return Array.prototype.slice.call(args, 1);
},
dateFormat = H.dateFormat,
defined = H.defined,
isArray = H.isArray,
isNumber = H.isNumber,
isObject = function (x) {
// Always use strict mode
return H.isObject(x, true);
},
merge = H.merge,
pick = H.pick,
wrap = H.wrap,
Axis = H.Axis,
Tick = H.Tick;
/**
* Set grid options for the axis labels. Requires Highcharts Gantt.
*
* @since 6.2.0
* @product gantt
* @apioption xAxis.grid
*/
/**
* Enable grid on the axis labels. Defaults to true for Gantt charts.
*
* @type {boolean}
* @default true
* @since 6.2.0
* @product gantt
* @apioption xAxis.grid.enabled
*/
/**
* Set specific options for each column (or row for horizontal axes) in the
* grid. Each extra column/row is its own axis, and the axis options can be set
* here.
*
* @sample gantt/demo/left-axis-table
* Left axis as a table
*
* @type {Array<Highcharts.XAxisOptions>}
* @apioption xAxis.grid.columns
*/
/**
* Set border color for the label grid lines.
*
* @type {Highcharts.ColorString}
* @apioption xAxis.grid.borderColor
*/
/**
* Set border width of the label grid lines.
*
* @type {number}
* @default 1
* @apioption xAxis.grid.borderWidth
*/
/**
* Set cell height for grid axis labels. By default this is calculated from font
* size.
*
* @type {number}
* @apioption xAxis.grid.cellHeight
*/
// Enum for which side the axis is on.
// Maps to axis.side
var axisSide = {
top: 0,
right: 1,
bottom: 2,
left: 3,
0: 'top',
1: 'right',
2: 'bottom',
3: 'left'
};
/**
* Checks if an axis is a navigator axis.
*
* @private
* @function Highcharts.Axis#isNavigatorAxis
*
* @return {boolean}
* true if axis is found in axis.chart.navigator
*/
Axis.prototype.isNavigatorAxis = function () {
return /highcharts-navigator-[xy]axis/.test(this.options.className);
};
/**
* Checks if an axis is the outer axis in its dimension. Since
* axes are placed outwards in order, the axis with the highest
* index is the outermost axis.
*
* Example: If there are multiple x-axes at the top of the chart,
* this function returns true if the axis supplied is the last
* of the x-axes.
*
* @private
* @function Highcharts.Axis#isOuterAxis
*
* @return {boolean}
* true if the axis is the outermost axis in its dimension; false if not
*/
Axis.prototype.isOuterAxis = function () {
var axis = this,
chart = axis.chart,
thisIndex = -1,
isOuter = true;
chart.axes.forEach(function (otherAxis, index) {
if (otherAxis.side === axis.side && !otherAxis.isNavigatorAxis()) {
if (otherAxis === axis) {
// Get the index of the axis in question
thisIndex = index;
// Check thisIndex >= 0 in case thisIndex has
// not been found yet
} else if (thisIndex >= 0 && index > thisIndex) {
// There was an axis on the same side with a
// higher index.
isOuter = false;
}
}
});
// There were either no other axes on the same side,
// or the other axes were not farther from the chart
return isOuter;
};
/**
* Get the largest label width and height.
*
* @private
* @function Highcharts.Axis#getMaxLabelDimensions
*
* @param {Highcharts.Dictionary<Highcharts.Tick>} ticks
* All the ticks on one axis.
*
* @param {Array<number|string>} tickPositions
* All the tick positions on one axis.
*
* @return {object}
* object containing the properties height and width.
*/
Axis.prototype.getMaxLabelDimensions = function (ticks, tickPositions) {
var dimensions = {
width: 0,
height: 0
};
tickPositions.forEach(function (pos) {
var tick = ticks[pos],
tickHeight = 0,
tickWidth = 0,
label;
if (isObject(tick)) {
label = isObject(tick.label) ? tick.label : {};
// Find width and height of tick
tickHeight = label.getBBox ? label.getBBox().height : 0;
tickWidth = isNumber(label.textPxLength) ? label.textPxLength : 0;
// Update the result if width and/or height are larger
dimensions.height = Math.max(tickHeight, dimensions.height);
dimensions.width = Math.max(tickWidth, dimensions.width);
}
});
return dimensions;
};
// Add custom date formats
H.dateFormats = {
// Week number
W: function (timestamp) {
var d = new Date(timestamp),
yearStart,
weekNo;
d.setHours(0, 0, 0, 0);
d.setDate(d.getDate() - (d.getDay() || 7));
yearStart = new Date(d.getFullYear(), 0, 1);
weekNo = Math.ceil((((d - yearStart) / 86400000) + 1) / 7);
return weekNo;
},
// First letter of the day of the week, e.g. 'M' for 'Monday'.
E: function (timestamp) {
return dateFormat('%a', timestamp, true).charAt(0);
}
};
addEvent(
Tick,
'afterGetLabelPosition',
/**
* Center tick labels in cells.
*
* @private
*/
function (e) {
var tick = this,
label = tick.label,
axis = tick.axis,
reversed = axis.reversed,
chart = axis.chart,
options = axis.options,
gridOptions = (
(options && isObject(options.grid)) ? options.grid : {}
),
labelOpts = axis.options.labels,
align = labelOpts.align,
// verticalAlign is currently not supported for axis.labels.
verticalAlign = 'middle', // labelOpts.verticalAlign,
side = axisSide[axis.side],
tickmarkOffset = e.tickmarkOffset,
tickPositions = axis.tickPositions,
tickPos = tick.pos - tickmarkOffset,
nextTickPos = (
isNumber(tickPositions[e.index + 1]) ?
tickPositions[e.index + 1] - tickmarkOffset :
axis.max + tickmarkOffset
),
tickSize = axis.tickSize('tick', true),
tickWidth = isArray(tickSize) ? tickSize[0] : 0,
crispCorr = tickSize && tickSize[1] / 2,
labelHeight,
lblMetrics,
lines,
bottom,
top,
left,
right;
// Only center tick labels in grid axes
if (gridOptions.enabled === true) {
// Calculate top and bottom positions of the cell.
if (side === 'top') {
bottom = axis.top + axis.offset;
top = bottom - tickWidth;
} else if (side === 'bottom') {
top = chart.chartHeight - axis.bottom + axis.offset;
bottom = top + tickWidth;
} else {
bottom = axis.top + axis.len - axis.translate(
reversed ? nextTickPos : tickPos
);
top = axis.top + axis.len - axis.translate(
reversed ? tickPos : nextTickPos
);
}
// Calculate left and right positions of the cell.
if (side === 'right') {
left = chart.chartWidth - axis.right + axis.offset;
right = left + tickWidth;
} else if (side === 'left') {
right = axis.left + axis.offset;
left = right - tickWidth;
} else {
left = Math.round(axis.left + axis.translate(
reversed ? nextTickPos : tickPos
)) - crispCorr;
right = Math.round(axis.left + axis.translate(
reversed ? tickPos : nextTickPos
)) - crispCorr;
}
tick.slotWidth = right - left;
// Calculate the positioning of the label based on alignment.
e.pos.x = (
align === 'left' ?
left :
align === 'right' ?
right :
left + ((right - left) / 2) // default to center
);
e.pos.y = (
verticalAlign === 'top' ?
top :
verticalAlign === 'bottom' ?
bottom :
top + ((bottom - top) / 2) // default to middle
);
lblMetrics = chart.renderer.fontMetrics(
labelOpts.style.fontSize,
label.element
);
labelHeight = label.getBBox().height;
// Adjustment to y position to align the label correctly.
// Would be better to have a setter or similar for this.
if (!labelOpts.useHTML) {
lines = Math.round(labelHeight / lblMetrics.h);
e.pos.y += (
// Center the label
// TODO: why does this actually center the label?
((lblMetrics.b - (lblMetrics.h - lblMetrics.f)) / 2) +
// Adjust for height of additional lines.
-(((lines - 1) * lblMetrics.h) / 2)
);
} else {
e.pos.y += (
// Readjust yCorr in htmlUpdateTransform
lblMetrics.b +
// Adjust for height of html label
-(labelHeight / 2)
);
}
e.pos.x += (axis.horiz && labelOpts.x || 0);
}
}
);
// Draw vertical axis ticks extra long to create cell floors and roofs.
// Overrides the tickLength for vertical axes.
addEvent(Axis, 'afterTickSize', function (e) {
var axis = this,
dimensions = axis.maxLabelDimensions,
options = axis.options,
gridOptions = (options && isObject(options.grid)) ? options.grid : {},
labelPadding,
distance;
if (gridOptions.enabled === true) {
labelPadding = (Math.abs(axis.defaultLeftAxisOptions.labels.x) * 2);
distance = labelPadding +
(axis.horiz ? dimensions.height : dimensions.width);
if (isArray(e.tickSize)) {
e.tickSize[0] = distance;
} else {
e.tickSize = [distance];
}
}
});
addEvent(Axis, 'afterGetTitlePosition', function (e) {
var axis = this,
options = axis.options,
gridOptions = (options && isObject(options.grid)) ? options.grid : {};
if (gridOptions.enabled === true) {
// compute anchor points for each of the title align options
var title = axis.axisTitle,
titleWidth = title && title.getBBox().width,
horiz = axis.horiz,
axisLeft = axis.left,
axisTop = axis.top,
axisWidth = axis.width,
axisHeight = axis.height,
axisTitleOptions = options.title,
opposite = axis.opposite,
offset = axis.offset,
tickSize = axis.tickSize() || [0],
xOption = axisTitleOptions.x || 0,
yOption = axisTitleOptions.y || 0,
titleMargin = pick(axisTitleOptions.margin, horiz ? 5 : 10),
titleFontSize = axis.chart.renderer.fontMetrics(
axisTitleOptions.style && axisTitleOptions.style.fontSize,
title
).f,
// TODO account for alignment
// the position in the perpendicular direction of the axis
offAxis = (horiz ? axisTop + axisHeight : axisLeft) +
(horiz ? 1 : -1) * // horizontal axis reverses the margin
(opposite ? -1 : 1) * // so does opposite axes
(tickSize[0] / 2) +
(axis.side === axisSide.bottom ? titleFontSize : 0);
e.titlePosition.x = horiz ?
axisLeft - titleWidth / 2 - titleMargin + xOption :
offAxis + (opposite ? axisWidth : 0) + offset + xOption;
e.titlePosition.y = horiz ?
(
offAxis -
(opposite ? axisHeight : 0) +
(opposite ? titleFontSize : -titleFontSize) / 2 +
offset +
yOption
) :
axisTop - titleMargin + yOption;
}
});
// Avoid altering tickInterval when reserving space.
wrap(Axis.prototype, 'unsquish', function (proceed) {
var axis = this,
options = axis.options,
gridOptions = (options && isObject(options.grid)) ? options.grid : {};
if (gridOptions.enabled === true && this.categories) {
return this.tickInterval;
}
return proceed.apply(this, argsToArray(arguments));
});
addEvent(
Axis,
'afterSetOptions',
/**
* Creates a left and right wall on horizontal axes:
*
* - Places leftmost tick at the start of the axis, to create a left wall
*
* - Ensures that the rightmost tick is at the end of the axis, to create a
* right wall.
*
* @private
* @function
*/
function (e) {
var options = this.options,
userOptions = e.userOptions,
gridAxisOptions,
gridOptions = (
(options && isObject(options.grid)) ? options.grid : {}
);
if (gridOptions.enabled === true) {
// Merge the user options into default grid axis options so that
// when a user option is set, it takes presedence.
gridAxisOptions = merge(true, {
className: (
'highcharts-grid-axis ' + (userOptions.className || '')
),
dateTimeLabelFormats: {
hour: {
list: ['%H:%M', '%H']
},
day: {
list: ['%A, %e. %B', '%a, %e. %b', '%E']
},
week: {
list: ['Week %W', 'W%W']
},
month: {
list: ['%B', '%b', '%o']
}
},
grid: {
borderWidth: 1
},
labels: {
padding: 2,
style: {
fontSize: '13px'
}
},
margin: 0,
title: {
text: null,
reserveSpace: false,
rotation: 0
},
// In a grid axis, only allow one unit of certain types, for
// example we shouln't have one grid cell spanning two days.
units: [[
'millisecond', // unit name
[1, 10, 100]
], [
'second',
[1, 10]
], [
'minute',
[1, 5, 15]
], [
'hour',
[1, 6]
], [
'day',
[1]
], [
'week',
[1]
], [
'month',
[1]
], [
'year',
null
]]
}, userOptions);
// X-axis specific options
if (this.coll === 'xAxis') {
// For linked axes, tickPixelInterval is used only if the
// tickPositioner below doesn't run or returns undefined (like
// multiple years)
if (
defined(userOptions.linkedTo) &&
!defined(userOptions.tickPixelInterval)
) {
gridAxisOptions.tickPixelInterval = 350;
}
// For the secondary grid axis, use the primary axis' tick
// intervals and return ticks one level higher.
if (
// Check for tick pixel interval in options
!defined(userOptions.tickPixelInterval) &&
// Only for linked axes
defined(userOptions.linkedTo) &&
!defined(userOptions.tickPositioner) &&
!defined(userOptions.tickInterval)
) {
gridAxisOptions.tickPositioner = function (min, max) {
var parentInfo = (
this.linkedParent &&
this.linkedParent.tickPositions &&
this.linkedParent.tickPositions.info
);
if (parentInfo) {
var unitIdx,
count,
unitName,
i,
units = gridAxisOptions.units,
unitRange;
for (i = 0; i < units.length; i++) {
if (units[i][0] === parentInfo.unitName) {
unitIdx = i;
break;
}
}
// Spanning multiple years, go default
if (!units[unitIdx][1]) {
return;
}
// Get the first allowed count on the next unit.
if (units[unitIdx + 1]) {
unitName = units[unitIdx + 1][0];
count = (units[unitIdx + 1][1] || [1])[0];
}
unitRange = H.timeUnits[unitName];
this.tickInterval = unitRange * count;
return this.getTimeTicks(
{
unitRange: unitRange,
count: count,
unitName: unitName
},
min,
max,
this.options.startOfWeek
);
}
};
}
}
// Now merge the combined options into the axis options
merge(true, this.options, gridAxisOptions);
if (this.horiz) {
/* _________________________
Make this: ___|_____|_____|_____|__|
^ ^
_________________________
Into this: |_____|_____|_____|_____|
^ ^ */
options.minPadding = pick(userOptions.minPadding, 0);
options.maxPadding = pick(userOptions.maxPadding, 0);
}
// If borderWidth is set, then use its value for tick and line
// width.
if (isNumber(options.grid.borderWidth)) {
options.tickWidth = options.lineWidth = gridOptions.borderWidth;
}
}
}
);
addEvent(
Axis,
'afterSetAxisTranslation',
function () {
var axis = this,
options = axis.options,
gridOptions = (
(options && isObject(options.grid)) ? options.grid : {}
),
tickInfo = this.tickPositions && this.tickPositions.info,
userLabels = this.userOptions.labels || {};
if (this.horiz) {
if (gridOptions.enabled === true) {
axis.series.forEach(function (series) {
series.options.pointRange = 0;
});
}
// Lower level time ticks, like hours or minutes, represent points
// in time and not ranges. These should be aligned left in the grid
// cell by default. The same applies to years of higher order.
if (
tickInfo &&
(
options.dateTimeLabelFormats[tickInfo.unitName]
.range === false ||
tickInfo.count > 1 // years
) &&
!defined(userLabels.align)
) {
options.labels.align = 'left';
if (!defined(userLabels.x)) {
options.labels.x = 3;
}
}
}
}
);
// @todo Does this function do what the drawing says? Seems to affect ticks and
// not the labels directly?
addEvent(
Axis,
'trimTicks',
/**
* Makes tick labels which are usually ignored in a linked axis displayed if
* they are within range of linkedParent.min.
* ```
* _____________________________
* | | | | |
* Make this: | | 2 | 3 | 4 |
* |___|_______|_______|_______|
* ^
* _____________________________
* | | | | |
* Into this: | 1 | 2 | 3 | 4 |
* |___|_______|_______|_______|
* ^
* ```
*
* @private
*/
function () {
var axis = this,
options = axis.options,
gridOptions = (
(options && isObject(options.grid)) ? options.grid : {}
),
categoryAxis = axis.categories,
tickPositions = axis.tickPositions,
firstPos = tickPositions[0],
lastPos = tickPositions[tickPositions.length - 1],
linkedMin = axis.linkedParent && axis.linkedParent.min,
linkedMax = axis.linkedParent && axis.linkedParent.max,
min = linkedMin || axis.min,
max = linkedMax || axis.max,
tickInterval = axis.tickInterval,
moreThanMin = firstPos > min,
lessThanMax = lastPos < max,
endMoreThanMin = firstPos < min && firstPos + tickInterval > min,
startLessThanMax = lastPos > max && lastPos - tickInterval < max;
if (
gridOptions.enabled === true &&
!categoryAxis &&
(axis.horiz || axis.isLinked)
) {
if ((moreThanMin || endMoreThanMin) && !options.startOnTick) {
tickPositions[0] = min;
}
if ((lessThanMax || startLessThanMax) && !options.endOnTick) {
tickPositions[tickPositions.length - 1] = max;
}
}
}
);
addEvent(
Axis,
'afterRender',
/**
* Draw an extra line on the far side of the outermost axis,
* creating floor/roof/wall of a grid. And some padding.
* ```
* Make this:
* (axis.min) __________________________ (axis.max)
* | | | | |
* Into this:
* (axis.min) __________________________ (axis.max)
* ___|____|____|____|____|__
* ```
*
* @private
* @function
*
* @param {Function} proceed
* the original function
*/
function () {
var axis = this,
options = axis.options,
gridOptions = ((
options && isObject(options.grid)) ? options.grid : {}
),
labelPadding,
distance,
lineWidth,
linePath,
yStartIndex,
yEndIndex,
xStartIndex,
xEndIndex,
renderer = axis.chart.renderer,
horiz = axis.horiz,
axisGroupBox;
if (gridOptions.enabled === true) {
// @todo acutual label padding (top, bottom, left, right)
// Label padding is needed to figure out where to draw the outer
// line.
labelPadding = (Math.abs(axis.defaultLeftAxisOptions.labels.x) * 2);
axis.maxLabelDimensions = axis.getMaxLabelDimensions(
axis.ticks,
axis.tickPositions
);
distance = axis.maxLabelDimensions.width + labelPadding;
lineWidth = options.lineWidth;
// Remove right wall before rendering if updating
if (axis.rightWall) {
axis.rightWall.destroy();
}
axisGroupBox = axis.axisGroup.getBBox();
/*
Draw an extra axis line on outer axes
>
Make this: |______|______|______|___
> _________________________
Into this: |______|______|______|__|
*/
if (axis.isOuterAxis() && axis.axisLine) {
if (horiz) {
// -1 to avoid adding distance each time the chart updates
distance = axisGroupBox.height - 1;
}
if (lineWidth) {
linePath = axis.getLinePath(lineWidth);
xStartIndex = linePath.indexOf('M') + 1;
xEndIndex = linePath.indexOf('L') + 1;
yStartIndex = linePath.indexOf('M') + 2;
yEndIndex = linePath.indexOf('L') + 2;
// Negate distance if top or left axis
if (axis.side === axisSide.top ||
axis.side === axisSide.left
) {
distance = -distance;
}
// If axis is horizontal, reposition line path vertically
if (horiz) {
linePath[yStartIndex] = (
linePath[yStartIndex] + distance
);
linePath[yEndIndex] = linePath[yEndIndex] + distance;
} else {
// If axis is vertical, reposition line path
// horizontally
linePath[xStartIndex] = (
linePath[xStartIndex] + distance
);
linePath[xEndIndex] = linePath[xEndIndex] + distance;
}
if (!axis.axisLineExtra) {
axis.axisLineExtra = renderer.path(linePath)
.attr({
stroke: options.lineColor,
'stroke-width': lineWidth,
zIndex: 7
})
.addClass('highcharts-axis-line')
.add(axis.axisGroup);
} else {
axis.axisLineExtra.animate({
d: linePath
});
}
// show or hide the line depending on options.showEmpty
axis.axisLine[axis.showAxis ? 'show' : 'hide'](true);
}
}
}
}
);
// Wraps axis init to draw cell walls on vertical axes.
addEvent(Axis, 'init', function (e) {
var axis = this,
chart = axis.chart,
userOptions = e.userOptions,
gridOptions = (
(userOptions && isObject(userOptions.grid)) ?
userOptions.grid :
{}
),
columnOptions,
column,
columnIndex,
i;
function applyGridOptions() {
var options = axis.options,
// TODO: Consider using cell margins defined in % of font size?
// 25 is optimal height for default fontSize (11px)
// 25 / 11 ≈ 2.28
fontSizeToCellHeightRatio = 25 / 11,
fontSize = options.labels.style.fontSize,
fontMetrics = axis.chart.renderer.fontMetrics(fontSize);
// Center-align by default
if (!options.labels) {
options.labels = {};
}
options.labels.align = pick(options.labels.align, 'center');
// @todo: Check against tickLabelPlacement between/on etc
/* Prevents adding the last tick label if the axis is not a category
axis.
Since numeric labels are normally placed at starts and ends of a
range of value, and this module makes the label point at the value,
an "extra" label would appear. */
if (!axis.categories) {
options.showLastLabel = false;
}
// Make tick marks taller, creating cell walls of a grid. Use cellHeight
// axis option if set
if (axis.horiz) {
options.tickLength = gridOptions.cellHeight ||
fontMetrics.h * fontSizeToCellHeightRatio;
}
// Prevents rotation of labels when squished, as rotating them would not
// help.
axis.labelRotation = 0;
options.labels.rotation = 0;
}
if (gridOptions.enabled) {
if (defined(gridOptions.borderColor)) {
userOptions.tickColor =
userOptions.lineColor = gridOptions.borderColor;
}
// Handle columns, each column is a grid axis
if (isArray(gridOptions.columns)) {
columnIndex = 0;
i = gridOptions.columns.length;
while (i--) {
columnOptions = merge(
userOptions,
gridOptions.columns[i],
{
// Force to behave like category axis
type: 'category'
}
);
delete columnOptions.grid.columns; // Prevent recursion
column = new Axis(axis.chart, columnOptions);
column.isColumn = true;
column.columnIndex = columnIndex;
wrap(column, 'labelFormatter', function (proceed) {
var axis = this.axis,
tickPos = axis.tickPositions,
value = this.value,
series = axis.series[0],
isFirst = value === tickPos[0],
isLast = value === tickPos[tickPos.length - 1],
point = H.find(series.options.data, function (p) {
return p[axis.isXAxis ? 'x' : 'y'] === value;
});
// Make additional properties available for the formatter
this.isFirst = isFirst;
this.isLast = isLast;
this.point = point;
// Call original labelFormatter
return proceed.call(this);
});
columnIndex++;
}
// This axis should not be shown, instead the column axes take over
addEvent(this, 'afterInit', function () {
H.erase(chart.axes, this);
H.erase(chart[axis.coll], this);
});
} else {
addEvent(this, 'afterInit', applyGridOptions);
}
}
});
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,816 @@
/* *
* (c) 2016 Highsoft AS
* Author: Øystein Moseng
*
* License: www.highcharts.com/license
*/
'use strict';
import H from '../parts/Globals.js';
import '../parts/Utilities.js';
var min = Math.min,
max = Math.max,
abs = Math.abs,
pick = H.pick;
/**
* Get index of last obstacle before xMin. Employs a type of binary search, and
* thus requires that obstacles are sorted by xMin value.
*
* @private
* @function findLastObstacleBefore
*
* @param {Array<object>} obstacles
* Array of obstacles to search in.
*
* @param {number} xMin
* The xMin threshold.
*
* @param {number} startIx
* Starting index to search from. Must be within array range.
*
* @return {number}
* The index of the last obstacle element before xMin.
*/
function findLastObstacleBefore(obstacles, xMin, startIx) {
var left = startIx || 0, // left limit
right = obstacles.length - 1, // right limit
min = xMin - 0.0000001, // Make sure we include all obstacles at xMin
cursor,
cmp;
while (left <= right) {
cursor = (right + left) >> 1;
cmp = min - obstacles[cursor].xMin;
if (cmp > 0) {
left = cursor + 1;
} else if (cmp < 0) {
right = cursor - 1;
} else {
return cursor;
}
}
return left > 0 ? left - 1 : 0;
}
/**
* Test if a point lays within an obstacle.
*
* @private
* @function pointWithinObstacle
*
* @param {object} obstacle
* Obstacle to test.
*
* @param {Highcharts.Point} point
* Point with x/y props.
*
* @return {boolean}
* Whether point is within the obstacle or not.
*/
function pointWithinObstacle(obstacle, point) {
return (
point.x <= obstacle.xMax &&
point.x >= obstacle.xMin &&
point.y <= obstacle.yMax &&
point.y >= obstacle.yMin
);
}
/**
* Find the index of an obstacle that wraps around a point.
* Returns -1 if not found.
*
* @private
* @function findObstacleFromPoint
*
* @param {Array<object>} obstacles
* Obstacles to test.
*
* @param {Highcharts.Point} point
* Point with x/y props.
*
* @return {number}
* Ix of the obstacle in the array, or -1 if not found.
*/
function findObstacleFromPoint(obstacles, point) {
var i = findLastObstacleBefore(obstacles, point.x + 1) + 1;
while (i--) {
if (obstacles[i].xMax >= point.x &&
// optimization using lazy evaluation
pointWithinObstacle(obstacles[i], point)) {
return i;
}
}
return -1;
}
/**
* Get SVG path array from array of line segments.
*
* @private
* @function pathFromSegments
*
* @param {Array<object>} segments
* The segments to build the path from.
*
* @return {Highcharts.SVGPathArray}
* SVG path array as accepted by the SVG Renderer.
*/
function pathFromSegments(segments) {
var path = [];
if (segments.length) {
path.push('M', segments[0].start.x, segments[0].start.y);
for (var i = 0; i < segments.length; ++i) {
path.push('L', segments[i].end.x, segments[i].end.y);
}
}
return path;
}
/**
* Limits obstacle max/mins in all directions to bounds. Modifies input
* obstacle.
*
* @private
* @function limitObstacleToBounds
*
* @param {object} obstacle
* Obstacle to limit.
*
* @param {object} bounds
* Bounds to use as limit.
*/
function limitObstacleToBounds(obstacle, bounds) {
obstacle.yMin = max(obstacle.yMin, bounds.yMin);
obstacle.yMax = min(obstacle.yMax, bounds.yMax);
obstacle.xMin = max(obstacle.xMin, bounds.xMin);
obstacle.xMax = min(obstacle.xMax, bounds.xMax);
}
// Define the available pathfinding algorithms.
// Algorithms take up to 3 arguments: starting point, ending point, and an
// options object.
var algorithms = {
/**
* Get an SVG path from a starting coordinate to an ending coordinate.
* Draws a straight line.
*
* @function Highcharts.Pathfinder.algorithms.straight
*
* @param {object} start
* Starting coordinate, object with x/y props.
*
* @param {object} end
* Ending coordinate, object with x/y props.
*
* @return {object}
* An object with the SVG path in Array form as accepted by the SVG
* renderer, as well as an array of new obstacles making up this
* path.
*/
straight: function (start, end) {
return {
path: ['M', start.x, start.y, 'L', end.x, end.y],
obstacles: [{ start: start, end: end }]
};
},
/**
* Find a path from a starting coordinate to an ending coordinate, using
* right angles only, and taking only starting/ending obstacle into
* consideration.
*
* @function Highcharts.Pathfinder.algorithms.simpleConnect
*
* @param {object} start
* Starting coordinate, object with x/y props.
*
* @param {object} end
* Ending coordinate, object with x/y props.
*
* @param {object} options
* Options for the algorithm:
* - chartObstacles: Array of chart obstacles to avoid
* - startDirectionX: Optional. True if starting in the X direction.
* If not provided, the algorithm starts in the direction that is
* the furthest between start/end.
*
* @return {object}
* An object with the SVG path in Array form as accepted by the SVG
* renderer, as well as an array of new obstacles making up this
* path.
*/
simpleConnect: H.extend(function (start, end, options) {
var segments = [],
endSegment,
dir = pick(
options.startDirectionX,
abs(end.x - start.x) > abs(end.y - start.y)
) ? 'x' : 'y',
chartObstacles = options.chartObstacles,
startObstacleIx = findObstacleFromPoint(chartObstacles, start),
endObstacleIx = findObstacleFromPoint(chartObstacles, end),
startObstacle,
endObstacle,
prevWaypoint,
waypoint,
waypoint2,
useMax,
endPoint;
// Return a clone of a point with a property set from a target object,
// optionally with an offset
function copyFromPoint(from, fromKey, to, toKey, offset) {
var point = {
x: from.x,
y: from.y
};
point[fromKey] = to[toKey || fromKey] + (offset || 0);
return point;
}
// Return waypoint outside obstacle
function getMeOut(obstacle, point, direction) {
var useMax = abs(point[direction] - obstacle[direction + 'Min']) >
abs(point[direction] - obstacle[direction + 'Max']);
return copyFromPoint(
point,
direction,
obstacle,
direction + (useMax ? 'Max' : 'Min'),
useMax ? 1 : -1
);
}
// Pull out end point
if (endObstacleIx > -1) {
endObstacle = chartObstacles[endObstacleIx];
waypoint = getMeOut(endObstacle, end, dir);
endSegment = {
start: waypoint,
end: end
};
endPoint = waypoint;
} else {
endPoint = end;
}
// If an obstacle envelops the start point, add a segment to get out,
// and around it.
if (startObstacleIx > -1) {
startObstacle = chartObstacles[startObstacleIx];
waypoint = getMeOut(startObstacle, start, dir);
segments.push({
start: start,
end: waypoint
});
// If we are going back again, switch direction to get around start
// obstacle.
if (
waypoint[dir] > start[dir] === // Going towards max from start
waypoint[dir] > endPoint[dir] // Going towards min to end
) {
dir = dir === 'y' ? 'x' : 'y';
useMax = start[dir] < end[dir];
segments.push({
start: waypoint,
end: copyFromPoint(
waypoint,
dir,
startObstacle,
dir + (useMax ? 'Max' : 'Min'),
useMax ? 1 : -1
)
});
// Switch direction again
dir = dir === 'y' ? 'x' : 'y';
}
}
// We are around the start obstacle. Go towards the end in one
// direction.
prevWaypoint = segments.length ?
segments[segments.length - 1].end :
start;
waypoint = copyFromPoint(prevWaypoint, dir, endPoint);
segments.push({
start: prevWaypoint,
end: waypoint
});
// Final run to end point in the other direction
dir = dir === 'y' ? 'x' : 'y';
waypoint2 = copyFromPoint(waypoint, dir, endPoint);
segments.push({
start: waypoint,
end: waypoint2
});
// Finally add the endSegment
segments.push(endSegment);
return {
path: pathFromSegments(segments),
obstacles: segments
};
}, {
requiresObstacles: true
}),
/**
* Find a path from a starting coordinate to an ending coordinate, taking
* obstacles into consideration. Might not always find the optimal path,
* but is fast, and usually good enough.
*
* @function Highcharts.Pathfinder.algorithms.fastAvoid
*
* @param {object} start
* Starting coordinate, object with x/y props.
*
* @param {object} end
* Ending coordinate, object with x/y props.
*
* @param {object} options
* Options for the algorithm.
* - chartObstacles: Array of chart obstacles to avoid
* - lineObstacles: Array of line obstacles to jump over
* - obstacleMetrics: Object with metrics of chartObstacles cached
* - hardBounds: Hard boundaries to not cross
* - obstacleOptions: Options for the obstacles, including margin
* - startDirectionX: Optional. True if starting in the X direction.
* If not provided, the algorithm starts in the
* direction that is the furthest between
* start/end.
*
* @return {object}
* An object with the SVG path in Array form as accepted by the SVG
* renderer, as well as an array of new obstacles making up this
* path.
*/
fastAvoid: H.extend(function (start, end, options) {
/*
Algorithm rules/description
- Find initial direction
- Determine soft/hard max for each direction.
- Move along initial direction until obstacle.
- Change direction.
- If hitting obstacle, first try to change length of previous line
before changing direction again.
Soft min/max x = start/destination x +/- widest obstacle + margin
Soft min/max y = start/destination y +/- tallest obstacle + margin
@todo:
- Make retrospective, try changing prev segment to reduce
corners
- Fix logic for breaking out of end-points - not always picking
the best direction currently
- When going around the end obstacle we should not always go the
shortest route, rather pick the one closer to the end point
*/
var dirIsX = pick(
options.startDirectionX,
abs(end.x - start.x) > abs(end.y - start.y)
),
dir = dirIsX ? 'x' : 'y',
segments,
useMax,
extractedEndPoint,
endSegments = [],
forceObstacleBreak = false, // Used in clearPathTo to keep track of
// when to force break through an obstacle.
// Boundaries to stay within. If beyond soft boundary, prefer to
// change direction ASAP. If at hard max, always change immediately.
metrics = options.obstacleMetrics,
softMinX = min(start.x, end.x) - metrics.maxWidth - 10,
softMaxX = max(start.x, end.x) + metrics.maxWidth + 10,
softMinY = min(start.y, end.y) - metrics.maxHeight - 10,
softMaxY = max(start.y, end.y) + metrics.maxHeight + 10,
// Obstacles
chartObstacles = options.chartObstacles,
startObstacleIx = findLastObstacleBefore(chartObstacles, softMinX),
endObstacleIx = findLastObstacleBefore(chartObstacles, softMaxX);
// How far can you go between two points before hitting an obstacle?
// Does not work for diagonal lines (because it doesn't have to).
function pivotPoint(fromPoint, toPoint, directionIsX) {
var firstPoint,
lastPoint,
highestPoint,
lowestPoint,
i,
searchDirection = fromPoint.x < toPoint.x ? 1 : -1;
if (fromPoint.x < toPoint.x) {
firstPoint = fromPoint;
lastPoint = toPoint;
} else {
firstPoint = toPoint;
lastPoint = fromPoint;
}
if (fromPoint.y < toPoint.y) {
lowestPoint = fromPoint;
highestPoint = toPoint;
} else {
lowestPoint = toPoint;
highestPoint = fromPoint;
}
// Go through obstacle range in reverse if toPoint is before
// fromPoint in the X-dimension.
i = searchDirection < 0 ?
// Searching backwards, start at last obstacle before last point
min(findLastObstacleBefore(chartObstacles, lastPoint.x),
chartObstacles.length - 1) :
// Forwards. Since we're not sorted by xMax, we have to look
// at all obstacles.
0;
// Go through obstacles in this X range
while (chartObstacles[i] && (
searchDirection > 0 && chartObstacles[i].xMin <= lastPoint.x ||
searchDirection < 0 && chartObstacles[i].xMax >= firstPoint.x
)) {
// If this obstacle is between from and to points in a straight
// line, pivot at the intersection.
if (
chartObstacles[i].xMin <= lastPoint.x &&
chartObstacles[i].xMax >= firstPoint.x &&
chartObstacles[i].yMin <= highestPoint.y &&
chartObstacles[i].yMax >= lowestPoint.y
) {
if (directionIsX) {
return {
y: fromPoint.y,
x: fromPoint.x < toPoint.x ?
chartObstacles[i].xMin - 1 :
chartObstacles[i].xMax + 1,
obstacle: chartObstacles[i]
};
}
// else ...
return {
x: fromPoint.x,
y: fromPoint.y < toPoint.y ?
chartObstacles[i].yMin - 1 :
chartObstacles[i].yMax + 1,
obstacle: chartObstacles[i]
};
}
i += searchDirection;
}
return toPoint;
}
/**
* Decide in which direction to dodge or get out of an obstacle.
* Considers desired direction, which way is shortest, soft and hard
* bounds.
*
* (? Returns a string, either xMin, xMax, yMin or yMax.)
*
* @private
* @function
*
* @param {object} obstacle
* Obstacle to dodge/escape.
*
* @param {object} fromPoint
* Point with x/y props that's dodging/escaping.
*
* @param {object} toPoint
* Goal point.
*
* @param {boolean} dirIsX
* Dodge in X dimension.
*
* @param {object} bounds
* Hard and soft boundaries.
*
* @return {boolean}
* Use max or not.
*/
function getDodgeDirection(
obstacle,
fromPoint,
toPoint,
dirIsX,
bounds
) {
var softBounds = bounds.soft,
hardBounds = bounds.hard,
dir = dirIsX ? 'x' : 'y',
toPointMax = { x: fromPoint.x, y: fromPoint.y },
toPointMin = { x: fromPoint.x, y: fromPoint.y },
minPivot,
maxPivot,
maxOutOfSoftBounds = obstacle[dir + 'Max'] >=
softBounds[dir + 'Max'],
minOutOfSoftBounds = obstacle[dir + 'Min'] <=
softBounds[dir + 'Min'],
maxOutOfHardBounds = obstacle[dir + 'Max'] >=
hardBounds[dir + 'Max'],
minOutOfHardBounds = obstacle[dir + 'Min'] <=
hardBounds[dir + 'Min'],
// Find out if we should prefer one direction over the other if
// we can choose freely
minDistance = abs(obstacle[dir + 'Min'] - fromPoint[dir]),
maxDistance = abs(obstacle[dir + 'Max'] - fromPoint[dir]),
// If it's a small difference, pick the one leading towards dest
// point. Otherwise pick the shortest distance
useMax = abs(minDistance - maxDistance) < 10 ?
fromPoint[dir] < toPoint[dir] :
maxDistance < minDistance;
// Check if we hit any obstacles trying to go around in either
// direction.
toPointMin[dir] = obstacle[dir + 'Min'];
toPointMax[dir] = obstacle[dir + 'Max'];
minPivot = pivotPoint(fromPoint, toPointMin, dirIsX)[dir] !==
toPointMin[dir];
maxPivot = pivotPoint(fromPoint, toPointMax, dirIsX)[dir] !==
toPointMax[dir];
useMax = minPivot ?
(maxPivot ? useMax : true) :
(maxPivot ? false : useMax);
// useMax now contains our preferred choice, bounds not taken into
// account. If both or neither direction is out of bounds we want to
// use this.
// Deal with soft bounds
useMax = minOutOfSoftBounds ?
(maxOutOfSoftBounds ? useMax : true) : // Out on min
(maxOutOfSoftBounds ? false : useMax); // Not out on min
// Deal with hard bounds
useMax = minOutOfHardBounds ?
(maxOutOfHardBounds ? useMax : true) : // Out on min
(maxOutOfHardBounds ? false : useMax); // Not out on min
return useMax;
}
// Find a clear path between point
function clearPathTo(fromPoint, toPoint, dirIsX) {
// Don't waste time if we've hit goal
if (fromPoint.x === toPoint.x && fromPoint.y === toPoint.y) {
return [];
}
var dir = dirIsX ? 'x' : 'y',
pivot,
segments,
waypoint,
waypointUseMax,
envelopingObstacle,
secondEnvelopingObstacle,
envelopWaypoint,
obstacleMargin = options.obstacleOptions.margin,
bounds = {
soft: {
xMin: softMinX,
xMax: softMaxX,
yMin: softMinY,
yMax: softMaxY
},
hard: options.hardBounds
};
// If fromPoint is inside an obstacle we have a problem. Break out
// by just going to the outside of this obstacle. We prefer to go to
// the nearest edge in the chosen direction.
envelopingObstacle =
findObstacleFromPoint(chartObstacles, fromPoint);
if (envelopingObstacle > -1) {
envelopingObstacle = chartObstacles[envelopingObstacle];
waypointUseMax = getDodgeDirection(
envelopingObstacle, fromPoint, toPoint, dirIsX, bounds
);
// Cut obstacle to hard bounds to make sure we stay within
limitObstacleToBounds(envelopingObstacle, options.hardBounds);
envelopWaypoint = dirIsX ? {
y: fromPoint.y,
x: envelopingObstacle[waypointUseMax ? 'xMax' : 'xMin'] +
(waypointUseMax ? 1 : -1)
} : {
x: fromPoint.x,
y: envelopingObstacle[waypointUseMax ? 'yMax' : 'yMin'] +
(waypointUseMax ? 1 : -1)
};
// If we crashed into another obstacle doing this, we put the
// waypoint between them instead
secondEnvelopingObstacle = findObstacleFromPoint(
chartObstacles, envelopWaypoint
);
if (secondEnvelopingObstacle > -1) {
secondEnvelopingObstacle = chartObstacles[
secondEnvelopingObstacle
];
// Cut obstacle to hard bounds
limitObstacleToBounds(
secondEnvelopingObstacle,
options.hardBounds
);
// Modify waypoint to lay between obstacles
envelopWaypoint[dir] = waypointUseMax ? max(
envelopingObstacle[dir + 'Max'] - obstacleMargin + 1,
(
secondEnvelopingObstacle[dir + 'Min'] +
envelopingObstacle[dir + 'Max']
) / 2
) :
min((
envelopingObstacle[dir + 'Min'] + obstacleMargin - 1
), (
(
secondEnvelopingObstacle[dir + 'Max'] +
envelopingObstacle[dir + 'Min']
) / 2
));
// We are not going anywhere. If this happens for the first
// time, do nothing. Otherwise, try to go to the extreme of
// the obstacle pair in the current direction.
if (fromPoint.x === envelopWaypoint.x &&
fromPoint.y === envelopWaypoint.y) {
if (forceObstacleBreak) {
envelopWaypoint[dir] = waypointUseMax ?
max(
envelopingObstacle[dir + 'Max'],
secondEnvelopingObstacle[dir + 'Max']
) + 1 :
min(
envelopingObstacle[dir + 'Min'],
secondEnvelopingObstacle[dir + 'Min']
) - 1;
}
// Toggle on if off, and the opposite
forceObstacleBreak = !forceObstacleBreak;
} else {
// This point is not identical to previous.
// Clear break trigger.
forceObstacleBreak = false;
}
}
segments = [{
start: fromPoint,
end: envelopWaypoint
}];
} else { // If not enveloping, use standard pivot calculation
pivot = pivotPoint(fromPoint, {
x: dirIsX ? toPoint.x : fromPoint.x,
y: dirIsX ? fromPoint.y : toPoint.y
}, dirIsX);
segments = [{
start: fromPoint,
end: {
x: pivot.x,
y: pivot.y
}
}];
// Pivot before goal, use a waypoint to dodge obstacle
if (pivot[dirIsX ? 'x' : 'y'] !== toPoint[dirIsX ? 'x' : 'y']) {
// Find direction of waypoint
waypointUseMax = getDodgeDirection(
pivot.obstacle, pivot, toPoint, !dirIsX, bounds
);
// Cut waypoint to hard bounds
limitObstacleToBounds(pivot.obstacle, options.hardBounds);
waypoint = {
x: dirIsX ?
pivot.x :
pivot.obstacle[waypointUseMax ? 'xMax' : 'xMin'] +
(waypointUseMax ? 1 : -1),
y: dirIsX ?
pivot.obstacle[waypointUseMax ? 'yMax' : 'yMin'] +
(waypointUseMax ? 1 : -1) :
pivot.y
};
// We're changing direction here, store that to make sure we
// also change direction when adding the last segment array
// after handling waypoint.
dirIsX = !dirIsX;
segments = segments.concat(clearPathTo({
x: pivot.x,
y: pivot.y
}, waypoint, dirIsX));
}
}
// Get segments for the other direction too
// Recursion is our friend
segments = segments.concat(clearPathTo(
segments[segments.length - 1].end, toPoint, !dirIsX
));
return segments;
}
// Extract point to outside of obstacle in whichever direction is
// closest. Returns new point outside obstacle.
function extractFromObstacle(obstacle, point, goalPoint) {
var dirIsX = min(obstacle.xMax - point.x, point.x - obstacle.xMin) <
min(obstacle.yMax - point.y, point.y - obstacle.yMin),
bounds = {
soft: options.hardBounds,
hard: options.hardBounds
},
useMax = getDodgeDirection(
obstacle, point, goalPoint, dirIsX, bounds
);
return dirIsX ? {
y: point.y,
x: obstacle[useMax ? 'xMax' : 'xMin'] + (useMax ? 1 : -1)
} : {
x: point.x,
y: obstacle[useMax ? 'yMax' : 'yMin'] + (useMax ? 1 : -1)
};
}
// Cut the obstacle array to soft bounds for optimization in large
// datasets.
chartObstacles =
chartObstacles.slice(startObstacleIx, endObstacleIx + 1);
// If an obstacle envelops the end point, move it out of there and add
// a little segment to where it was.
if ((endObstacleIx = findObstacleFromPoint(chartObstacles, end)) > -1) {
extractedEndPoint = extractFromObstacle(
chartObstacles[endObstacleIx],
end,
start
);
endSegments.push({
end: end,
start: extractedEndPoint
});
end = extractedEndPoint;
}
// If it's still inside one or more obstacles, get out of there by
// force-moving towards the start point.
while (
(endObstacleIx = findObstacleFromPoint(chartObstacles, end)) > -1
) {
useMax = end[dir] - start[dir] < 0;
extractedEndPoint = {
x: end.x,
y: end.y
};
extractedEndPoint[dir] = chartObstacles[endObstacleIx][
useMax ? dir + 'Max' : dir + 'Min'
] + (useMax ? 1 : -1);
endSegments.push({
end: end,
start: extractedEndPoint
});
end = extractedEndPoint;
}
// Find the path
segments = clearPathTo(start, end, dirIsX);
// Add the end-point segments
segments = segments.concat(endSegments.reverse());
return {
path: pathFromSegments(segments),
obstacles: segments
};
}, {
requiresObstacles: true
})
};
export default algorithms;
@@ -0,0 +1,158 @@
/* *
*
* (c) 2016-2019 Highsoft AS
*
* Authors: Jon Arild Nygard
*
* License: www.highcharts.com/license
*
* */
/* eslint no-console: 0 */
'use strict';
import H from '../parts/Globals.js';
import '../parts/Utilities.js';
var extend = H.extend,
isNumber = H.isNumber,
pick = H.pick,
isFunction = function (x) {
return typeof x === 'function';
};
/**
* Creates an object map from parent id to childrens index.
*
* @private
* @function Highcharts.Tree#getListOfParents
*
* @param {Array<*>} data
* List of points set in options. `Array<*>.parent`is parent id of point.
*
* @param {Array<string>} ids
* List of all point ids.
*
* @return {object}
* Map from parent id to children index in data
*/
var getListOfParents = function (data, ids) {
var listOfParents = data.reduce(function (prev, curr) {
var parent = pick(curr.parent, '');
if (prev[parent] === undefined) {
prev[parent] = [];
}
prev[parent].push(curr);
return prev;
}, {}),
parents = Object.keys(listOfParents);
// If parent does not exist, hoist parent to root of tree.
parents.forEach(function (parent, list) {
var children = listOfParents[parent];
if ((parent !== '') && (ids.indexOf(parent) === -1)) {
children.forEach(function (child) {
list[''].push(child);
});
delete list[parent];
}
});
return listOfParents;
};
var getNode = function (id, parent, level, data, mapOfIdToChildren, options) {
var descendants = 0,
height = 0,
after = options && options.after,
before = options && options.before,
node = {
data: data,
depth: level - 1,
id: id,
level: level,
parent: parent
},
start,
end,
children;
// Allow custom logic before the children has been created.
if (isFunction(before)) {
before(node, options);
}
// Call getNode recursively on the children. Calulate the height of the
// node, and the number of descendants.
children = ((mapOfIdToChildren[id] || [])).map(function (child) {
var node = getNode(
child.id,
id,
(level + 1),
child,
mapOfIdToChildren,
options
),
childStart = child.start,
childEnd = (
child.milestone === true ?
childStart :
child.end
);
// Start should be the lowest child.start.
start = (
(!isNumber(start) || childStart < start) ?
childStart :
start
);
// End should be the largest child.end.
// If child is milestone, then use start as end.
end = (
(!isNumber(end) || childEnd > end) ?
childEnd :
end
);
descendants = descendants + 1 + node.descendants;
height = Math.max(node.height + 1, height);
return node;
});
// Calculate start and end for point if it is not already explicitly set.
if (data) {
data.start = pick(data.start, start);
data.end = pick(data.end, end);
}
extend(node, {
children: children,
descendants: descendants,
height: height
});
// Allow custom logic after the children has been created.
if (isFunction(after)) {
after(node, options);
}
return node;
};
var getTree = function (data, options) {
var ids = data.map(function (d) {
return d.id;
}),
mapOfIdToChildren = getListOfParents(data, ids);
return getNode('', null, 1, null, mapOfIdToChildren, options);
};
var Tree = {
getListOfParents: getListOfParents,
getNode: getNode,
getTree: getTree
};
export default Tree;
File diff suppressed because it is too large Load Diff