gantt-schedule-timeline-calendar
Gantt, schedule, timeline and calendar components all in one!
gantt-schedule-timeline-calendar is all-in-one component that you can use in different scenarios.
features
- elastic - you can change almost everything from DOM tree to logic (without any compilation, without modyfing original code - with config, state or plugin)
- super fast! even with large dataset
- multiple items in one row - suitable for different usecases like booking, reservation, resource manager etc.
- tree like structures - collapsible / expandable groups
- moveable / resizeable items
- item movement strategies - x, xy, specified row, basically anything you want
- snap to specified time when resizing / moving
- selectable items, rows, grid cells
- you can define your selection strategy and select only what you want
- gradual time zoom up to 5 minutes
- resizeable list columns
- BEM based CSS rules
- you can easily stylize things when data has been changed (items, rows, grid)
- you can easily add third party libraries
- higly configurable
- plugins support - no need to modify original code
- attractive visually
- written in typescript
You can use it in react, vue, angular, svelte or any other projects.
You can use it as schedule for reservation system. You can use it for organizing events. You can use it as gantt chart. Or you can use it as calendar for different purposes.
gantt-schedule-timeline-calendar is very extensible and elastic. You can make your own plugins or modify configuration in couple of ways to achieve your goals.
You can control almost everything. You can change html structure, stylize every html element and even override original components without any compilation stage!
examples
basic, booking or period selector
examples folder
react, angular and vue versions
screenshots
install
npm i gantt-schedule-timeline-calendar
or
<script src="https://cdn.jsdelivr.net/npm/gantt-schedule-timeline-calendar"></script>
usage
Basically you need to create some configuration described below, create state for it and mount component to DOM.
const config = {
};
const state = GSTC.api.stateFromConfig(config);
const app = GSTC({
element: document.getElementById('your-element-id'),
state
});
state
gantt-schedule-timeline-calendar is using deep-state-observer so you can checkout its documentation.
configuration
Your configuration will be merged recursively with default configuration options (default-config) so almost all options are optional.
TIP: most of the time when you need to modify html or add some events you will need to use actions
TIP: take a look at types or default-config
height
{number}
- component height in pixelsheaderHeight
{number}
- height of header and calendar in pixelslist
{object}
- list configurationchart
{object}
- chart configurationlocale
{object}
- locale configurationutcMode
{boolean}
- dayjs UTC mode on / offactions
{object}
- actions can operate directly on HTMLElements
and can be used to add some event listener or inject/modify some html of the componentwrappers
{object}
- wrappers are functions that can wrap any component html - you can wrap component html in div
's or add some html before or aftercomponents
{object}
- object that holds components used inside GSTC
- you can replace any component you wantplugins
{array}
- array of plugins that needs to be initialized before GSTC
plugin
{object}
- this is a container for plugins to store some data
list
rows
Rows are listed on the left side of component (list) and are kind of containers for items (right side - chart).
Rows can contain multiple items.
Rows configuration is an object where key is a row id ({string}
) and value is an object with data you need for columns configuration or for your use case.
Row should have an id inside as id
property {string}
.
Rows can contain also those values:
parentId
{string}
- this is a parent row id for hierarchical data structuresexpanded
{boolean}
- if this row have children should it be expanded or collapsed?
const config = {
list: {
rows: {
'1': {
id: '1'
},
'2': {
id: '2',
parentId: '1'
},
'3': {
id: '3',
parentId: '2',
expanded: true
},
'4': {
id: '4',
parentId: '3'
}
}
}
};
columns
data
{object}
- columns data configurationresizer
{object}
- resizer configurationpercent
{number}
- percentage width of all columns (0 - 100) if 0 list will disappear (from DOM)minWidth
{number}
- default minimal width of the column in pixels
columns data
Columns data configuration is an object where key is an id of the column ({string}
) and value is column configuration.
Column configuration must contain id
property too.
column
id
{string}
- id of the columndata
{string | function}
- for string it is a property name that should exists inside row configuration and will display coresponding value, if data is a function it will be executed with row as argument - that function should return a string or lit-html templateisHTML
{boolean}
- if set to true data
option will be rendered as HTML so be careful and do not let user to inject anything unsafe!width
{number}
- width of the column in pixelsheader
{object}
- column header configurationexpander
{boolean}
- should this column contain expander?
const config = {
list: {
rows: {
'1': {
id: '1',
label: 'Row 1'
},
'2': {
id: '2',
parentId: '1',
label: 'Row 2'
},
'3': {
id: '3',
parentId: '2',
expanded: true,
label: 'Row 3'
},
'4': {
id: '4',
parentId: '3',
label: 'Row 4'
}
},
columns: {
data: {
'label-column-or-whatever': {
id: 'label-column-or-whatever',
data: 'label',
width: 300,
expander: true,
header: {
content: 'Label'
}
},
'some-html': {
id: 'some-html',
isHTML: true,
data: '<div>your html here</div>',
width: 400,
header: {
content: 'anything'
}
}
}
}
}
};
column header
content
{string}
- Label for this headerhtml
{lit-html template}
- lit-html template if you want html
expander
padding
{number}
- left padding size in pixelssize
{number}
- size in pixels (width and height)icon
{object}
- with width
and height
properties in pixels {numbers}
icons
{object}
- expander icons configuration
expander icons
child
{string}
- svg code for non expandable child elementopen
{string}
- svg code for openclosed
{string}
- svg code for closed
toggle
toggle icons
open
{string}
- svg code for openclosed
{string}
- svg code for closed
resizer
width
{number}
- resizer width in pixelsdots
{number}
- number of dots
chart
time
from
{number}
- can be set to limit left side of the chart to specified time in millisecondsto
{number}
- can be set to limit right side of the chart to specified time in millisecondszoom
{number}
- horizontal zoom - lower values for zoom in - values between 10 and 22
items
Items like rows and columns are objects where key is an item id ({string}
) and value is item configuration
item
id
{string}
- item idrowId
{string}
- in which row this item should appearlabel
{string}
- item labeltime
{object}
- item time configurationstyle
{object}
- you can define styling for an item here like {background:'red'}
item time
start
{number}
- start time in millisecondsend
{number}
- end time in milliseconds
const config = {
list: {
rows: {
'1': {
id: '1',
label: 'Row 1'
},
'2': {
id: '2',
parentId: '1',
label: 'Row 2'
}
}
},
chart: {
items: {
'1': {
id: '1',
rowId: '1',
label: 'Item 1',
time: {
start: new Date('2020-01-01').getTime(),
end: new Date('2020-01-02').getTime()
}
},
'2': {
id: '2',
rowId: '2',
label: 'Item 2',
time: {
start: new Date('2020-01-01').getTime(),
end: new Date('2020-01-02').getTime()
}
},
'3': {
id: '3',
rowId: '2',
label: 'Item 3',
time: {
start: new Date('2020-01-03').getTime(),
end: new Date('2020-01-04').getTime()
}
}
}
}
};
locale
Basically locale configuration comes from dayjs locale object
name
{string}
- locale name (en
for example)weekdays
{string[]}
- array of strings with weekdays starting from sunday (Sunday
, Monday
etc)weekdaysShort
{string[]}
- same as weekdays but little bit shorter (Sun
, Mon
...)weekdaysMin
{string[]}
- shortest weekdays (Su
, Mo
...)months
{string[]}
- month names as array of stringsmonthsShort
{string[]}
- shorter month namesweekStart
{number}
- week start number from 0 to 6 where 0 = sunday, 1 = monday
actions
Actions are functions (or classes) that can operate directly on DOM Tree.
With actions you can add additional event listeners, add/update/inject some HTMLElements or add behavior from third party libraries like popups or dialogs.
Action structure is an object where key is component name (kebab-cased) and value is an array of actions that should be fired.
One action is executed on all elements/component instances of specified type.
Available action names:
main
list
list-column
list-column-header
list-column-header-resizer
list-column-header-resizer-dots
list-column-row
list-column-row-expander
list-column-row-expander-toggle
list-toggle
chart
chart-calendar
chart-calendar-date
chart-timeline
chart-timeline-grid
chart-timeline-grid-row
chart-timeline-grid-row-block
chart-timeline-items
chart-timeline-items-row
chart-timeline-items-row-item
Action is a function that is fired when specified DOM Node is created and should return an object with update
and destroy
functions.
Each of the functions takes two arguments:
- Element
{HTMLElement}
- Data
{object}
update
lifecycle method is fired when element stays where it was, but is reused to display another portion of data (performance optimization).
destroy
is fired when element is removed from the DOM tree and component is destroyed.
Example action that will add title
property to each item inside chart.
function addItemTitle(element, data) {
element.title = data.item.label;
return {
update(element, data) {
element.title = data.item.label;
},
destroy(element, data) {
element.title = '';
}
};
}
const config = {
actions: {
'chart-timeline-items-row-item': [addItemTitle]
}
};
Add item click event example:
function clickAction(element, data) {
function onClick(event) {
alert(`Event ${data.item.id} clicked!`);
}
element.addEventListener('click', onClick);
return {
update(element, newData) {
data = newData;
},
destroy(element, data) {
element.removeEventListener('click', onClick);
}
};
}
const config = {
actions: {
'chart-timeline-items-row-item': [clickAction]
}
};
Actions can be classes too - with constructor
, update
and destroy
methods.
class ItemClickAction {
constructor(element, data) {
this.data = data;
this.onClick = this.onClick.bind(this);
element.addEventListener('click', this.onClick);
}
update(element, data) {
this.data = data;
}
destroy(element, data) {
element.removeEventListener('click', this.onClick);
}
onClick(event) {
alert(`Item ${this.data.item.id} clicked!`);
}
}
const config = {
actions: {
'chart-timeline-items-row-item': [ItemClickAction]
}
};
Action example - add class if item has property example
set to true
.
class AddExampleClass{
constructor(element, data) {
this.data = data;
this.element = element;
this.updateExamplelass();
}
update(element, data) {
this.data = data;
this.element = element;
this.updateExampleClass();
}
destroy(element, data) {
element.classList.remove('example-class');
}
updateExampleClass() {
const hasClass = this.element.classList.contains('example-class');
if(this.data.example && !hasClass){
this.element.classList.add('example-class');
}else if(!this.data.example && hasClass){
this.element.classList.remove('example-class');
}
}
}
const config = {
chart:{
items:{
'1': {
id: '1',
time: {},
label: 'with class',
example: true,
},
'2': {
id: '2',
time: {},
label: 'without class',
example: false,
}
}
}
actions: {
'chart-timeline-items-row-item': [AddExampleClass]
}
};
wrappers
gantt-schedule-timeline-calendar is using lit-html from polymer project to easly render templates without compilation stage, so if you want to wrap some GSTC component to add some functionality, you can use html
from lit-html
and wrappers and at the end your code will be much cleaner.
Wrappers are functions that takes TemplateResult
from html
(from lit-html
) and returns wrapped (or not) version.
Wrappers configuration options is an object where key is component name and value is just function so you can use decorator pattern (replace that function) on it.
Available component names:
Main
List
ListColumn
ListColumnHeader
ListColumnHeaderResizer
ListColumnRow
ListColumnRowExpander
ListColumnRowExpanderToggle
ListToggle
Chart
ChartCalendar
ChartCalendarDate
ChartTimeline
ChartTimelineGrid
ChartTimelineGridRow
ChartTimelineGridRowBlock
ChartTimelineItems
ChartTimelineItemsRow
ChartTimelineItemsRowItem
Example that shows how to wrap list column row with div
and additional class.
let oldWrapper;
function addClassWrapper(input, data) {
let result = oldWrapper(input, data);
result = html`
<div class="additional-class">${result}</div>
`;
return result;
}
state.update('config.wrappers', wrappers => {
oldWrapper = wrappers.ListColumnRow;
wrappers.ListColumnRow = addClassWrapper;
return wrappers;
});
components
In gantt-schedule-timeline-calendar you can replace any component with your implementation.
Just copy interesting component - modify it and set up in components
configuration property.
Component configuration is (just like above) object where key is a component name and value is just component itself.
Available component names:
Main
List
ListColumn
ListColumnHeader
ListColumnHeaderResizer
ListColumnRow
ListColumnRowExpander
ListColumnRowExpanderToggle
ListToggle
Chart
ChartCalendar
ChartCalendarDate
ChartTimeline
ChartTimelineGrid
ChartTimelineGridRow
ChartTimelineGridRowBlock
ChartTimelineItems
ChartTimelineItemsRow
ChartTimelineItemsRowItem
Components are functions that takes operational functions vido
and props
as second argument.
Component must return a render function with html
(lit-html).
function ExampleComponent(vido, props) {
const { html, update } = vido;
let name = 'John';
const onClickHandler = event => {
name = 'Jack';
update();
};
return () =>
html`
<div class="example-component" @click=${onClickHanlder}>Hello ${name}</div>
`;
}
plugins
By default there are couple of plugins available:
CalendarScroll plugin
With this plugin you will be able shift / scroll view horizontally by grabbing and moving dates at the top.
options
speed
{number}
default: 1
hideScroll
{boolean}
default: false
- hide gstc bottom scrollbaronChange
{function}
- on change callback onChange(time){/*...*/}
usage
<script src="https://cdn.jsdelivr.net/npm/gantt-schedule-timeline-calendar/dist/CalendarScroll.plugin.js"></script>
or from your local node_modules
dir
<script src="/node_modules/gantt-schedule-timeline-calendar/dist/CalendarScroll.plugin.js"></script>
or
import CalendarScroll from "gantt-schedule-timeline-calendar/dist/CalendarScroll.plugin.js"
const config = {
plugins: [
CalendarScroll({
speed: 1,
hideScroll: true,
onChange(time) {
console.log(time);
}
})
]
};
ItemMovement plugin
With this plugin you will be able to move / resize items.
options
moveable
{boolean | string}
- you can turn off moving capabilities and use just resizing feature, items also might be moveable only along with the specified axis moveable:'x'
, moveable:'y'
resizeable
{boolean}
- should items be resizeable?resizerContent
{string}
- html content of the resizercollisionDetection
{boolean}
- block overlaping items when resizing / movingsnapStart
{function}
(timeStart: number, startDiff: number, item: object) => number;
- function that will return new item time in miliseconds while moving - if you want snap to days - checkout examplesnapEnd
{function}
(timeEnd: number, endDiff: number, item: object) => number
same as above but for end of item api.time.date(timeEnd+endDiff).endOf('day')
ghostNode
{boolean}
- ghost node should be visible?wait
{number}
- sometimes you just want to click an item and sometimes you want to move it, this option will tell gstc to wait some time while mouse button is down to turn on moving mode to prevent accidental item move while clicking - time in miliseconds
You can also add moveable
and resizeable
option to each item so you will be able to block movement / resizing of some items or limit movement / resizing to specified axis.
When you need to move specified item only in some rows you can set item.moveable
to array of row ids item.moveable = ['1','2','5']
.
If you need to add some actions or want to be notified whenever any item is moving or resizing you can subscribe to config.plugin.ItemMovement
like const unsubscribe = GSTCState.subscribe('config.plugin.ItemMovement', itemMovement => { /* do something with info */})
.
const config = {
chart: {
items: {
'1': {
id: '1',
rowId: '1',
moveable: false,
resizeable: false,
label: 'Item 1',
time: {
start: new Date('2020-01-01').getTime(),
end: new Date('2020-01-02').getTime()
}
},
'2': {
id: '2',
rowId: '2',
moveable: ['1'],
resizeable: true,
label: 'Item 2',
time: {
start: new Date('2020-01-01').getTime(),
end: new Date('2020-01-02').getTime()
}
},
'3': {
id: '3',
rowId: '2',
moveable: 'y',
resizeable: true,
label: 'Item 3',
time: {
start: new Date('2020-01-03').getTime(),
end: new Date('2020-01-04').getTime()
}
}
}
}
};
Example of adding style to currently resizing item:
class ResizingItemClassAction {
update(element, data) {
const hasClass = element.classList.contains('resizing');
if (data.item.isResizing && !hasClass) {
element.classList.add('resizing');
} else if (!data.item.isResizing && hasClass) {
element.classList.remove('resizing');
}
}
}
const config = {
actions: {
'chart-timeline-items-row-item': [ResizingItemClassAction]
}
};
let GSTCState = (window.state = GSTC.api.stateFromConfig(config));
GSTCState.subscribe('config.plugin.ItemMovement', itemMovement => {
if (!itemMovement || !itemMovement.item) return;
state.update(`config.chart.items.${itemMovement.item.id}.isResizing`, itemMovement.item.resizing);
});
usage
<script src="https://cdn.jsdelivr.net/npm/gantt-schedule-timeline-calendar/dist/ItemMovement.plugin.js"></script>
or from your local node_modules
dir
<script src="/node_modules/gantt-schedule-timeline-calendar/dist/ItemMovement.plugin.js"></script>
or
import ItemMovement from "gantt-schedule-timeline-calendar/dist/ItemMovement.plugin.js"
const config = {
plugins: [
ItemMovement({
moveable: 'x',
resizerContent: '<div class="resizer">-></div>',
ghostNode: false,
snapStart(time, diff, item) {
return api.time
.date(time)
.add(diff, 'milliseconds')
.startOf('day')
.valueOf();
},
snapEnd(time, diff, item) {
return api.time
.date(time)
.add(diff, 'milliseconds')
.endOf('day')
.valueOf();
}
})
]
};
Selection plugin
With this plugin you can select cells or items and then fire some action.
options
grid
{boolean}
- can we select grid cells?items
{boolean}
- can we select items?rows
{boolean}
- can we select rows?horizontal
{boolean}
- can we select horizontally? (or just vertically if selected)vertical
{boolean}
- can we select vertically? (or just horizontally if selected)rectStyle
{object}
- selecting rectangle style definition as objectselecting
{function}
(data, type: string) => void;
- event callback while selecting - will inform you what is actually selected while selecting process is running (realtime) so you can modify selected cellsdeselecting
{function}
(data, type: string) => void;
- event callback that will inform you what was unselected while selecting process is running - realtimeselected
{function}
(data, type) => void;
- event callback fired when selection process is finished (final event) and something is selected (or not)deselected
{function}
(data, type) => void;
- event callback fired when something previously selected is unselected now (after selection process is finished - final)canSelect
{function}
(type, state, all) => any[];
- can we select this things? should return what should be selected - you can remove what you don't want to selectcanDeselect
{function}
(type, state, all) => any[];
- can we deselect this things? should return what could be unselected - you can filter it out
usage
<script src="https://cdn.jsdelivr.net/npm/gantt-schedule-timeline-calendar/dist/Selection.plugin.js"></script>
or from your local node_modules
dir
<script src="/node_modules/gantt-schedule-timeline-calendar/dist/Selection.plugin.js"></script>
or
import Selection from "gantt-schedule-timeline-calendar/dist/Selection.plugin.js"
const config = {
plugins: [
Selection({
items: false,
rows: false,
grid: true,
rectStyle: { opacity: '0.0' },
canSelect(type, currentlySelecting) {
if (type === 'chart-timeline-grid-row-block') {
return currentlySelecting.filter(selected => {
if (!selected.row.canSelect) return false;
for (const item of selected.row._internal.items) {
if (
(item.time.start >= selected.time.leftGlobal && item.time.start <= selected.time.rightGlobal) ||
(item.time.end >= selected.time.leftGlobal && item.time.end <= selected.time.rightGlobal) ||
(item.time.start <= selected.time.leftGlobal && item.time.end >= selected.time.rightGlobal)
) {
return false;
}
}
return true;
});
}
return currentlySelecting;
},
canDeselect(type, currently, all) {
if (type === 'chart-timeline-grid-row-blocks') {
return all.selecting['chart-timeline-grid-row-blocks'].length ? [] : currently;
}
return [];
},
selecting(data, type) {
},
deselecting(data, type) {
},
selected(data, type) {
},
deselected(data, type) {
}
})
]
};
WeekendHighlight plugin
With this plugin gantt-schedule-timeline-calendar will be able to highlight weekends.
options
weekdays
{number[]}
- array of weekdays we want to highlight where 0 = SundayclassName
{string}
- class that you want to add to highlighet days - gantt-schedule-timeline-calendar__chart-timeline-grid-row-block--weekend
by default
usage
<script src="https://cdn.jsdelivr.net/npm/gantt-schedule-timeline-calendar/dist/WeekendHighlight.plugin.js"></script>
or from your local node_modules
dir
<script src="/node_modules/gantt-schedule-timeline-calendar/dist/WeekendHighlight.plugin.js"></script>
or
import WeekendHighlight from "gantt-schedule-timeline-calendar/dist/WeekendHighlight.plugin.js"
const config = {
plugins: [
WeekendHighlight({
weekdays: [6, 0],
className: 'your-class-for-highlighted-days'
})
]
};
your own plugins - example
will higlight weekends
import { Action } from '@neuronet.io/vido/vido.esm';
export default function WeekendHiglight(options: {}) {
const weekdays = options.weekdays || [6, 0];
let className;
let api;
class WeekendHighlightAction extends Action {
constructor(element, data) {
super();
this.highlight(element, data.time.leftGlobal);
}
update(element, data) {
this.highlight(element, data.time.leftGlobal);
}
highlight(element, time) {
const isWeekend = weekdays.includes(api.time.date(time).day());
const hasClass = element.classList.contains(className);
if (!hasClass && isWeekend) {
element.classList.add(className);
} else if (hasClass && !isWeekend) {
element.classList.remove(className);
}
}
}
return function initialize(vido) {
api = vido.api;
className = options.className || api.getClass('chart-timeline-grid-row-block') + '--weekend';
vido.state.update('config.actions.chart-timeline-grid-row-block', actions => {
actions.push(WeekendHighlightAction);
return actions;
});
};
}
LICENSE
AGPL-3.0 if you are using it - your project must be AGPL-3.0 compatible.
For commercial license please contact me at neuronet.io@gmail.com