Appearance
GanttChart Component Code
Dependencies
This component requires:
- Vue 3 with Composition API
- Pinia for state management
- D3.js for SVG rendering (
d3-selection,d3-scale,d3-axis,d3-time,d3-drag) - Moment.js for date manipulation
OMenucomponent for dropdown menus- Composables:
useGanttZoomPan,useGanttDatePicker,useGanttContextMenu,useGanttLazyLoading - Store:
ganttChart.store.js
Component Structure
The GanttChart component is organized into several files:
src/
├── components/
│ └── GanttChart/
│ └── GanttChart.vue # Main component (5023 lines)
├── composables/
│ ├── useGanttZoomPan.js # Zoom and pan functionality
│ ├── useGanttDatePicker.js # Date picker management
│ ├── useGanttContextMenu.js # Context menu operations
│ └── useGanttLazyLoading.js # Lazy loading logic
└── stores/
└── ganttChart.store.js # Pinia store for state managementMain Component
The main GanttChart component is quite large (5023 lines). Here's the template structure:
vue
<template>
<div class="gantt-wrapper">
<!-- Header with timeline -->
<div class="gantt-header" ref="headerRef">
<svg class="chart-header-svg" ref="headerSvgRef"></svg>
</div>
<!-- Main content area with task bars -->
<div
class="gantt-content"
ref="containerRef"
@mousemove="handleMouseMoveGanttChart"
@mouseleave="showCursorToolTip = false"
>
<svg ref="svgRef" class="gantt-chart-svg"></svg>
</div>
<!-- Add milestone tooltip -->
<div
v-if="showCursorToolTip"
class="add-milestone-pointer-tooltip"
:style="cursorTooltipPosition"
>
<svg><!-- Plus icon --></svg>
Add Milestone
</div>
<!-- Zoom and pan controls -->
<div class="zoom-controls-pan-view-button-wrapper">
<!-- View mode selector -->
<OMenu ref="viewModeMenu" placement="bottom-start">
<template #trigger>
<div class="button-wrapper view-mode-button">
<span class="view-mode-text">{{ currentViewModeLabel }}</span>
</div>
</template>
<template #content>
<div class="view-mode-menu-content">
<div
v-for="mode in viewModes"
:key="mode.value"
class="view-mode-menu-item"
@click="selectViewMode(mode.value)"
>
{{ mode.label }}
</div>
</div>
</template>
</OMenu>
<!-- Zoom controls -->
<div class="zoom-controls-wrapper">
<div class="button-wrapper" @click="onClickZoomControl('minus')">
<!-- Minus icon -->
</div>
<div class="button-wrapper" @click="onClickZoomControl('plus')">
<!-- Plus icon -->
</div>
</div>
<!-- Pan view button -->
<div
class="pan-view-button-wrapper"
:class="{ 'is-active': isPanModeActive }"
@click="onClickPanViewButton"
>
<!-- Hand icon -->
</div>
</div>
<!-- Project start date button -->
<div class="start-project-schedule-buttons-wrapper">
<div class="button-wrapper" @click="onClickProjectScheduleDateButton">
<!-- Arrow icon -->
</div>
</div>
<!-- Date picker (invisible trigger positioned dynamically) -->
<div
ref="datePickerTrigger"
class="date-picker-trigger"
:style="datePickerTriggerStyle"
>
<OMenu
ref="datePickerMenu"
:placement="datePickerPlacement"
:close-on-outside-click="true"
>
<template #trigger>
<div style="width: 1px; height: 1px"></div>
</template>
<template #content>
<!-- Custom date picker implementation -->
<div class="date-picker-content">
<!-- Date picker UI would go here -->
</div>
</template>
</OMenu>
</div>
<!-- Context menu (invisible trigger positioned dynamically) -->
<div
ref="contextMenuTrigger"
class="context-menu-trigger"
:style="contextMenuTriggerStyle"
>
<OMenu
ref="contextMenuMenu"
:placement="contextMenuPlacement"
:close-on-outside-click="true"
>
<template #trigger>
<div style="width: 1px; height: 1px"></div>
</template>
<template #content>
<div class="context-menu-content">
<div
v-for="item in contextMenuItems"
:key="item._id"
class="context-menu-item"
@click="onContextMenuSelect(item)"
>
{{ item.name }}
</div>
</div>
</template>
</OMenu>
</div>
</div>
</template>Script Setup Overview
vue
<script setup>
import { ref, computed, watch, onMounted, onUnmounted, nextTick } from "vue";
import { storeToRefs } from "pinia";
import * as d3 from "d3";
import moment from "moment";
import OMenu from "./OMenu.vue";
// Composables
import { useGanttZoomPan } from "@/composables/useGanttZoomPan";
import { useGanttDatePicker } from "@/composables/useGanttDatePicker";
import { useGanttContextMenu } from "@/composables/useGanttContextMenu";
import { useGanttLazyLoading } from "@/composables/useGanttLazyLoading";
// Store
import { useGanttChart } from "@/stores/ganttChart.store";
// Props
const props = defineProps({
viewMode: {
type: String,
default: "daily",
validator: (val) => ["daily", "weekly", "monthly"].includes(val),
},
});
// Emits
const emit = defineEmits(["viewModeChanged"]);
// Store
const ganttChartStore = useGanttChart();
const { items, phasesList, ganttBoundaryStartDate, ganttBoundaryEndDate } =
storeToRefs(ganttChartStore);
// Refs
const containerRef = ref(null);
const headerRef = ref(null);
const svgRef = ref(null);
const headerSvgRef = ref(null);
// Initialize composables
const zoomPanComposable = useGanttZoomPan({
containerRef,
onScroll: handleScroll,
onZoomChange: handleZoomChange,
shouldIgnoreKeyEvent: (e) => e.target.tagName === "INPUT",
});
const datePickerComposable = useGanttDatePicker();
const contextMenuComposable = useGanttContextMenu();
const lazyLoadingComposable = useGanttLazyLoading({
containerRef,
fetchItemsAPI: null, // Optional API function
});
// D3 scales, rendering logic, event handlers...
// (Full implementation in GanttChart/GanttChart.vue)
onMounted(() => {
initializeChart();
zoomPanComposable.setupContainerInteractions();
zoomPanComposable.setupWindowListeners();
lazyLoadingComposable.setupHorizontalObserver();
lazyLoadingComposable.setInitialFetchedRange(
ganttBoundaryStartDate.value,
ganttBoundaryEndDate.value
);
});
onUnmounted(() => {
zoomPanComposable.cleanup();
datePickerComposable.cleanup();
lazyLoadingComposable.cleanup();
});
</script>Composables
useGanttZoomPan.js
js
import { ref, nextTick, watch } from "vue";
const MIN_ZOOM = 0.5;
const MAX_ZOOM = 2;
const ZOOM_SENSITIVITY = 0.0012;
export const useGanttZoomPan = (options = {}) => {
const { containerRef, onScroll, onZoomChange, shouldIgnoreKeyEvent } = options;
const zoomFactor = ref(1);
const isSpacePressed = ref(false);
const isPanModeActive = ref(false);
const isPanning = ref(false);
// Pan and zoom logic...
// (See full implementation in useGanttZoomPan.js)
return {
zoomFactor,
isSpacePressed,
isPanModeActive,
isPanning,
onClickZoomControl,
onClickPanViewButton,
setupContainerInteractions,
teardownContainerInteractions,
setupWindowListeners,
cleanup,
};
};useGanttDatePicker.js
js
import { ref, computed, nextTick } from "vue";
import { useGanttChart } from "@/stores/ganttChart.store";
import moment from "moment";
const MENU_HEIGHT = 300;
const SPACING = 8;
const DATE_FORMAT = "YYYY-MM-DD";
export const useGanttDatePicker = () => {
const ganttChartStore = useGanttChart();
const datePickerMenu = ref(null);
const datePickerTrigger = ref(null);
const datePickerValue = ref(null);
const currentEditingItem = ref(null);
const currentDateType = ref(null);
// Date picker logic...
// (See full implementation in useGanttDatePicker.js)
return {
datePickerMenu,
datePickerTrigger,
datePickerValue,
currentEditingItem,
currentDateType,
openDatePicker,
closeDatePicker,
handleDateChange,
cleanup,
};
};useGanttContextMenu.js
js
import { ref, nextTick } from "vue";
import { useGanttChart } from "@/stores/ganttChart.store";
export const useGanttContextMenu = () => {
const ganttChartStore = useGanttChart();
const contextMenuMenu = ref(null);
const contextMenuTrigger = ref(null);
const contextMenuItem = ref(null);
const contextMenuItems = ref([
{ _id: 1, name: "Edit" },
{ _id: 2, name: "Add Sub item" },
{ _id: 3, name: "Delete" },
{ _id: 4, name: "Duplicate Item" },
]);
// Context menu logic...
// (See full implementation in useGanttContextMenu.js)
return {
contextMenuMenu,
contextMenuTrigger,
contextMenuItem,
contextMenuItems,
openContextMenu,
closeContextMenu,
onContextMenuSelect,
};
};useGanttLazyLoading.js
js
import { ref, onUnmounted } from "vue";
import { storeToRefs } from "pinia";
import moment from "moment";
import { useGanttChart } from "@/stores/ganttChart.store";
const DAYS_TO_FETCH = 30;
const DATE_FORMAT = "YYYY-MM-DD";
export const useGanttLazyLoading = (options = {}) => {
const { containerRef, fetchItemsAPI } = options;
const ganttChartStore = useGanttChart();
const { ganttBoundaryStartDate, ganttBoundaryEndDate, items } =
storeToRefs(ganttChartStore);
const isLoading = ref(false);
const horizontalObserver = ref(null);
const fetchedDateRanges = ref(new Set());
// Lazy loading logic...
// (See full implementation in useGanttLazyLoading.js)
return {
isLoading,
setupHorizontalObserver,
observeDateIntervals,
setInitialFetchedRange,
cleanup,
};
};Store (ganttChart.store.js)
js
import { defineStore } from "pinia";
export const useGanttChart = defineStore("ganttChart", {
state: () => ({
items: [],
phasesList: [
{
_id: "1234567890",
name: "Planning",
colour: "#3098f2",
textColor: "#ffffff",
},
{
_id: "1234567891",
name: "Design",
colour: "#8B5CF6",
textColor: "#ffffff",
},
{
_id: "1234567892",
name: "Development",
colour: "#10b981",
textColor: "#ffffff",
},
{
_id: "1234567893",
name: "Testing",
colour: "#f59e0b",
textColor: "#ffffff",
},
{
_id: "1234567894",
name: "Deployment",
colour: "#ef4444",
textColor: "#ffffff",
},
],
ganttBoundaryStartDate: null,
ganttBoundaryEndDate: null,
projectScheduleDate: null,
}),
actions: {
updateItemTime(itemId, startDate, dueDate) {
const item = this.items.find((i) => i._id === itemId);
if (!item) return;
if (startDate) item.startDate = new Date(startDate).toISOString();
if (dueDate) item.dueDate = new Date(dueDate).toISOString();
},
// Additional actions...
},
});Full Component Files
Due to the size and complexity of the GanttChart component, the complete implementation is available in the source files:
- Main Component:
src/components/GanttChart/GanttChart.vue(5023 lines) - Zoom/Pan Composable:
src/composables/useGanttZoomPan.js(290 lines) - Date Picker Composable:
src/composables/useGanttDatePicker.js(159 lines) - Context Menu Composable:
src/composables/useGanttContextMenu.js(120 lines) - Lazy Loading Composable:
src/composables/useGanttLazyLoading.js(250 lines) - Store:
src/stores/ganttChart.store.js(447 lines)
Key Implementation Details
D3.js Integration
The component uses D3.js for:
- Time scales for X-axis positioning
- SVG rendering and manipulation
- Drag-and-drop interactions
- Smooth transitions and animations
Date Handling
All dates are handled using Moment.js for:
- Date formatting and parsing
- Date range calculations
- View mode granularity (daily/weekly/monthly)
- Timeline interval generation
State Management
Pinia store manages:
- Task items and their properties
- Project phases and colors
- Timeline boundaries
- Shared state across components
Performance Optimizations
- Lazy loading for large datasets
- Virtual rendering of visible items only
- Debounced drag operations
- Efficient D3 data joins
- IntersectionObserver for horizontal lazy loading
Accessibility
- Keyboard navigation (Space, Escape)
- ARIA labels on interactive elements
- Focus management for popups
- Screen reader support
Styling
The component uses SCSS for styling with:
- BEM-like class naming
- CSS variables for theming
- Responsive design patterns
- Custom scrollbar styling
- Smooth transitions
Usage Example
vue
<template>
<div class="app-container">
<GanttChart :viewMode="currentViewMode" @viewModeChanged="handleViewModeChange" />
</div>
</template>
<script setup>
import { ref, onMounted } from "vue";
import GanttChart from "@/components/GanttChart/GanttChart.vue";
import { useGanttChart } from "@/stores/ganttChart.store";
const currentViewMode = ref("weekly");
const ganttStore = useGanttChart();
onMounted(() => {
// Initialize store with data
ganttStore.items = [
// ... your task data
];
ganttStore.ganttBoundaryStartDate = "2024-01-01";
ganttStore.ganttBoundaryEndDate = "2024-12-31";
});
const handleViewModeChange = ({ mode }) => {
currentViewMode.value = mode;
console.log("View mode changed to:", mode);
};
</script>
<style scoped>
.app-container {
width: 100%;
height: 100vh;
padding: 1rem;
}
</style>