Making JavaScript Gantt Workflows Easier to Navigate with a Custom Minimap

DHTMLX Gantt is renowned for its ability to handle large projects containing thousands of tasks without compromising performance. While large datasets are rendered smoothly, end-users may need extra time to get to specific parts of the timeline. That’s where a minimap comes in handy. This small but helpful tool can enhance navigation through complex workflows.

In this tutorial, you will learn how to add a custom minimap to a JavaScript Gantt chart using the API of our Gantt component and pure JavaScript.

Example of a JavaScript Gantt Chart with a Mini Map

Large-scale Gantt charts are notable for extensive task hierarchies and expanded timelines, where some users might get lost in the information maze. In such cases, it is nice to have a navigation tool such as a minimap. It can serve as a compact representation of the entire project (i.e., a bird-eye view of the main Gantt chart), enabling end-users to conveniently look through the whole Gantt chart. To help developers enhance the usability of the JavaScript Gantt component, our team prepared a live sample that vividly demonstrates how a minimap can be added to real Gantt projects.

Check the sample >

In this sample, you can specify any number of tasks for the project and see how convenient it becomes to interact with the Gantt chart. Using the minimap feature, you can quickly scroll through the project and instantly move to specific Gantt sections by simply clicking on them in the minimap.

Step-by-Step Guide on Adding a Minimap to a JS Gantt Chart

Generally speaking, the minimap feature is implemented by adding a second Gantt chart and synchronizing data between them. Now, let’s delve into the details.

Toggling Minimap On and Off

To display the minimap alongside the main Gantt chart, you need to use the addMiniMap() function. It is called inside the toggleMinimap() function, which shows or hides a minimap on a web page. First, the toggleMinimap() function checks if the miniGantt object already exists, and if so, removes the minimap instance and its container from the DOM. If the minimap doesn’t exist (i.e., it is currently hidden), the toggleMinimap() function calls addMiniMap() to generate and render the minimap.

function toggleMinimap() {
    if (miniGantt) {
        miniGantt.destructor();
        miniGantt = null;
        let oldMinimapContainer = document.querySelector("#minimap");
        if (oldMinimapContainer) {
            oldMinimapContainer.parentNode.removeChild(oldMinimapContainer)
        }
    }
    else {
        addMiniMap()
    }
}

This toggle option allows you to show or hide the minimap with a single click, keeping the UI flexible and interactive.

Configuring the Minimap

Inside the addMiniMap() function, you create a new Gantt instance by calling Gantt.getGanttInstance().

function addMiniMap() {
    miniGantt = Gantt.getGanttInstance();
    miniGantt.config.show_task_cells = false;

    miniGantt.config.readonly = true;
    miniGantt.config.drag_progress = false;
    miniGantt.config.show_links = false;
    miniGantt.config.row_height = 1;
    miniGantt.config.min_column_width = 2;
    miniGantt.config.scale_height = 0;

Several configuration settings should be changed to make the minimap more compact and lightweight:

  • Hide task cells and dependencies
  • Disable changing task progress
  • Enable read-only mode
  • Make rows and columns as small as possible
  • Hide the timeline scale (by setting scale_height to 0)

As a result, you get a stripped-down Gantt chart suitable for the use as a minimap.

Setting up Minimap Layout and Tooltips

Before rendering the minimap, you need to define its layout using miniGantt.config.layout. This configuration sets a compact structure with the timeline and scrollbars, excluding the grid panel.

miniGantt.config.layout = {
    css: "gantt_container",
    rows: [
        {
            cols: [
                {
                    view: "timeline",
                    scrollX: "scrollHor",
                    scrollY: "scrollVer"
                },
                {
                    view: "scrollbar",
                    id: "scrollVer"
                }
            ]
        },
        {
            view: "scrollbar",
            id: "scrollHor"
        }
    ]
}

After adding the layout configuration, you can also enable the tooltip extension:

miniGantt.plugins({
    tooltip: true
})

With this feature enabled, end-users will be able to view task details by hovering over a task bar in the minimap.

Setting Up Adaptive Time Scales

Now, it is time to tackle the issue of time scales in the minimap. By default, DHTMLX Gantt uses the “day” unit for the time scale, which will require too much scrolling in the minimap with large projects. The “year” scale could be an option, but it will lead to the impractical use of minimap space. Also, it is not a good fit for scenarios with not so many tasks and a small range of tasks. Therefore, the optimal approach here is to apply Zoom-to-Fit. This feature will automatically select the most appropriate scale (zoom level) in the minimap, depending on the timeline width and the number of tasks.

All Zoom configuration settings are specified in the zoomConfig object:

const zoomConfig = {
    levels: [
        {
            name: "day",
            scales: [
                { unit: "day", step: 1, format: "%d %M" }
            ]
        },
        {
            name: "week",
            scales: [
                { unit: "week", format: "Week #%W" }
            ]
        },
        {
            name: "month",
            scales: [
                { unit: "month", step: 1, format: "%M" },
            ]
        },
        {
            name: "year",
            scales: [
                { unit: "year", step: 1, format: "%Y" }
            ]
        }
    ],
    element: function () {
        return miniGantt.$root.querySelector(".gantt_task");
    }
};

Check the full information about the Zoom extension and its configuration settings in this section of the Gantt documentation.

Implementing Zoom-to-Fit Functionality

You’ve already configured zoom levels and loaded tasks into the minimap; now you can dynamically adjust the timeline view to fit the project’s timespan. For this purpose, you can use the zoomToFit() function.

function zoomToFit() {
    const project = miniGantt.getSubtaskDates(),
        areaWidth = miniGantt.$task.offsetWidth;
    const scaleConfigs = zoomConfig.levels

Here’s how it goes:

  • If the number of cells, multiplied by the minimum timeline column width, fits within the width of the minimap container, the loop breaks, and the current zoom level is selected
  • If not, the loop continues checking the next scale

After passing the loop, you check if the zoom level matches the length of the scale array. If yes, the previous scale is selected.

let zoomLevel = 0;
    for (let i = 0; i < scaleConfigs.length; i++) {
        zoomLevel = i;
        const level = scaleConfigs[i].scales;
        const lowestScale = level[level.length - 1]
        const columnCount = getUnitsBetween(project.start_date, project.end_date, lowestScale.unit, lowestScale.step || 1);

        if ((columnCount + 2) * miniGantt.config.min_column_width <= areaWidth) {
            break;
        }
    }

    if (zoomLevel == scaleConfigs.length) {
        zoomLevel--;
    }

You pass the configuration to the applyConfig function, where you specify the date range and zoom level.

function applyConfig(config, dates) {
    if (dates && dates.start_date && dates.start_date) {
        miniGantt.config.start_date = miniGantt.date.add(dates.start_date, -1, config.scales[0].unit);
        miniGantt.config.end_date = miniGantt.date.add(miniGantt.date[config.scales[config.scales.length - 1].unit + "_start"](dates.end_date), 2, config.scales[0].unit || 1);
    } else {
        miniGantt.config.start_date = miniGantt.config.end_date = null;
    }

    miniGantt.ext.zoom.setLevel(config.name);
}
Creating and Initializing Minimap

The next step is to create a new container, which will host the minimized version of the Gantt chart (i.e. minimap).

const minimapContainer = document.createElement("div");
minimapContainer.id = "minimap";
document.body.appendChild(minimapContainer);

After that, you need to initialize the minimap (second Gantt) by calling the .init(“minimap”), which will attach this “mini Gantt” to the container.

miniGantt.init("minimap");
miniGantt.$container.parentNode.draggable = false;

The second line of code is used to disable the drag-and-drop functionality, thereby preventing accidental text selection within the minimap.

After that, you need to call the zoomToFit() function described in the previous section.

Displaying and Fitting Project Tasks in Minimap

To display the project workflow from the main Gantt chart in the minimap, you should serialize the project data into JSON format and then parse that data into the minimap with miniGantt.parse(). As a result, the data from the main Gantt chart will be duplicated in the minimap.

const ganttTasks = JSON.stringify(gantt.serialize());
miniGantt.parse(ganttTasks);

Depending on the number of tasks and the container’s height, it is necessary to adjust the height of rows with tasks and task bars.

const minimalRowSize = miniGantt.$container.offsetHeight / miniGantt.getTaskCount();
miniGantt.config.bar_height =
    miniGantt.config.row_height = Math.max(Math.floor(minimalRowSize), 1);
Adding a Viewport to the Minimap

Next, you introduce the visual drag element (minimapDrag), which represents the visible area (viewport) of the main Gantt chart inside the minimap. This element is appended to the minimap (miniGantt) container.

const minimapDrag = document.createElement("div");
minimapDrag.className = "minimap_drag";
minimapDrag.draggable = false;
minimapDrag.style.left = "0px";
minimapDrag.style.width = Math.max((gantt.$task.offsetWidth / gantt.$task.scrollWidth * miniGantt.$task.offsetWidth), 20) + "px";
minimapDrag.style.top = "0px";
minimapDrag.style.height = Math.max((gantt.$task.offsetHeight / gantt.$task_bg.scrollHeight * miniGantt.$task.offsetHeight), 20) + "px";
miniGantt.$container.appendChild(minimapDrag);

The drag element size is calculated based on how much of the full Gantt chart is currently visible. A minimum size is set to ensure the drag element stays large enough to easily click and move, even for big projects.

Making the Minimap Interactive

Now that the minimap is fully rendered, you come to the point of implementing interactivity within the minimap by setting up the logic for drag (scroll) behavior. It will take several steps:

1) Adding the mousedown event

Using the mousedown event listener, you determine which element is being clicked on. If it is the minimap (mini Gantt) container, the initial scroll position is stored. If it is the drag handle, you disable all drag handle events for the minimap and enable the dragActualGantt variable.

miniGantt.dragMiniGantt = false;
miniGantt.dragActualGantt = false;
let initialDragPosition = null;
let initialScroll = miniGantt.getScrollState()
window.addEventListener('mousedown', function (e) {
    if (miniGantt) {
        miniGantt.dragMiniGantt = true;
        const minimapElement = miniGantt.utils.dom.closest(e.target, ".minimap_drag");

        if (dragInsideContainer(e, miniGantt.$container)) {
            initialScroll = miniGantt.getScrollState();
        }
        if (minimapElement && dragInsideContainer(e, minimapElement)) {
            miniGantt.dragActualGantt = true;
            minimapDrag.style.pointerEvents = "none";
        }
    }
});
2) Adding the mouseup event

With the mouseup event listener, you reset all the variables, disable the ability to drag tasks, and return mouse events to the drag handle.

window.addEventListener('mouseup', function (e) {
    if (miniGantt) {
        miniGantt.dragMiniGantt = false;
        miniGantt.dragActualGantt = false;
        initialDragPosition = null;
        minimapDrag.style.pointerEvents = "all";
    }
});
3) Adding the onMouseMove event

The onMouseMove listener is responsible for the actual dragging (scrolling) operation. It is added not to the window object, but to the minimap (miniGantt object).

miniGantt.attachEvent("onMouseMove", function (id, e) {
4) Setting the minimap’s drag handle position

If the dragActualGantt variable is enabled, you check the mouse position relative to the mini Gantt (minimap) timeline and retrieve the coordinates. After that, you subtract half of the mini drag’s width and height from these coordinates to place the drag handle centered on the mouse position.

if (miniGantt.dragActualGantt) {
    const position = miniGantt.utils.dom.getRelativeEventPosition(e, miniGantt.$task_data);
    const taskRow = miniGantt.utils.dom.closest(e.target, "[data-task-id]")

    if (taskRow) {
        const relativePosition = miniGantt.utils.dom.getRelativeEventPosition(e, miniGantt.$container);

        miniGantt.dragPositionX = relativePosition.x - minimapDrag.offsetWidth / 2;
        miniGantt.dragPositionY = relativePosition.y - minimapDrag.offsetHeight / 2;

For the X coordinate, you convert the mouse position in the mini-Gantt into a date, then calculate the corresponding horizontal position for this date in the main Gantt. For the Y coordinate, you get the vertical position of the corresponding task in the main Gantt.

const id = taskRow.dataset.taskId;
const x = gantt.posFromDate(miniGantt.dateFromPos(position.x)) - gantt.$task.offsetWidth / 3;
const y = gantt.getTaskPosition(gantt.getTask(id)).top - gantt.$task.offsetHeight / 3;

After that, the drag’s position in the minimap (miniGantt) is updated to follow the drag position, and both the minimap and the main Gantt charts are scrolled to the calculated coordinates.

minimapDrag.style.left = (miniGantt.dragPositionX) + "px";
minimapDrag.style.top = (miniGantt.dragPositionY) + "px";

gantt.scrollTo(x, y);
5) Auto-scrolling the minimap while dragging

Using the scrollMiniGantt() function, you check whether the current drag position has reached or crossed the minimap borders (based on the predefined offset threshold). If so, you scroll the minimap using the scrollTo() method. After a timeout, the function is called again to check whether further scrolling is needed.

scrollMiniGantt(miniGantt.dragPositionX, miniGantt.dragPositionY)

function scrollMiniGantt() {
    if (!miniGantt) {
        return
    }

    const newScrollPosition = miniGantt.getScrollState();
    const leftBorder = miniGantt.dragPositionX - scrollOffset / 2;
    const rightBorder = miniGantt.dragPositionX - (miniGantt.$task.offsetWidth - scrollOffset);
    const topBorder = miniGantt.dragPositionY - scrollOffset / 2;
    const bottomBorder = miniGantt.dragPositionY - (miniGantt.$task_data.offsetHeight - scrollOffset);

    let shouldScroll = false;
    if (leftBorder < 0) {
        newScrollPosition.x -= miniGantt.$task_data.scrollWidth / scrollOffset;
        shouldScroll = true;
    }
    if (rightBorder > 0) {
        newScrollPosition.x += miniGantt.$task_data.scrollWidth / scrollOffset;
        shouldScroll = true;
    }
    if (topBorder < 0) {
        newScrollPosition.y -= miniGantt.$task_data.scrollHeight / scrollOffset;
        shouldScroll = true;
    }
    if (bottomBorder > 0) {
        shouldScroll = true;
        newScrollPosition.y += miniGantt.$task_data.scrollHeight / scrollOffset;
    }

    if (shouldScroll) {
        miniGantt.scrollTo(newScrollPosition.x, newScrollPosition.y);
        setTimeout(function () {
            scrollMiniGantt()
        }, 4)
    }
}
6) Dragging the minimap itself

If the dragMiniGantt variable is enabled, it means the minimap itself is being dragged, not the drag handle. In this case, you check whether the drag handle is still inside the minimap container using the dragInsideContainer function.

else if (miniGantt.dragMiniGantt && dragInsideContainer(e, miniGantt.$container)) {

Then, you determine the initial drag position and calculate the dragged distance (expressed as a percentage) relative to the width and height of the minimap container.

const dragPos = miniGantt.utils.dom.getRelativeEventPosition(e, miniGantt.$container);
initialDragPosition = initialDragPosition || dragPos;
const percentX = (initialDragPosition.x - dragPos.x) / miniGantt.$container.scrollWidth;
const percentY = (initialDragPosition.y - dragPos.y) / miniGantt.$container.scrollHeight;

const positionPercentX = percentX * miniGantt.$task_bg.scrollWidth;
const positionPercentY = percentY * miniGantt.$task_bg.scrollHeight;

After that, you add the percentage values ​​to the initial scroll position and scroll the minimap using the scrollTo() method.

const newPositionX = initialScroll.x + positionPercentX
const newPositionY = initialScroll.y + positionPercentY

miniGantt.scrollTo(newPositionX, newPositionY)

It might seem that the drag_timeline extension would help do the same job. It would work, but not the way you need. The scrolling would be calculated by coordinates (not by percentage values), i.e. in very small steps.

7) Adding click events

Also, you need to add a couple of click events for interacting with the minimap. First is the onEmptyClick event, which will fire after a click on an empty space in the Gantt chart (not on tasks) in the minimap. The main Gantt chart will scroll to the same coordinates. You get these coordinates in the same way as for the case described above, when you dragged the minimap drag handle.

miniGantt.attachEvent("onEmptyClick", function (e) {
    const position = miniGantt.utils.dom.getRelativeEventPosition(e, miniGantt.$task_data);
    const taskRow = miniGantt.utils.dom.closest(e.target, "[data-task-id]")
    if (taskRow) {
        const id = taskRow.dataset.taskId;
        const y = gantt.getTaskPosition(gantt.getTask(id)).top;
        const x = gantt.posFromDate(miniGantt.dateFromPos(position.x));
        gantt.scrollTo(x, y);
    }
});

In addition, you need to add the onTaskClick event for the minimap. When a task is clicked in the minimap, the corresponding task is brought into view in the main Gantt chart using the showTask() method.

miniGantt.attachEvent("onTaskClick", function (id, e) {
    gantt.showTask(id);
});

That’s it. Now, your minimap fully supports dragging, smooth scrolling, and click-based navigation.

Synchronizing the Main Gantt Chart and Minimap

The next major step is to synchronize the minimap and the main Gantt chart so that both views stay aligned and always show the same part of the project.

To do that, you need to add several event listeners, such as onAfterTaskAdd, onAfterTaskDelete, and onAfterTaskUpdate. Using these listeners, you will trigger corresponding actions in the minimap whenever tasks are added, deleted, or updated in the main Gantt chart.

gantt.attachEvent("onAfterTaskAdd", function (id, task) {
    if (miniGantt) {
        miniGantt.addTask(task)
    }
});
gantt.attachEvent("onAfterTaskDelete", function (id, task) {
    if (miniGantt) {
        miniGantt.deleteTask(id)
    }
});
gantt.attachEvent("onAfterTaskUpdate", function (id, task) {
    if (miniGantt) {
        miniGantt.updateTask(id, task)
    }
});

After that, you add the onGanttScroll listener, which updates the minimap’s scroll position whenever the main Gantt is scrolled.

gantt.attachEvent("onGanttScroll", function (left, top) {
    if (miniGantt && !miniGantt.dragActualGantt) {

To obtain the scroll coordinates from the main Gantt, you first take the full width and height of its timeline, and calculate how far the current scroll position is as a percentage of these dimensions. Then, you calculate two coordinate points in the minimap that represent the currently visible area in the main Gantt chart:

  • (x1, y1) – the top-left point of the visible area
  • (x2, y2) – the bottom-right point of the visible area
const scroll = gantt.getScrollState();

const fullWidth = gantt.$task_data.scrollWidth;
const fullHeight = gantt.$task_data.scrollHeight;

const scrollX1Percent = scroll.x / fullWidth;
const scrollY1Percent = scroll.y / fullHeight;

const x1 = miniGantt.posFromDate(gantt.dateFromPos(scroll.x));
const y1 = miniGantt.$task_data.scrollHeight * scrollY1Percent;

const scrollX2Percent = (scroll.x + gantt.$task.offsetWidth) / fullWidth;
const scrollY2Percent = (scroll.y + gantt.$task.offsetHeight) / fullHeight;

const x2 = miniGantt.posFromDate(gantt.dateFromPos(scroll.x + gantt.$task.offsetWidth));
const y2 = miniGantt.$task_data.scrollHeight * scrollY2Percent;

The next step is to get the current scroll position of the minimap timeline and calculate the visible area boundaries. Then, you check if the Gantt’s visible area (represented by x1 and y1) is inside this minimap viewport. If it’s not, you scroll the minimap to bring it into view.

miniGantt.utils.dom.getNodePosition(miniGantt.$task_data)

const miniGanttLeft = miniGantt.$task.scrollLeft;
const miniGanttRight = miniGantt.$task.scrollLeft + miniGantt.$task.offsetWidth;
const miniGanttTop = miniGantt.$task_data.scrollTop;
const miniGanttBottom = miniGantt.$task_data.scrollTop + miniGantt.$task.offsetHeight;

const xInsideViewport = miniGanttLeft <= x1 && x1 <= miniGanttRight;
const yInsideViewport = miniGanttTop <= y1 && y1 <= miniGanttBottom;

if (!xInsideViewport || !yInsideViewport) {
    miniGantt.scrollTo(x1, y1);
}

It remains only to repeat a similar check for the minimap drag handle to ensure it stays correctly aligned with the visible area of the main Gantt chart.

let minimapDrag = miniGantt.$container.querySelector(".minimap_drag");

const miniMapDragLeft = minimapDrag.offsetLeft;
const miniMapDragRight = minimapDrag.offsetLeft + minimapDrag.offsetWidth;
const miniMapDragTop = minimapDrag.offsetTop;
const miniMapDragBottom = minimapDrag.offsetTop + minimapDrag.offsetHeight;

const x1InsideDrag = miniMapDragLeft <= x1 && x1 <= miniMapDragRight;
const y1InsideDrag = miniMapDragTop <= y1 && y1 <= miniMapDragBottom;
const x2InsideDrag = miniMapDragLeft <= x2 && x2 <= miniMapDragRight;
const y2InsideDrag = miniMapDragTop <= y2 && y2 <= miniMapDragBottom;

if (!x1InsideDrag || !y1InsideDrag || !x2InsideDrag || !y2InsideDrag) {
    minimapDrag.style.left = (x1 - miniGantt.$task_data.scrollWidth / scrollOffset - miniGantt.$task.scrollLeft) + "px";
    minimapDrag.style.top = (y1 - miniGantt.$task_data.scrollTop) + "px";
}
Final Touch

The last thing to do is to add styles for the minimap and the drag handle, as well as the minimum width for the task bars:

.gantt_bar_task{
    min-width: 1px;
}

#minimap {
    position: fixed;
    bottom: 15px;
    right: 15px;
    width: 200px;
    height: 200px;
    opacity: 0.7;
    z-index: 1;
}

#minimap .gantt_row,
#minimap .gantt_task_row {
    border-bottom: 0;
}

#minimap .minimap_drag {
    background: cornflowerblue;
    opacity: 0.5;
    width: 20px;
    height: 20px;
    position: absolute;
    top: 0px;
}

By following the steps above, you can successfully add a fully functional minimap to your Gantt chart like in this DHTMLX sample.

Wrapping Up

In this tutorial, we covered how to create the minimap, synchronize it with the main JavaScript Gantt chart, and handle user interactions, such as dragging, clicking, and scrolling. The minimap enables end-users to quickly navigate through large projects, improving usability and user experience. This approach can be further customized depending on your project needs using the extensive Gantt API. Having doubts? Just download a free 30-day trial version of our Gantt component and give it a test run.

Related Materials

Advance your web development with DHTMLX

Gantt chart
Event calendar
Diagram library
30+ other JS components