Skip to content

Commit 58f2e4e

Browse files
hangtime79claude
andcommitted
feat(v0.9.5): Tooltip content and interaction polish (#65, #68)
- Show task names instead of IDs in dependency tooltips - Add pin/close buttons to keep tooltips visible - Fallback to ID if dependency name lookup fails Changes: - python-lib/ganttchart/task_transformer.py: Dependency name resolution - webapps/gantt-chart/app.js: Pin/close buttons, hide_popup patch - resource/webapp/style.css: Popup header/actions styles - plugin.json: Version 0.9.4 → 0.9.5 Fixes #65, Fixes #68 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 9f3275d commit 58f2e4e

5 files changed

Lines changed: 595 additions & 9 deletions

File tree

plan/specs/feature-v0.9.5-spec.md

Lines changed: 354 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,354 @@
1+
# Feature v0.9.5 Specification
2+
3+
## Branch
4+
`feature/v0.9.5-tooltip-polish-ii`
5+
6+
## Linked Issues
7+
- Fixes #65 - Show Task Names instead of IDs in Dependency Tooltips
8+
- Fixes #68 - Pin Tooltip to Screen
9+
10+
## Overview
11+
Enhance tooltip usability by showing human-readable task names in dependencies and allowing users to pin tooltips to keep them visible.
12+
13+
---
14+
15+
## Feature #1: Human-Readable Dependency Names (#65)
16+
17+
### Current Behavior
18+
Tooltip shows `Depends on: task_123, task_456` - raw task IDs that are often cryptic.
19+
20+
The `_display_dependencies` field currently stores the original raw dependency string from the data source.
21+
22+
### Desired Behavior
23+
Tooltip shows `Depends on: Design Phase, Requirements Gathering` - human-readable task names.
24+
25+
### Root Cause
26+
`TaskTransformer._process_row()` stores dependencies row-by-row without access to the full task list. The `id_to_name` mapping can only be built after all tasks are processed.
27+
28+
### Implementation
29+
30+
**File:** `python-lib/ganttchart/task_transformer.py`
31+
32+
Add post-processing step in `transform()` method after the row processing loop (line ~139):
33+
34+
```python
35+
# After row processing, before dependency validation
36+
# Build id_to_name lookup for dependency resolution (#65)
37+
id_to_name = {t['id']: t['name'] for t in tasks}
38+
39+
# Post-process: resolve dependency IDs to names
40+
for task in tasks:
41+
if task.get('dependencies'):
42+
resolved_names = []
43+
for dep_id in task['dependencies']:
44+
# Lookup name, fallback to ID if not found
45+
name = id_to_name.get(dep_id, dep_id)
46+
resolved_names.append(name)
47+
# Update display field with resolved names
48+
task['_display_dependencies'] = ', '.join(resolved_names)
49+
```
50+
51+
**Logic:**
52+
1. After all tasks are created, build `id_to_name` lookup dictionary
53+
2. Iterate through tasks with dependencies
54+
3. Resolve each dependency ID to its task name
55+
4. Fallback to ID if lookup fails (data error, external reference)
56+
5. Update `_display_dependencies` with comma-separated names
57+
58+
**Edge Cases:**
59+
- Missing ID in lookup (external dependency) → show ID
60+
- No dependencies → `_display_dependencies` remains empty string
61+
- Already display-friendly IDs → still works (name lookup succeeds)
62+
63+
---
64+
65+
## Feature #2: Pin Tooltip to Screen (#68)
66+
67+
### Current Behavior
68+
Tooltip closes when:
69+
- User clicks elsewhere on the chart
70+
- Mouse leaves the task bar (if hover mode)
71+
- Another task is clicked
72+
73+
### Desired Behavior
74+
- Add a "Pin" button to the tooltip
75+
- Pinned tooltip stays visible until unpinned or closed
76+
- Clicking another task closes pinned tooltip and opens new one
77+
- Unpin/close button restores normal behavior
78+
79+
### Root Cause
80+
Library's `hide_popup()` unconditionally hides the popup. No state tracking for "pinned" status.
81+
82+
### Implementation
83+
84+
**File:** `webapps/gantt-chart/app.js`
85+
86+
#### Step 1: Modify buildPopupHTML to add Pin button
87+
88+
```javascript
89+
function buildPopupHTML(task) {
90+
// ... existing code ...
91+
92+
let html = `
93+
<div class="gantt-popup">
94+
<div class="popup-header">
95+
<div class="popup-title">${escapeHtml(task.name)}</div>
96+
<div class="popup-actions">
97+
<button class="popup-pin-btn" title="Pin tooltip">
98+
<i class="fas fa-thumbtack"></i>
99+
</button>
100+
<button class="popup-close-btn" title="Close">
101+
<i class="fas fa-times"></i>
102+
</button>
103+
</div>
104+
</div>
105+
`;
106+
// ... rest of popup content ...
107+
}
108+
```
109+
110+
#### Step 2: Add popup button event handlers
111+
112+
After popup is shown, attach event handlers:
113+
114+
```javascript
115+
// After show_popup patch, add button handlers
116+
requestAnimationFrame(() => {
117+
const popup = ganttInstance.$popup_wrapper;
118+
if (!popup) return;
119+
120+
const pinBtn = popup.querySelector('.popup-pin-btn');
121+
const closeBtn = popup.querySelector('.popup-close-btn');
122+
123+
if (pinBtn) {
124+
pinBtn.addEventListener('click', (e) => {
125+
e.stopPropagation();
126+
popup.dataset.pinned = popup.dataset.pinned === 'true' ? 'false' : 'true';
127+
pinBtn.classList.toggle('active', popup.dataset.pinned === 'true');
128+
});
129+
}
130+
131+
if (closeBtn) {
132+
closeBtn.addEventListener('click', (e) => {
133+
e.stopPropagation();
134+
popup.dataset.pinned = 'false';
135+
ganttInstance.hide_popup();
136+
});
137+
}
138+
});
139+
```
140+
141+
#### Step 3: Monkey-patch hide_popup to respect pinned state
142+
143+
```javascript
144+
// Patch hide_popup to check pinned state (#68)
145+
const originalHidePopup = ganttInstance.hide_popup.bind(ganttInstance);
146+
ganttInstance.hide_popup = function() {
147+
const popup = ganttInstance.$popup_wrapper;
148+
if (popup && popup.dataset.pinned === 'true') {
149+
return; // Don't hide pinned popup
150+
}
151+
originalHidePopup();
152+
};
153+
```
154+
155+
#### Step 4: Reset pinned state when new popup opens
156+
157+
In the existing show_popup patch:
158+
```javascript
159+
ganttInstance.show_popup = function(opts) {
160+
// Reset any existing pinned state
161+
const popup = ganttInstance.$popup_wrapper;
162+
if (popup) {
163+
popup.dataset.pinned = 'false';
164+
}
165+
originalShowPopup(opts);
166+
// ... rest of positioning logic ...
167+
};
168+
```
169+
170+
**File:** `resource/webapp/style.css`
171+
172+
```css
173+
/* Popup header with actions (#68) */
174+
.gantt-popup .popup-header {
175+
display: flex;
176+
justify-content: space-between;
177+
align-items: flex-start;
178+
gap: 8px;
179+
}
180+
181+
.gantt-popup .popup-actions {
182+
display: flex;
183+
gap: 4px;
184+
flex-shrink: 0;
185+
}
186+
187+
.gantt-popup .popup-pin-btn,
188+
.gantt-popup .popup-close-btn {
189+
background: none;
190+
border: none;
191+
cursor: pointer;
192+
padding: 2px;
193+
opacity: 0.6;
194+
transition: opacity 0.15s;
195+
}
196+
197+
.gantt-popup .popup-pin-btn:hover,
198+
.gantt-popup .popup-close-btn:hover {
199+
opacity: 1;
200+
}
201+
202+
.gantt-popup .popup-pin-btn.active {
203+
opacity: 1;
204+
color: var(--color-primary, #0078d4);
205+
}
206+
207+
/* Dark mode icons */
208+
.dark-theme .gantt-popup .popup-pin-btn,
209+
.dark-theme .gantt-popup .popup-close-btn {
210+
color: var(--color-text);
211+
}
212+
```
213+
214+
---
215+
216+
## Version Bump
217+
218+
**File:** `plugin.json`
219+
220+
Change version from `0.9.4` to `0.9.5`.
221+
222+
---
223+
224+
## Files to Modify
225+
226+
| File | Action | Description |
227+
|------|--------|-------------|
228+
| `python-lib/ganttchart/task_transformer.py` | Modify | Add post-processing to resolve dependency IDs to names |
229+
| `webapps/gantt-chart/app.js` | Modify | Add pin/close buttons, patch hide_popup |
230+
| `resource/webapp/style.css` | Modify | Add popup header/actions styles |
231+
| `plugin.json` | Modify | Version 0.9.4 → 0.9.5 |
232+
233+
---
234+
235+
## Testing Checklist
236+
237+
### #65 - Dependency Names
238+
- [ ] Tooltip shows task names instead of IDs for dependencies
239+
- [ ] Single dependency: `Depends on: Task Name`
240+
- [ ] Multiple dependencies: `Depends on: Name1, Name2, Name3`
241+
- [ ] Missing dependency (data error): Falls back to showing ID
242+
- [ ] No dependencies: No "Depends on" line shown
243+
- [ ] Works with all ID formats (numeric, string, generated)
244+
245+
### #68 - Pin Tooltip
246+
- [ ] Pin button visible in tooltip header
247+
- [ ] Close button visible in tooltip header
248+
- [ ] Click Pin: Tooltip stays visible when clicking chart background
249+
- [ ] Click Pin again: Tooltip unpins (returns to normal behavior)
250+
- [ ] Click Close: Tooltip closes immediately
251+
- [ ] Click another task: Pinned tooltip closes, new one opens
252+
- [ ] Dark mode: Icons visible and styled correctly
253+
- [ ] Light mode: Icons visible and styled correctly
254+
255+
---
256+
257+
## User QA Gate
258+
259+
**CRITICAL: Code must be committed BEFORE User QA.**
260+
261+
Dataiku plugins load from committed code, not working directory files.
262+
263+
**Pre-QA Commit Process:**
264+
1. After implementing fixes, commit with:
265+
```
266+
feat(v0.9.5): Tooltip content and interaction polish (#65, #68)
267+
268+
- Show task names instead of IDs in dependency tooltips
269+
- Add pin/close buttons to keep tooltips visible
270+
- Fallback to ID if dependency name lookup fails
271+
272+
Changes:
273+
- python-lib/ganttchart/task_transformer.py: Dependency name resolution
274+
- webapps/gantt-chart/app.js: Pin/close buttons, hide_popup patch
275+
- resource/webapp/style.css: Popup header/actions styles
276+
- plugin.json: Version 0.9.4 → 0.9.5
277+
278+
Fixes #65, Fixes #68
279+
280+
🤖 Generated with [Claude Code](https://claude.com/claude-code)
281+
282+
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
283+
```
284+
2. Verify: `git log --oneline -1`
285+
3. Notify user code is ready for QA
286+
287+
**QA Script for User:**
288+
```
289+
1. Reload plugin in Dataiku (Actions menu → Reload)
290+
2. Open a Gantt chart with tasks that have dependencies
291+
292+
DEPENDENCY NAMES TEST (#65):
293+
3. Click on a task that has dependencies
294+
4. VERIFY: "Depends on" shows task NAMES (not IDs)
295+
5. Check a task with multiple dependencies
296+
6. VERIFY: All dependency names shown, comma-separated
297+
298+
PIN TOOLTIP TEST (#68):
299+
7. Click on any task bar
300+
8. VERIFY: Tooltip shows Pin icon (pushpin) and Close icon (X)
301+
9. Click the Pin icon
302+
10. VERIFY: Pin icon becomes highlighted/active
303+
11. Click somewhere else on the chart (not on a task)
304+
12. VERIFY: Tooltip STAYS VISIBLE (pinned)
305+
13. Click on a DIFFERENT task
306+
14. VERIFY: Old tooltip closes, new tooltip opens
307+
15. Click the Close (X) button
308+
16. VERIFY: Tooltip closes immediately
309+
310+
Report: PASS or describe any issues observed.
311+
```
312+
313+
**Do not proceed to PR/merge until user confirms both features work.**
314+
315+
---
316+
317+
## Rollback Plan
318+
319+
**If #65 breaks:**
320+
Remove the post-processing loop in `task_transformer.py`. `_display_dependencies` will return to showing raw IDs.
321+
322+
**If #68 breaks:**
323+
- Remove pin/close buttons from `buildPopupHTML()`
324+
- Remove `hide_popup` monkey-patch
325+
- Remove popup header CSS
326+
327+
Both features are independent; can roll back one without affecting the other.
328+
329+
---
330+
331+
## Watch Out For
332+
333+
1. **Dependency self-reference:** If a task ID references itself, the lookup will still work (just returns its own name). Not a bug, but edge case to be aware of.
334+
335+
2. **Performance:** Post-processing is O(N×M) where N=tasks, M=average dependencies. For 1000 tasks with ~3 deps each, this is negligible.
336+
337+
3. **Event listener cleanup:** Pin/close button handlers attach on each popup show. Use event delegation or ensure handlers don't stack.
338+
339+
4. **CSS-safe IDs vs display IDs:** The `_display_dependencies` already stores display-friendly values. The post-processing should use the CSS-safe `id` for lookup (since that's what `dependencies` array contains).
340+
341+
5. **Button icons:** Use FontAwesome classes (`fas fa-thumbtack`, `fas fa-times`) since Dataiku includes FontAwesome 5.15.4.
342+
343+
---
344+
345+
## Spec Complete
346+
347+
**Ready for SDE implementation.**
348+
349+
The SDE should:
350+
1. Implement dependency name resolution (#65) first - Python side
351+
2. Implement pin/close tooltip (#68) second - JS/CSS side
352+
3. Bump version
353+
4. Commit and request User QA
354+
5. Do NOT proceed past QA gate without user approval

plugin.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
"id": "gantt-chart",
44

55
// It is highly recommended to use Semantic Versioning
6-
"version": "0.9.4",
6+
"version": "0.9.5",
77

88
// Meta data for display purposes:
99
"meta": {

python-lib/ganttchart/task_transformer.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,17 @@ def transform(self, df: pd.DataFrame) -> Dict[str, Any]:
146146
# Sort tasks
147147
tasks = sort_tasks(tasks, self.config.sort_by)
148148

149+
# Resolve dependency IDs to task names for display (#65)
150+
id_to_name = {t['id']: t['name'] for t in tasks}
151+
for task in tasks:
152+
if task.get('dependencies'):
153+
resolved_names = []
154+
for dep_id in task['dependencies']:
155+
# Lookup name, fallback to ID if not found
156+
name = id_to_name.get(dep_id, dep_id)
157+
resolved_names.append(name)
158+
task['_display_dependencies'] = ', '.join(resolved_names)
159+
149160
# Apply maxTasks limit
150161
if self.config.max_tasks > 0 and len(tasks) > self.config.max_tasks:
151162
original_count = len(tasks)

0 commit comments

Comments
 (0)