@amcharts/amcharts5
Version:
amCharts 5
677 lines (674 loc) • 28.6 kB
JavaScript
import { Button } from "../../core/render/Button";
import { ConfirmButton } from "../../core/render/ConfirmButton";
import { Graphics } from "../../core/render/Graphics";
import { percent } from "../../core/util/Percent";
import { GanttDateAxis } from "./GanttDateAxis";
import { XYChart } from "../xy/XYChart";
import { GanttDefaultTheme } from "./GanttDefaultTheme";
import { GanttSeries } from "./GanttSeries";
import { color, Container, Rectangle, Scrollbar, Tooltip } from "../../..";
import { GanttCategoryAxis } from "./GanttCategoryAxis";
import { GanttCategoryAxisRenderer } from "./GanttCategoryAxisRenderer";
import { GanttDateAxisRenderer } from "./GanttDateAxisRenderer";
import { XYCursor } from "../xy/XYCursor";
import { ColorPicker } from "../../plugins/colorPicker/ColorPicker";
import { ColorPickerButton } from "../../plugins/colorPicker/ColorPickerButton";
import { registry } from "../../core/Registry";
import * as $utils from "../../core/util/Utils";
import * as $array from "../../core/util/Array";
import * as $time from "../../core/util/Time";
/**
* Creates a [[Gantt]] chart.
*
* @see {@link https://www.amcharts.com/docs/v5/charts/gantt/} for more info
* @important
* @since 5.14.0
*/
export class Gantt extends Container {
constructor() {
super(...arguments);
/**
* A scrollbar for horizontal scrolling.
*/
this.scrollbarX = Scrollbar.new(this._root, {
orientation: "horizontal"
})._markC("orientation");
/**
* A scrollbar for vertical scrolling.
*/
this.scrollbarY = Scrollbar.new(this._root, {
orientation: "vertical"
})._markC("orientation");
/**
* Controls (add, color picker, expand, collaps, clear buttons) container.
*/
this.controls = Container.new(this._root, {
themeTags: ["controls"],
layout: this._root.horizontalLayout,
});
this.zoomControls = Container.new(this._root, {
themeTagsSelf: ["zoomcontrols"]
});
/**
* The [[Button]] element to add new tasks.
*/
this.addButton = this.controls.children.push(Button.new(this._root, {
themeTags: ["add", "plus"],
tooltip: this.root.systemTooltip,
icon: Graphics.new(this._root, { themeTags: ["icon"] })
}));
/**
* The [[ColorPickerButton]] element to select colors for tasks.
*/
this.colorPickerButton = this.controls.children.push(ColorPickerButton.new(this._root, {
disableOpacity: true,
tooltip: this.root.systemTooltip
}));
/**
* The [[Button]] elements to expand and collapse all tasks.
*/
this.expandButton = this.controls.children.push(Button.new(this._root, {
themeTags: ["expand", "secondary", "fixedwidth"],
icon: Graphics.new(this._root, { themeTags: ["icon"] }),
tooltip: this.root.systemTooltip
}));
/**
* The [[Button]] elements to collapse all tasks.
*/
this.collapseButton = this.controls.children.push(Button.new(this._root, {
themeTags: ["collapse", "secondary", "fixedwidth"],
icon: Graphics.new(this._root, { themeTags: ["icon"] }),
tooltip: this.root.systemTooltip
}));
/**
* The [[Button]] element to toggle `linkNewTasks` setting.
*/
this.linkButton = this.controls.children.push(Button.new(this._root, {
themeTags: ["link", "secondary", "fixedwidth"],
icon: Graphics.new(this._root, { themeTags: ["icon"] }),
tooltip: this.root.systemTooltip
}));
/**
* The [[Button]] element to horizontally fit visible tasks into a view.
*/
this.fitButton = this.zoomControls.children.push(Button.new(this._root, {
themeTags: ["fit", "secondary", "zoombutton"],
icon: Graphics.new(this._root, { themeTags: ["icon"] }),
tooltip: this.root.systemTooltip
}));
/**
* The [[Button]] element to zoom out the X axis.
*/
this.zoomOutButton = this.zoomControls.children.push(Button.new(this._root, {
themeTags: ["zoomout", "secondary", "zoombutton"],
icon: Graphics.new(this._root, { themeTags: ["icon"] }),
tooltip: this.root.systemTooltip
}));
/**
* The [[Button]] element to toggle edit mode.
*
* @since 5.14.1
*/
this.editButton = this.controls.children.push(Button.new(this._root, {
themeTags: ["edit", "secondary", "fixedwidth"],
icon: Graphics.new(this._root, { themeTags: ["icon"] }),
tooltip: this.root.systemTooltip
}));
/**
* The [[Button]] element to clear all tasks.
*/
this.clearButton = this.controls.children.push(ConfirmButton.new(this._root, {
themeTags: ["clear", "secondary", "confirm"],
icon: Graphics.new(this._root, { themeTags: ["icon"] }),
tooltip: this.root.systemTooltip,
active: false
}));
/**
* The [[ColorPicker]] element to select colors for tasks.
*/
this.colorPicker = this.children.push(ColorPicker.new(this._root, { visible: false }));
}
_applyThemes(force = false) {
const colors = this.get("colors");
if (colors) {
colors.reset();
}
return super._applyThemes(force);
}
_afterNew() {
this.addTag("gantt");
this._defaultThemes.push(GanttDefaultTheme.new(this._root));
super._afterNew();
const root = this._root;
this.colorPickerButton.events.on("click", () => {
this.yAxis.xButton.hide(0);
this.colorPicker._setCAll({
colorButton: this.colorPickerButton
});
this.children.moveValue(this.colorPicker, this.children.length - 1);
this.yAxis._disposeXHideDP();
});
this.colorPicker.events.on("colorchanged", () => {
this._customColor = this.colorPicker.get("color");
this.yAxis.unselectDataItems();
});
this.addButton.events.on("click", () => {
this.addNewTask();
});
const chart = this.children.push(XYChart.new(root, {}));
chart.plotContainer.children.push(this.zoomControls);
chart.zoomOutButton.set("forceHidden", true);
chart.children.push(this.controls);
chart._setC("cursor", XYCursor.new(root, {
xAxis: this.xAxis,
yAxis: this.yAxis
})._markC("xAxis", "yAxis"));
const yRenderer = GanttCategoryAxisRenderer.new(root, {});
// to prevent moving horizontally while dragging
yRenderer.containers.template.adapters.add("x", () => {
return 0;
});
// can't set fields in theme, as data is set before theme is applied
const yAxis = chart.yAxes.push(GanttCategoryAxis.new(root, {
categoryField: "id",
cellSizeField: "cellSize",
collapsedField: "collapsed",
parentIdField: "parentId",
colorField: "color",
idField: "id",
nameField: "name",
renderer: yRenderer
})._markC("categoryField", "cellSizeField", "collapsedField", "parentIdField", "colorField", "idField", "nameField", "renderer"));
yAxis.gantt = this;
// update widht of scrollbar and controls container
yAxis.events.on("boundschanged", () => {
this._updateScrollbar();
});
chart.plotContainer.events.on("boundschanged", () => {
this._updateScrollbar();
});
this.on("visible", () => {
this.root.events.once("frameended", () => {
this._updateScrollbar();
});
});
// minor x axis
const ganttAxisRenderrerMinor = GanttDateAxisRenderer.new(root, { themeTags: ["minor"] });
// so that grid would be hidden if axis range is added
ganttAxisRenderrerMinor.grid.template.adapters.add("forceHidden", (forceHidden, target) => {
const dataItem = target.dataItem;
if (dataItem && dataItem.get("active")) {
return true;
}
return forceHidden;
});
const xAxisMinor = chart.xAxes.push(GanttDateAxis.new(root, {
themeTags: ["minor"],
baseInterval: { timeUnit: "day", count: 1 },
renderer: ganttAxisRenderrerMinor,
background: Rectangle.new(root, {
fill: color(0xfff),
fillOpacity: 0
})
})._markC("baseInterval", "renderer", "background"));
xAxisMinor.gantt = this;
// return default color for grid lines
xAxisMinor.events.on("pointerout", () => {
ganttAxisRenderrerMinor.grid.each((grid) => {
grid.unhover();
});
});
// add axis range on click
xAxisMinor.events.on("click", () => {
if (closestGrid) {
const dataItem = closestGrid.dataItem;
if (dataItem) {
const value = dataItem.get("endValue");
if (value !== undefined) {
let found = false;
xAxisMinor.axisRanges.each((axisRange) => {
// remove axis range if already exists
if (axisRange.get("value") === value) {
this.unmarkDate(value);
found = true;
}
});
// add axis range if not exists
if (!found) {
dataItem.set("active", true);
this.markDate(value);
}
}
}
}
});
// find closest grid line on hover
let closestGrid;
xAxisMinor.events.on("globalpointermove", (ev) => {
if (xAxisMinor.isHover()) {
// find most close grid line
const point = xAxisMinor.toLocal({ x: ev.point.x, y: ev.point.y });
let minX = Infinity;
ganttAxisRenderrerMinor.grid.each((grid) => {
if (grid.isVisible()) {
const dataItem = grid.dataItem;
if (dataItem) {
if (!dataItem.get("isRange")) {
let distance = Math.abs(grid.x() - point.x);
if (!grid.get("active")) {
grid.unhover();
}
if (distance < minX) {
minX = distance;
closestGrid = grid;
}
}
}
}
});
if (closestGrid) {
closestGrid.hover();
}
}
});
const ganttAxisRenderrer = GanttDateAxisRenderer.new(root, {});
const xAxis = chart.xAxes.push(GanttDateAxis.new(root, {
baseInterval: { timeUnit: "day", count: 1 },
renderer: ganttAxisRenderrer,
tooltip: Tooltip.new(root, {})
})._markC("baseInterval", "renderer", "tooltip"));
xAxis.gantt = this;
// sync minor x axis with main x axis
xAxis.onPrivate("min", (value) => {
xAxisMinor.setPrivate("min", value);
});
xAxis.onPrivate("max", (value) => {
xAxisMinor.setPrivate("max", value);
});
// sync minor x axis with main x axis
xAxis.on("start", (value) => {
xAxisMinor._setC("start", value);
});
xAxis.on("end", (value) => {
xAxisMinor._setC("end", value);
});
// set min max if not set
const baseDuration = xAxis.baseDuration();
if (xAxis.getPrivate("min") === undefined) {
xAxis.setPrivate("min", new Date().getTime());
}
if (xAxis.getPrivate("max") === undefined) {
xAxis.setPrivate("max", new Date().getTime() + baseDuration * 3);
}
// set grid interval
xAxis.onPrivate("gridInterval", (value) => {
const gridIntervals = this.get("gridIntervals");
if (gridIntervals && gridIntervals[value.timeUnit]) {
xAxisMinor._setC("gridIntervals", gridIntervals[value.timeUnit]);
}
});
// Add series
// https://www.amcharts.com/docs/v5/charts/xy-chart/series/
var series = chart.series.push(GanttSeries.new(root, {
xAxis: xAxis,
yAxis: yAxis,
baseAxis: yAxis,
openValueXField: "start",
valueXField: "end",
progressField: "progress",
durationField: "duration",
linkToField: "linkTo",
categoryYField: "id",
idField: "id"
})._markC("xAxis", "yAxis", "baseAxis", "openValueXField", "valueXField", "progressField", "durationField", "linkToField", "categoryYField", "idField"));
series.gantt = this;
// Add scrollbars
this.scrollbarX = chart._setC("scrollbarX", this.scrollbarX);
this.scrollbarY = chart._setC("scrollbarY", this.scrollbarY);
this.scrollbarY.startGrip.set("forceHidden", true);
this.scrollbarY.endGrip.set("forceHidden", true);
this.children.push(this.scrollbarX);
this.xyChart = chart;
this.series = series;
this.xAxis = xAxis;
this.yAxis = yAxis;
this.xAxisMinor = xAxisMinor;
const eventType = "valueschanged";
this.series.events.on(eventType, () => {
if (this.get("editable")) {
if (this.events.isEnabled(eventType)) {
this.events.dispatch(eventType, { type: eventType, target: this });
}
}
});
// ad dbehavior to buttons
this.expandButton.events.on("click", () => {
this.yAxis.expandAll();
});
this.collapseButton.events.on("click", () => {
this.yAxis.collapseAll();
});
this.clearButton.events.on("confirmed", () => {
this.clearAll();
});
this.linkButton.on("active", (active) => {
// The button is synced from the setting, so only a click disagrees with it
if (this.get("linkNewTasks", false) !== active) {
this.set("linkNewTasks", active);
}
});
this.colorPicker.on("color", (c) => {
this.yAxis.setDataItemColor(this.yAxis.get("selectedDataItem"), c);
});
this.editButton.on("active", (active) => {
// As above: syncing the button back into the setting is not configuration
if (this.get("editable", true) !== active) {
this.set("editable", active);
}
});
this.fitButton.events.on("click", () => {
let min = series.getPrivate("selectionMinX", 0);
let max = series.getPrivate("selectionMaxX", 0);
const extraMin = xAxis.get("extraMin", 0.1);
const extraMax = xAxis.get("extraMax", 0.1);
xAxis.zoomToValues(min - (max - min) * extraMin, max + (max - min) * extraMax);
/*
xAxis.setAll({
"strictMinMax": false,
"strictMinMaxSelection": false,
"autoZoom": true,
})
else {
xAxis.setAll({
"strictMinMax": true,
"strictMinMaxSelection": true,
"autoZoom": false
})
xAxis.setPrivate("selectionMin", undefined);
xAxis.setPrivate("selectionMax", undefined);
}*/
});
this.zoomOutButton.events.on("click", () => {
xAxis.zoom(0, 1);
});
let license = false;
for (let i = 0; i < registry.licenses.length; i++) {
if (registry.licenses[i].match(/^AM5G.{5,}/i)) {
license = true;
}
}
if (!license) {
this._root._showBranding();
}
else {
this._root._licenseApplied();
}
}
// end _afterNew()
_prepareChildren() {
super._prepareChildren();
if (this.isDirty("editable")) {
this._toggleEditable(this.get("editable", true));
}
}
clearAll() {
var _a;
this.yAxis.deleteAll();
this._customColor = undefined;
(_a = this.get("colors")) === null || _a === void 0 ? void 0 : _a.reset();
this.colorPickerButton.set("color", undefined);
this._nextColor();
const today = new Date();
today.setHours(0, 0, 0, 0);
this.xAxis.setPrivate("min", today.getTime());
this.xAxis.setPrivate("max", today.getTime() + this.xAxis.baseDuration() * 5);
for (let i = this.xAxisMinor.axisRanges.length - 1; i >= 0; i--) {
const axisRange = this.xAxisMinor.axisRanges.getIndex(i);
if (axisRange) {
this.unmarkDate(axisRange.get("value"));
}
}
;
}
_updateChildren() {
super._updateChildren();
if (this.isDirty("linkNewTasks")) {
this.linkButton.set("active", this.get("linkNewTasks", false));
}
if (this.isDirty("editable")) {
this.editButton.set("active", this.get("editable", true));
}
if (this._sizeDirty || this.isDirty("sidebarWidth")) {
let width = $utils.relativeToValue(this.get("sidebarWidth", percent(20)), this.innerWidth());
width = Math.max(width, this.yAxis.get("minWidth", 100));
this.yAxis.labelsContainer._setC("width", width);
this._updateScrollbar();
}
if (this.isDirty("durationUnit")) {
const durationUnit = this.get("durationUnit");
if (durationUnit) {
this.xAxis._setC("baseInterval", { timeUnit: durationUnit, count: 1 });
this.xAxisMinor._setC("baseInterval", { timeUnit: durationUnit, count: 1 });
}
}
if (this.colorPickerButton.getPrivate("color") === undefined) {
this._nextColor();
}
}
_updateScrollbar() {
const point = this.xyChart.plotContainer.toGlobal({ x: 0, y: 0 });
this.scrollbarX._setCAll({
x: point.x,
width: this.xyChart.plotContainer.width()
});
}
/**
* Adds a new task to the Gantt chart.
*
* @param category The category name for the task. If not provided, will be auto-generated.
* @param start The start time of the task in milliseconds. If not provided, will be determined based on context.
* @param duration The duration of the task. If not provided, defaults to 1.
* @param parentId The parent category if this is a subtask.
* @param progress The initial progress of the task (0-1). Defaults to 0.
* @param linkToPrevious Should the task automatically link to the previous one?
*/
addNewTask(name, start, duration = 1, parentId, progress = 0, linkToPrevious) {
// Generate a unique ID for the new task
let len = this.series.dataItems.length;
let c = this.yAxis.dataItems.length;
$array.each(this.yAxis.dataItems, (dataItem) => {
if (dataItem.get("id") == "gantt_" + c) {
c++;
}
});
const uid = "gantt_" + c;
if (!name) {
name = "New Task";
}
if (linkToPrevious === undefined) {
linkToPrevious = this.get("linkNewTasks", false);
}
let parentDataItem;
// Find parent data item either by specified category or selected item
if (parentId != undefined) {
parentDataItem = this.yAxis.getDataItemById(parentId);
}
else {
parentDataItem = this.yAxis.get("selectedDataItem");
}
let childCount = 0;
// Handle parent-child relationship if a parent exists
if (parentDataItem) {
parentId = parentDataItem.get("id");
if (parentId) {
// If start time not specified, try to use parent's start time
if (start === undefined) {
const seriesDataItem = this.series.getDataItemById(parentId);
if (seriesDataItem) {
start = seriesDataItem.get("valueX", new Date().getTime());
}
}
}
// Initialize or get the children array for the parent
let children = parentDataItem.get("children");
if (!children) {
children = parentDataItem.set("children", []);
}
childCount = children.length;
}
// Determine start time if not specified
if (start === undefined) {
start = $time.roun(this.xAxis.getPrivate("min", 0), this.get("durationUnit", "day"), 1, this.root);
if (this.series.dataItems.length > 0) {
start = this.series.dataItems[len - 1].get("valueX", new Date().getTime());
}
}
const dataObject = {
name: name,
id: uid
};
if (parentId) {
dataObject["parentId"] = parentId;
}
// Add the category data
let index = 0;
let previousDataItem;
if (parentDataItem) {
index = this.yAxis.dataItems.indexOf(parentDataItem) + 1 + childCount;
const children = parentDataItem.get("children", []);
let len = this.yAxis.dataItems.length;
if (index >= len) {
if (children.length > 0) {
previousDataItem = children[children.length - 1];
}
this.yAxis.data.push(dataObject);
}
else {
if (children.length > 0) {
previousDataItem = this.yAxis.dataItems[index - 1];
}
this.yAxis.data.insertIndex(index, dataObject);
}
index++;
}
else {
// go backwards and find the first item that has no parent
for (let i = this.yAxis.dataItems.length - 1; i >= 0; i--) {
const dataItem = this.yAxis.dataItems[i];
if (!dataItem.get("parentId")) {
previousDataItem = dataItem;
break;
}
}
this.yAxis.data.push(dataObject);
index = this.yAxis.dataItems.length;
}
// Add the task data with slightly offset start time to avoid overlapping
const baseDuration = this.xAxis.baseDuration();
const seriesDataObject = {
start: start,
duration: duration,
progress: progress,
id: uid,
name: name
};
// create link to previous task if needed
if (linkToPrevious && previousDataItem) {
const seriesDataItem = this.series.getDataItemById(previousDataItem.get("id"));
if (seriesDataItem) {
const linkTo = seriesDataItem.get("linkTo", []);
if (!linkTo.includes(uid)) {
linkTo.push(uid);
seriesDataItem.set("linkTo", linkTo);
}
}
}
this.series.data.moveValue(seriesDataObject, index - 1);
const newSeriesDataItem = this.series.dataItems[index - 1];
newSeriesDataItem.animate({ key: "valueX", to: start + baseDuration * duration, duration: this.series.get("interpolationDuration", 0), easing: this.series.get("interpolationEasing") });
this.yAxis._disposeXHideDP();
this.yAxis.xButton.hide(0);
const maxZoomCount = this.yAxis.get("maxZoomCount", 20);
let startIndex = Math.max(0, Math.round(index - maxZoomCount / 2));
let endIndex = startIndex + maxZoomCount;
len = this.yAxis.dataItems.length;
if (endIndex > len) {
endIndex = len;
startIndex = Math.max(0, endIndex - maxZoomCount);
}
this.yAxis.zoomToIndexes(startIndex, endIndex);
this.yAxis.adjustZoom();
}
// Get next color from the color set
_nextColor() {
var _a;
if (this._customColor) {
return this._customColor;
}
const fill = (_a = this.get("colors")) === null || _a === void 0 ? void 0 : _a.next();
this.colorPickerButton.setPrivate("color", fill);
return fill;
}
/**
* Marks a date on the minor date axis.
*
* @param date Date to be marked
*/
markDate(date) {
const xAxisMinor = this.xAxisMinor;
const dataItem = xAxisMinor.createAxisRange(xAxisMinor.makeDataItem({
value: date
}));
const eventType = "datemarked";
if (this.events.isEnabled(eventType)) {
this.events.dispatch(eventType, { type: eventType, target: this, dataItem: dataItem, date: date });
}
}
/**
* Unmarks a date on the minor date axis.
*
* @param date Date to be unmarked
*/
unmarkDate(date) {
const xAxisMinor = this.xAxisMinor;
let dataItem = undefined;
xAxisMinor.axisRanges.each((axisRange) => {
// remove axis range if already exists
if (axisRange && axisRange.get("value") === date) {
xAxisMinor.axisRanges.removeValue(axisRange);
dataItem = axisRange;
}
});
const eventType = "dateunmarked";
if (this.events.isEnabled(eventType)) {
this.events.dispatch(eventType, { type: eventType, target: this, dataItem: dataItem, date: date });
}
}
_toggleEditable(value) {
const forceHidden = "forceHidden";
const forceInactive = "forceInactive";
const draggable = "draggable";
this.series.markDirtyValues();
// All of this follows from `editable`, so it is the Gantt's own doing
this.addButton._setC(forceHidden, !value);
this.colorPickerButton._setC(forceHidden, !value);
this.clearButton._setC(forceHidden, !value);
this.linkButton._setC(forceHidden, !value);
const renderer = this.yAxis.get("renderer");
renderer.labels.template._setC(forceInactive, !value);
renderer.grips.template._setC(forceHidden, !value);
renderer.containers.template._setC(draggable, value);
renderer.controlsContainers.template._setC(forceInactive, !value);
this.series.columns.template._setC(draggable, value);
this.series.startGrips.template._setC(forceHidden, !value);
this.series.endGrips.template._setC(forceHidden, !value);
this.series.progressGrips.template._setC(forceHidden, !value);
this.series.startBullets.template._setC(forceHidden, !value);
this.series.endBullets.template._setC(forceHidden, !value);
this.series.links.template._setC(forceInactive, !value);
this.xAxisMinor._setC(forceInactive, !value);
}
}
Gantt.className = "Gantt";
Gantt.classNames = Container.classNames.concat([Gantt.className]);
//# sourceMappingURL=Gantt.js.map