Skip to content

Commit 22abbbc

Browse files
committed
feat: Gantt performance update
1 parent c90839c commit 22abbbc

3 files changed

Lines changed: 147 additions & 97 deletions

File tree

src/components/gantt/Gantt.tsx

Lines changed: 67 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -17,74 +17,93 @@ export const Gantt: React.FC<GanttProps> = (props) => {
1717

1818
const {items, stepWidth = "50px", rowHeight = "50px", step = 1, children} = props
1919

20-
const groups: {
21-
step: number
22-
items: GanttItem[]
23-
}[] = []
20+
const {targetItems, targetStep, minStart} = React.useMemo(() => {
21+
const groups: {
22+
step: number
23+
items: GanttItem[]
24+
}[] = []
2425

25-
items?.forEach(item => {
26+
items?.forEach(item => {
2627

27-
const itemDuration = item.end - item.start
28-
let itemAdded = false
28+
const itemDuration = item.end - item.start
29+
let itemAdded = false
2930

30-
if (groups.length <= 0) {
31-
groups.push({step: itemDuration, items: [item]})
32-
return
33-
}
31+
if (groups.length <= 0) {
32+
groups.push({step: itemDuration, items: [item]})
33+
return
34+
}
3435

3536

36-
groups.forEach((group) => {
37-
const newStep = (group.step + itemDuration) / 1.75
38-
const lowerBound = (group.step / 3) - 10
39-
const upperBound = (group.step * 3) + 10
37+
for (let i = 0; i < groups.length; i++) {
38+
const group = groups[i]
39+
const newStep = (group.step + itemDuration) / 1.75
40+
const lowerBound = (group.step / 3) - 10
41+
const upperBound = (group.step * 3) + 10
4042

41-
if (lowerBound < itemDuration && itemDuration < upperBound && !itemAdded) {
42-
group.step = newStep
43-
group.items.push(item)
44-
itemAdded = true
43+
if (lowerBound < itemDuration && itemDuration < upperBound) {
44+
group.step = newStep
45+
group.items.push(item)
46+
itemAdded = true
47+
break
48+
}
4549
}
50+
51+
if (!itemAdded) {
52+
groups.push({step: itemDuration, items: [item]})
53+
}
54+
4655
})
4756

48-
if (!itemAdded) {
49-
groups.push({step: itemDuration, items: [item]})
50-
}
57+
groups.sort((a, b) => b.step - a.step).forEach((group, index) => {
5158

52-
})
53-
54-
groups.sort((a, b) => b.step - a.step).forEach((group, index) => {
55-
56-
if (index > 0) {
57-
const minStart = Math.min(...group.items.map(item => item.start))
58-
const maxEnd = Math.max(...group.items.map(item => item.end))
59-
groups[0].items.push({
60-
start: minStart,
61-
end: (maxEnd + ((groups[0].step * step) / 6)),
62-
id: `group-source-${index}`,
63-
type: "group",
64-
data: {
65-
items: group.items,
66-
step: step,
67-
firstGroupStep: groups[0].step,
68-
groupStep: group.step,
69-
displayMessage: `${index}`,
70-
color: hashToColor(`group-source-${index}`),
59+
if (index > 0) {
60+
let minStart = Infinity
61+
let maxEnd = -Infinity
62+
for (let i = 0; i < group.items.length; i++) {
63+
const it = group.items[i]
64+
if (it.start < minStart) minStart = it.start
65+
if (it.end > maxEnd) maxEnd = it.end
7166
}
72-
})
73-
}
67+
groups[0].items.push({
68+
start: minStart,
69+
end: (maxEnd + ((groups[0].step * step) / 6)),
70+
id: `group-source-${index}`,
71+
type: "group",
72+
data: {
73+
items: group.items,
74+
step: step,
75+
firstGroupStep: groups[0].step,
76+
groupStep: group.step,
77+
displayMessage: `${index}`,
78+
color: hashToColor(`group-source-${index}`),
79+
}
80+
})
81+
}
82+
83+
})
7484

75-
})
85+
let minStart = Infinity
86+
const firstItems = groups[0].items
87+
for (let i = 0; i < firstItems.length; i++) {
88+
if (firstItems[i].start < minStart) minStart = firstItems[i].start
89+
}
7690

77-
const minStart = Math.min(...groups[0].items.map(item => item.start))
91+
return {
92+
targetItems: firstItems,
93+
targetStep: groups[0].step,
94+
minStart,
95+
}
96+
}, [items, step])
7897

7998
return (
8099
<ScrollArea w={"100%"} h={"100%"} type={"hover"}>
81100
<ScrollAreaViewport>
82101
<GanttGroup children={children}
83102
id={`group-target`}
84103
hideScaling={false}
85-
start={minStart - (((minStart / (groups[0].step * step)) * (groups[0].step * step)))}
86-
step={groups[0].step * step}
87-
stepWidth={stepWidth} rowHeight={rowHeight} items={groups[0].items}
104+
start={minStart - (((minStart / (targetStep * step)) * (targetStep * step)))}
105+
step={targetStep * step}
106+
stepWidth={stepWidth} rowHeight={rowHeight} items={targetItems}
88107
key={`group-target`}/>
89108
</ScrollAreaViewport>
90109
<ScrollAreaScrollbar orientation={"horizontal"}>

src/components/gantt/GanttGroup.tsx

Lines changed: 58 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -36,20 +36,37 @@ export const GanttGroup: React.FC<GanttGroupProps> = (props) => {
3636
const [activeGroup, setActiveGroup] = React.useState<string | undefined>(undefined)
3737

3838
// Parse stepWidth to pixels
39-
const stepWidthPx = parseInt(stepWidth as string)
39+
const stepWidthPx = React.useMemo(() => parseInt(stepWidth as string), [stepWidth])
4040

4141
// Timeline calculations
4242
const timeRange = end - start
4343
const timelineColumns = Math.ceil(timeRange / step)
4444
const totalTimelineWidth = timelineColumns * stepWidthPx
4545

4646
// Item statistics
47-
const nonGroupItems = items?.filter((item: any) => item.type !== "group") ?? []
48-
const avgDuration = nonGroupItems.length === 0
49-
? step
50-
: nonGroupItems.reduce((sum: number, item: any) => sum + (item.end - item.start), 0) / nonGroupItems.length
51-
const itemMinStart = items && items.length > 0 ? Math.min(...items.map(item => item.start)) : start
52-
const itemMaxEnd = items && items.length > 0 ? Math.max(...items.map(item => item.end)) : end
47+
const {avgDuration, itemMinStart, itemMaxEnd} = React.useMemo(() => {
48+
if (!items || items.length === 0) {
49+
return {avgDuration: step, itemMinStart: start, itemMaxEnd: end}
50+
}
51+
let sumDuration = 0
52+
let nonGroupCount = 0
53+
let minStart = Infinity
54+
let maxEnd = -Infinity
55+
for (let i = 0; i < items.length; i++) {
56+
const item = items[i]
57+
if (item.start < minStart) minStart = item.start
58+
if (item.end > maxEnd) maxEnd = item.end
59+
if ((item as any).type !== "group") {
60+
sumDuration += item.end - item.start
61+
nonGroupCount++
62+
}
63+
}
64+
return {
65+
avgDuration: nonGroupCount === 0 ? step : sumDuration / nonGroupCount,
66+
itemMinStart: minStart,
67+
itemMaxEnd: maxEnd,
68+
}
69+
}, [items, start, end, step])
5370

5471
// Column rendering calculations
5572
const columnsNeeded = items && items.length > 0 ? Math.ceil((itemMaxEnd - start) / step) : timelineColumns
@@ -62,13 +79,14 @@ export const GanttGroup: React.FC<GanttGroupProps> = (props) => {
6279
}
6380

6481
handleResize()
65-
viewportRef?.current?.addEventListener("resize", handleResize)
82+
const viewport = viewportRef.current
83+
viewport?.addEventListener("resize", handleResize)
6684
window.addEventListener("resize", handleResize)
6785
return () => {
6886
window.removeEventListener("resize", handleResize)
69-
viewportRef.current?.removeEventListener("resize", handleResize)
87+
viewport?.removeEventListener("resize", handleResize)
7088
}
71-
})
89+
}, [])
7290

7391
// Calculate row assignments (non-overlapping rows)
7492
const itemRows = items?.length ? items
@@ -82,12 +100,24 @@ export const GanttGroup: React.FC<GanttGroupProps> = (props) => {
82100
: []
83101

84102
// Style configurations
85-
const containerStyles: CSSProperties = {
103+
const containerStyles: CSSProperties = React.useMemo(() => ({
86104
display: "grid",
87105
gridTemplateColumns: `repeat(${columnsToRender}, ${stepWidth})`,
88106
minWidth: "100%",
89107
gridColumn: "1 / -1",
90-
}
108+
}), [columnsToRender, stepWidth])
109+
110+
const rowStyle: CSSProperties = React.useMemo(() => ({
111+
gridColumn: "1 / -1",
112+
minHeight: rowHeight,
113+
position: "relative",
114+
backgroundColor: "transparent"
115+
}), [rowHeight])
116+
117+
const gridLineColumns = React.useMemo(
118+
() => Array.from({length: Math.max(0, columnsToRender - 1)}),
119+
[columnsToRender]
120+
)
91121

92122
return (
93123
<div data-gantt-id={props.id} id={props.id} ref={viewportRef} style={containerStyles}>
@@ -98,14 +128,9 @@ export const GanttGroup: React.FC<GanttGroupProps> = (props) => {
98128
avgDuration={avgDuration}
99129
stepWidth={stepWidth}/>}
100130
{itemRows.map((row, rowIndex) => (
101-
<>
102-
<div key={`row-${rowIndex}`} style={{
103-
gridColumn: "1 / -1",
104-
minHeight: rowHeight,
105-
position: "relative",
106-
backgroundColor: "transparent"
107-
}}>
108-
{Array.from({length: columnsToRender - 1}).map((_, columnIndex) => (
131+
<React.Fragment key={`row-frag-${rowIndex}`}>
132+
<div key={`row-${rowIndex}`} style={rowStyle}>
133+
{gridLineColumns.map((_, columnIndex) => (
109134
<div key={`grid-${columnIndex}`} style={{
110135
position: "absolute",
111136
left: (columnIndex + 1) * stepWidthPx,
@@ -158,20 +183,18 @@ export const GanttGroup: React.FC<GanttGroupProps> = (props) => {
158183
const hasVisibleWidth = itemPosition.width > 0
159184

160185
return hasVisibleWidth && (
161-
<>
162-
<GanttItem
163-
key={item.id}
164-
id={item.id}
165-
w={`${itemPosition.width}px`}
166-
left={`${itemPosition.left}px`}
167-
onClick={() => {
168-
if (item.type != "group") return
169-
setActiveGroup(prevState => item.id === prevState ? undefined : item.id)
170-
}}
171-
>
172-
{children?.(item, itemIndex)}
173-
</GanttItem>
174-
</>
186+
<GanttItem
187+
key={item.id}
188+
id={item.id}
189+
w={`${itemPosition.width}px`}
190+
left={`${itemPosition.left}px`}
191+
onClick={() => {
192+
if (item.type != "group") return
193+
setActiveGroup(prevState => item.id === prevState ? undefined : item.id)
194+
}}
195+
>
196+
{children?.(item, itemIndex)}
197+
</GanttItem>
175198
)
176199
})}
177200
</div>
@@ -183,7 +206,7 @@ export const GanttGroup: React.FC<GanttGroupProps> = (props) => {
183206
stepWidth={stepWidth} rowHeight={rowHeight} items={item.data.items}
184207
key={`group-target-${itemIndex}`}/>
185208
})}
186-
</>
209+
</React.Fragment>
187210
))}
188211

189212
</div>

src/components/gantt/GanttHeader.tsx

Lines changed: 22 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -14,17 +14,31 @@ export const GanttHeader: React.FC<GanttHeaderProps> = (props) => {
1414

1515
const {columnCount, start, step, avgDuration, stepWidth, ...rest} = props
1616

17-
const stepWidthPx = parseInt(stepWidth as string)
17+
const stepWidthPx = React.useMemo(() => parseInt(stepWidth as string), [stepWidth])
18+
const label = React.useMemo(() => getTimelineLabel(avgDuration), [avgDuration])
19+
const columns = React.useMemo(() => Array.from({length: columnCount}), [columnCount])
1820

1921
return <div {...mergeComponentProps("gantt__header", rest)}>
20-
{Array.from({length: columnCount}).map((_, columnIndex) => {
21-
const timelineValue = start + columnIndex * step
22+
{columns.map((_, columnIndex) => {
23+
if (columnIndex === 0) {
24+
return (
25+
<div key={`header-${columnIndex}`} className={"gantt__header-label-column"}>
26+
<Text className={"gantt__header-label"}>
27+
Range in {label.unit}
28+
</Text>
29+
</div>
30+
)
31+
}
32+
2233
const shouldShowLabel = columnIndex % 4 === 0
23-
const label = getTimelineLabel(avgDuration)
24-
const {value, unit} = getTimelineLabel(timelineValue)
25-
const displayValue = `${Math.round(value * 10) / 10}${unit}`
34+
let displayValue = ""
35+
if (shouldShowLabel) {
36+
const timelineValue = start + columnIndex * step
37+
const {value, unit} = getTimelineLabel(timelineValue)
38+
displayValue = `${Math.round(value * 10) / 10}${unit}`
39+
}
2640

27-
return columnIndex !== 0 ? (
41+
return (
2842
<div
2943
key={`header-${columnIndex}`}
3044
className={"gantt__header-column"}
@@ -34,13 +48,7 @@ export const GanttHeader: React.FC<GanttHeaderProps> = (props) => {
3448
}}
3549
>
3650
<Text>
37-
{shouldShowLabel ? displayValue : ""}
38-
</Text>
39-
</div>
40-
) : (
41-
<div key={`header-${columnIndex}`} className={"gantt__header-label-column"}>
42-
<Text className={"gantt__header-label"}>
43-
Range in {label.unit}
51+
{displayValue}
4452
</Text>
4553
</div>
4654
)

0 commit comments

Comments
 (0)