Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Poor performance with many items #408

Open
dq-cg opened this issue Nov 6, 2024 · 0 comments
Open

Poor performance with many items #408

dq-cg opened this issue Nov 6, 2024 · 0 comments

Comments

@dq-cg
Copy link

dq-cg commented Nov 6, 2024

Describe the bug
Hey Rafał, I've had reports from a client of two similar issues so I've included them both in the same ticket here. When I refer to the top and bottom sections of the chart below, it is just to indicate that the top section has many items, while the bottom section is mostly empty.

Issue 1.

When many items (100+) are visible on the page at once, the chart stutters and becomes slow to the point of being unusable. This seems exacerbated when many of these items are within the same cell.

Replication steps

  1. Scroll chart up and down over bottom section.
    Chart works fine; see chrome performance profile below.
    image
  2. Scroll chart up and down over top section
    Chart lags and stutters; see chrome performance profile below.
    image
    Note that the pointerMoveWrite function is now taking 550ms to complete instead of the 6-7ms it took in the previous image.
    Looks like the primary cause of this is the recalculateRowHeight, fixOverLappedItems, and itemOverlapsWithOthers functions.

Issue 2.
Attempting to move items within the chart does not work when there are many items visible on the page.

Replication steps

  1. Move item from bottom section to another slot.
    Item movement works as expected.
  2. Try to move item from top section to another slot.
    Item follows cursor for the first few frames, but eventually disappears.

Code
Due to the large number of records I have moved the row and item data into separate files attached to this ticket.
rows and items.zip

/* eslint-disable max-len */
/* eslint-disable no-bitwise */
/* eslint-disable import/extensions */
/* eslint-disable no-use-before-define */
/* eslint-disable no-case-declarations */
/* eslint-disable import/no-unresolved */
/* eslint-disable import/no-absolute-path */

/* globals
	gstc
	moment
	ganttChartKey
*/

import GSTC from '/vendor/gantt-schedule-calendar-timeline/gstc.esm.min.js';
import { Plugin as Selection } from '/vendor/gantt-schedule-calendar-timeline/plugins/selection.esm.min.js';
import { Plugin as ItemMovement } from '/vendor/gantt-schedule-calendar-timeline/plugins/item-movement.esm.min.js';
import { Plugin as CalendarScroll } from '/vendor/gantt-schedule-calendar-timeline/plugins/calendar-scroll.esm.min.js';
import { Plugin as TimelinePointer } from '/vendor/gantt-schedule-calendar-timeline/plugins/timeline-pointer.esm.min.js';

let state;
let currentRowId;
let originalRowId;
let createdCopy;

const gstcApi = GSTC.api;

const getCorrectChartZoom = () => {
	const scrollBar = 20;
	const minimumWidth = 700;
	const minimumZoom = 19.725;
	const fullCalendar = $('#calendarContainer').width();
	const crewColumn = $('.gstc__list-column-header-resizer').width() ?? 200;
	const width = fullCalendar - crewColumn - scrollBar;

	if (width < minimumWidth) {
		return minimumZoom;
	}

	return -24.54442 + (63.90415 - (-24.54442)) / (1 + (width / 720.2646) ** 0.06526076);
};

const generateId = () => {
	let result = '';
	const characters = 'abcdefghijklmnopqrstuvwxyz0123456789';
	const charactersLength = characters.length;

	for (let i = 0; i < 5; i += 1) {
		result += characters.charAt(Math.floor(Math.random() * charactersLength));
	}

	return result;
};

/**
 * GSTC will die if it's supplied an item which
 * specifies a row that doesn't exist...
 * @param {Object} data Calendar state object
 * @returns {Object} Safe calendar state object
 */
const cleanseData = (data) => {
	const rows = Object.values(data.config.list.rows);
	const items = Object.keys(data.config.chart.items);

	for (const item of items) {
		const { rowId } = data.config.chart.items[item];

		if (!rows.find((row) => row.id === rowId)) {
			delete data.config.chart.items[item];
		}
	}

	return data;
};

const updateChartItem = (items) => {
	if (!Array.isArray(items)) {
		items = [items];
	}

	state.update('config.chart.items', (currentItems) => {
		for (const item of items) {
			if (item?.id) {
				currentItems[item.id] = item;
			}
		}

		return currentItems;
	});
};

const onItemMoveEnd = async (items) => {
	try {
		updateChartItem(items.map((item) => ({
			...item,
			isLoading: true
		})));

		// API call to update item in db
	} catch (error) {
		console.error(error);
	}
};

const onCopyClick = async ({ item }) => {
	try {
		const id = item?.id || gstcApi.GSTCID(generateId());

		state.update('config.chart.items', (items) => {
			items[id] = {
				...item,
				id
			};

			return items;
		});

		// API call to create item in db
	} catch (error) {
		console.error(error);
	}
};

const initialiseCalendar = async () => {
	try {
		const gstcConfig = (to, from, rows, columns, items) => {
			const movementConfig = {
				shouldMuteNotNeededMethods: true,
				enabled: true,
				events: {
					// eslint-disable-next-line no-unused-vars
					onStart({ items: { before, targetData }, clickedItem }) {
						currentRowId = targetData.rowId;
						// eslint-disable-next-line no-undef
						originalRowId = targetData.rowId;

						// Old one keeps moving to the new cell, but then we create another one in the initial cell
						createdCopy = GSTC.api.merge({}, targetData, { id: generateId() });
						createdCopy.dependant = [];
						createdCopy.linkedWith = [];

						if (window.isCtrlDown) {
							state.update('config.chart.items', (calItems) => {
								calItems[createdCopy.id] = createdCopy;

								return calItems;
							});
						}

						return before;
					},
					onMove({ items: { before, after } }) {
						if (currentRowId !== after[0].rowId) {
							currentRowId = after[0].rowId;
						}

						return before.map((previousItemState, index) => {
							const { isLoading } = previousItemState;
							const hasLeave = previousItemState.userLeaveId;
							const currentItemState = gstcApi.merge({}, after[index]);
							const isAreaGroupRow = currentItemState.rowId.includes('areaGroup');

							if (hasLeave || isAreaGroupRow) {
								currentItemState.rowId = previousItemState.rowId;
							}

							if (isLoading) {
								currentItemState.rowId = previousItemState.rowId;
								currentItemState.time.end = previousItemState.time.end;
								currentItemState.time.start = previousItemState.time.start;
							}

							return currentItemState;
						});
					},
					onEnd({ items: { after, initial } }) {
						let someItemHasChanged;

						for (const [i, initialItem] of initial.entries()) {
							const movedItem = after[i];

							if (!movedItem) {
								continue;
							}

							if (initialItem.rowId !== movedItem.rowId) {
								someItemHasChanged = true;
								break;
							}

							if (initialItem.time.start !== movedItem.time.start) {
								someItemHasChanged = true;
								break;
							}
						}

						if (someItemHasChanged) {
							/**
							 * For dev: When isCtrl is uncommented, probably need to await onItemMoveEnd before calling onCopyClick to avoid bugs with assigning users
							 */
							onItemMoveEnd(after);

							if (window.isCtrlDown) {
								const userId = createdCopy.rowId.includes('user') ? createdCopy.rowId.split('-')[2] : null;

								onCopyClick({ item: createdCopy }, userId);
							}
						} else {
							/**
							 *  When users holds ctrl and drags elements it created new temp item in original cell.
							 * 	If item is returned into the same cell, we remove the temp item straight away for user experience.
							 * */
							state.update('config.chart.items', (calItems) => {
								delete calItems[createdCopy.id];

								return calItems;
							});
						}

						currentRowId = null;

						return after;
					}
				}
			};

			const zoom = getCorrectChartZoom();

			const plugins = [
				CalendarScroll(),
				TimelinePointer(),
				Selection(),
				ItemMovement(movementConfig)
			];

			return {
				autoInnerHeight: true,
				scroll: {
					vertical: { precise: false, },
					horizontal: { precise: false, },
				},
				chart: {
					item: {
						gap: {
							top: 0,
							bottom: 1,
						},
						height: 20,
					},
					time: {
						to,
						from,
						zoom
					},
					items,
					calendarLevels: [
						[
							{
								zoomTo: 100,
								period: 'month',
								periodIncrement: 1,
								format({ timeStart }) {
									const weekIndex = moment(new Date(timeStart)).isoWeek();

									return `${timeStart.format('MMMM')}, week ${weekIndex}`;
								}
							}
						],
						[{
							main: true,
							zoomTo: 100,
							period: 'day',
							periodIncrement: 1,
							format({ timeStart }) {
								// day.js format
								return timeStart.format('dddd D');
							}

						}]
					]
				},
				list: {
					rows,
					row: { height: 20 },
					columns: { data: gstcApi.fromArray(columns) }
				},
				licenseKey: ganttChartKey.replace(/(\[)|(])|(&quot;)/gm, '').replace(/&#x3D;/gm, '='),
				plugins
			};
		};

		// INSERT DATA FROM rows.js HERE
		const rows = {};
		// INSERT DATA FROM items.js HERE
		const items = {};

		const columns = [
			{
				width: 200,
				id: 'user',
				isHTML: true,
				data: 'label',
				expander: true,
				header: { content: 'User' }
			}
		];

		const to = 1731196800000;
		const from = 1730642400000;

		state = gstcApi.stateFromConfig(gstcConfig(to, from, rows, columns, items));

		const element = $('#calendarContainer')[0];

		element.addEventListener('gstc-loaded', () => {
			gstc.api.scrollToTime(new Date());
			$('#calendarContainer').css('opacity', '1');
			state.update('config.plugin.Selection.cells', false);
		});

		state.data = cleanseData(state.data);

		window.state = state;
		window.gstc = GSTC({
			state,
			element
		});
	} catch (error) {
		console.error(error);
	}
};

initialiseCalendar();

gantt-schedule-timeline-calendar version
3.37.5

Screenshots or movies
Issue 1.
Bottom section (works as expected)
bottom section (good)

Top section (major lag)
top section (bad)

Issue 2.
Bottom section (works as expected)
bottom section moving (good)

Top section (item being moved disappears)
top section moving (bad)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant