Skip to content

Commit b4204f5

Browse files
committed
tweak(panel/playerDrops): improved timeline chart cursor behavior
1 parent f69b024 commit b4204f5

3 files changed

Lines changed: 81 additions & 32 deletions

File tree

panel/src/pages/PlayerDropsPage/TimelineDropsChart.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import drawDropsTimeline, { TimelineDropsDatum } from "./drawDropsTimeline";
55
import { playerDropCategories } from "@/lib/playerDropCategories";
66
import { PlayerDropsMessage } from "./PlayerDropsGenericSubcards";
77
import { DrilldownRangeSelectionType } from "./PlayerDropsPage";
8+
import './timeline.css';
89

910
export type TimelineDropsChartData = {
1011
displayLod: string;
@@ -158,7 +159,6 @@ function TimelineDropsChart({ chartData, chartName, width, height, rangeSelected
158159
position: 'absolute',
159160
top: `${margins.top}px`,
160161
left: `${margins.left}px`,
161-
// imageRendering: 'pixelated',
162162
}}
163163
/>
164164
</>);

panel/src/pages/PlayerDropsPage/drawDropsTimeline.ts

Lines changed: 66 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,10 @@ export default function drawDropsTimeline({
7676
console.log('Drawable area:', drawableAreaWidth, drawableAreaHeight);
7777

7878
const chartGroup = svg.append('g')
79-
.attr('transform', translate(margins.left, margins.top));
79+
.attr('transform', translate(margins.left, margins.top))
80+
.attr('class', 'interaction-chart')
81+
.attr('data-selected', 'false');
82+
const chartGroupData = chartGroup.node()?.dataset;
8083

8184

8285
//Scales
@@ -238,6 +241,12 @@ export default function drawDropsTimeline({
238241
.attr('stroke-width', 1)
239242
.attr('stroke-dasharray', '3,3');
240243

244+
const cursorHighlight = chartGroup.append('rect')
245+
.attr('fill', 'transparent')
246+
.attr('height', drawableAreaHeight)
247+
.attr('opacity', '0')
248+
.attr('class', 'transition-opacity interaction-highlight');
249+
241250
//Range selector mark
242251
const defs = chartGroup.append('defs');
243252
const mask = defs.append('mask')
@@ -258,6 +267,14 @@ export default function drawDropsTimeline({
258267
.attr('mask', `url(#${maskElmntId})`);
259268

260269
//Helpers
270+
const getRangeBar = (x1: number, x2: number) => {
271+
const x1Floor = Math.floor(x1);
272+
const x2Floor = Math.floor(x2);
273+
const start = Math.min(x1Floor, x2Floor) + 0.5 - barCenterOffset;
274+
const width = Math.abs(x2Floor - x1Floor) + intervalWidth + 0.5;
275+
return { start, width };
276+
}
277+
261278
const updateRangeRect = (x1?: number, x2?: number) => {
262279
//Hide mask
263280
if (
@@ -266,16 +283,15 @@ export default function drawDropsTimeline({
266283
|| (x1 > drawableAreaWidth && x2 > drawableAreaWidth)
267284
) {
268285
maskArea.attr('opacity', '0');
286+
chartGroup.attr('data-selected', 'false');
269287
return;
270288
}
271289

272290
//Set mask
273-
const x1Floor = Math.floor(x1);
274-
const x2Floor = Math.floor(x2);
275-
maskRect
276-
.attr('x', Math.min(x1Floor, x2Floor) + 0.5 - barCenterOffset)
277-
.attr('width', Math.abs(x2Floor - x1Floor) + intervalWidth + 0.5);
291+
const { start, width } = getRangeBar(x1, x2);
292+
maskRect.attr('x', start).attr('width', width);
278293
maskArea.attr('opacity', '1');
294+
chartGroup.attr('data-selected', 'true');
279295
}
280296
const setUpstreamRangeState = (range: [date1: Date, date2: Date] | null) => {
281297
if (!Array.isArray(range) || range.length !== 2) {
@@ -320,13 +336,14 @@ export default function drawDropsTimeline({
320336
}
321337

322338
//Find the closest data point for a given X value
339+
const filteredLog = data.log.filter((datum) => datum.drops.length > 0);
323340
const timeBisector = d3.bisector((interval: TimelineDropsDatum) => interval.startDate).center;
324341
const findClosestDatum = (pointerX: number) => {
325342
// const xPosDate = timeScale.invert(pointerX - intervalWidth / 2);
326343
const xPosDate = timeScale.invert(pointerX);
327-
const indexFound = timeBisector(data.log, xPosDate);
344+
const indexFound = timeBisector(filteredLog, xPosDate);
328345
if (indexFound === -1) return;
329-
const datum = data.log[indexFound];
346+
const datum = filteredLog[indexFound];
330347
const datumStartTs = datum.startDate.getTime();
331348
return {
332349
datum,
@@ -341,7 +358,10 @@ export default function drawDropsTimeline({
341358
const handleMouseMove = (pointerX: number) => {
342359
// Find closest data point
343360
const datumFound = findClosestDatum(pointerX);
344-
if (!datumFound) return;
361+
if (!datumFound) {
362+
cursorHighlight.attr('opacity', '0');
363+
return;
364+
}
345365
const { datum, datumStartX, dataumIndex } = datumFound;
346366
if (dataumIndex === lastDatumIndex) return;
347367
lastDatumIndex = dataumIndex;
@@ -352,6 +372,8 @@ export default function drawDropsTimeline({
352372
return updateRangeRect(rangeStartData.x, datumStartX);
353373
}
354374

375+
if (chartGroupData?.selected === 'true') return;
376+
355377
//Set legend data
356378
const allNumEls = legendRef.querySelectorAll<HTMLSpanElement>('span[data-category]');
357379
for (const numEl of allNumEls) {
@@ -389,13 +411,21 @@ export default function drawDropsTimeline({
389411
}
390412

391413
// Draw cursor
392-
const cursorX = Math.round(datumStartX + intervalWidth / 2) + 0.5 - barCenterOffset;
393-
cursorLineVert.attr('x1', cursorX).attr('y1', 0).attr('x2', cursorX).attr('y2', drawableAreaHeight);
414+
// const cursorX = Math.round(datumStartX + intervalWidth / 2) + 0.5 - barCenterOffset;
415+
// cursorLineVert.attr('x1', cursorX).attr('y1', 0).attr('x2', cursorX).attr('y2', drawableAreaHeight);
416+
417+
// Draw cursor highlight bar
418+
const barX = getRangeBar(datumStartX, datumStartX);
419+
cursorHighlight
420+
.attr('x', barX.start)
421+
.attr('width', barX.width)
422+
.attr('opacity', '1');
394423
};
395424

396425
const handleMouseDown = (pointerX: number) => {
397426
const datumFound = findClosestDatum(pointerX);
398427
if (!datumFound) return;
428+
chartGroup.attr('data-mousedown', 'true');
399429
hideCursor();
400430
rangeStartData = {
401431
x: datumFound.datumStartX,
@@ -405,10 +435,15 @@ export default function drawDropsTimeline({
405435
}
406436

407437
const handleMouseUp = (pointerX: number) => {
438+
chartGroup.attr('data-mousedown', 'false');
408439
if (!rangeStartData) return clearRangeData(true);
409440
const datumFound = findClosestDatum(pointerX);
410441
if (!datumFound) return clearRangeData(true);
411-
if (!rangeCrossedThreshold && rangeStartData.datum.startDate.getTime() === datumFound.datum.startDate.getTime()) {
442+
// if (!rangeCrossedThreshold && rangeStartData.datum.startDate.getTime() === datumFound.datum.startDate.getTime()) {
443+
// return clearRangeData(true);
444+
// }
445+
446+
if (rangeSelected && chartGroupData?.selected !== 'true') {
412447
return clearRangeData(true);
413448
}
414449

@@ -423,6 +458,8 @@ export default function drawDropsTimeline({
423458
}
424459

425460
const handleMouseLeave = () => {
461+
chartGroup.attr('data-mousedown', 'false');
462+
cursorHighlight.attr('opacity', '0');
426463
setTimeout(() => {
427464
clearRangeData(!!rangeStartData);
428465
hideCursor();
@@ -432,30 +469,28 @@ export default function drawDropsTimeline({
432469
// Handle svg mouse events
433470
let isEventInCooldown = false;
434471
const cooldownTime = 20;
435-
chartGroup.append('rect')
436-
.attr('width', drawableAreaWidth)
437-
.attr('height', drawableAreaHeight)
438-
.attr('fill', 'transparent')
439-
.on('mousemove', function (event) {
440-
const [pointerX] = d3.pointer(event);
441-
if (!isEventInCooldown) {
442-
isEventInCooldown = true;
472+
chartGroup.on('mousemove', function (event) {
473+
const [pointerX] = d3.pointer(event);
474+
if (!isEventInCooldown) {
475+
isEventInCooldown = true;
476+
handleMouseMove(pointerX);
477+
setTimeout(() => {
478+
isEventInCooldown = false;
479+
}, cooldownTime);
480+
} else {
481+
clearTimeout(cursorRedrawTimeout);
482+
cursorRedrawTimeout = setTimeout(() => {
443483
handleMouseMove(pointerX);
444-
setTimeout(() => {
445-
isEventInCooldown = false;
446-
}, cooldownTime);
447-
} else {
448-
clearTimeout(cursorRedrawTimeout);
449-
cursorRedrawTimeout = setTimeout(() => {
450-
handleMouseMove(pointerX);
451-
}, cooldownTime);
452-
}
453-
})
454-
.on('mousedown', function (event) {
484+
}, cooldownTime);
485+
}
486+
})
487+
.on('mousedown', function (event: MouseEvent) {
488+
if (event.button !== 0) return; //left btn only
455489
const [pointerX] = d3.pointer(event);
456490
handleMouseDown(pointerX);
457491
})
458492
.on('mouseup', function (event) {
493+
if (event.button !== 0) return; //left btn only
459494
const [pointerX] = d3.pointer(event);
460495
handleMouseUp(pointerX);
461496
});
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
.interaction-highlight:not([data-mousedown="true"] *) {
2+
fill: rgba(255, 255, 255, 0.1);
3+
}
4+
5+
.interaction-chart[data-mousedown="true"] {
6+
cursor: col-resize;
7+
}
8+
.interaction-chart[data-selected="true"]:not([data-mousedown="true"]) {
9+
cursor: zoom-out;
10+
/* cursor: pointer; */
11+
}
12+
.interaction-chart[data-selected="false"]:not([data-mousedown="true"]) {
13+
cursor: pointer;
14+
}

0 commit comments

Comments
 (0)