initial commit
This commit is contained in:
@@ -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
Reference in New Issue
Block a user