Skip to content

Commit

Permalink
feat: multiple segment selection
Browse files Browse the repository at this point in the history
  • Loading branch information
yelhouti committed May 23, 2024
1 parent a392124 commit f211673
Show file tree
Hide file tree
Showing 4 changed files with 78 additions and 34 deletions.
5 changes: 3 additions & 2 deletions example/random-data.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,11 +39,12 @@ function getRandomData(ordinal = false) {

return {
timeRange: [start, end],
val: ordinal ? categoryLabels[Math.ceil(Math.random()*nCategories)] : Math.random()
val: ordinal ? categoryLabels[Math.ceil(Math.random()*nCategories)] : Math.random(),
//labelVal: is optional - only displayed in the labels
//selected: is optional, allow to preselect items
};
});

}
}
}
}
3 changes: 2 additions & 1 deletion src/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export interface Line {
export interface Segment {
timeRange: [TS, TS];
val: Val;
selected?: boolean;
}

export type TS = Date | number;
Expand Down Expand Up @@ -107,7 +108,7 @@ export interface TimelinesChartGenericInstance<ChainableInstance> {
label: string,
val: Val,
timeRange: Range<TS>
}) => void): ChainableInstance;
}[]) => void): ChainableInstance;

segmentTooltipContent(cb: (segment: {
group: string,
Expand Down
1 change: 0 additions & 1 deletion src/timelines.css
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,6 @@
}

.series-segment {
stroke: none;
resize: horizontal;
}

Expand Down
103 changes: 73 additions & 30 deletions src/timelines.js
Original file line number Diff line number Diff line change
Expand Up @@ -84,13 +84,14 @@ export default Kapsule({

for (let j= 0, jlen=rawData[i].data.length; j<jlen; j++) {
for (let k= 0, klen=rawData[i].data[j].data.length; k<klen; k++) {
const { timeRange, val, labelVal } = rawData[i].data[j].data[k];
const { timeRange, val, labelVal, selected } = rawData[i].data[j].data[k];
state.completeFlatData.push({
group: group,
label: rawData[i].data[j].label,
timeRange: timeRange.map(d => new Date(d)),
val,
labelVal: labelVal !== undefined ? labelVal : val,
selected,
data: rawData[i].data[j].data[k]
});
}
Expand Down Expand Up @@ -976,7 +977,7 @@ export default Kapsule({

let timelines = state.graph.selectAll('rect.series-segment').data(
state.flatData.filter(dataFilter),
d => d.group + d.label + d.timeRange[0]
d => d.group + d.label + d.timeRange[0] + d.selected
);

timelines.exit()
Expand All @@ -1002,6 +1003,10 @@ export default Kapsule({
.on('mouseover.segmentTooltip', state.segmentTooltip.show)
.on('mouseout.segmentTooltip', state.segmentTooltip.hide);

newSegments.filter(d => d.selected)
.attr('stroke', "rgb(99,229,227)")
.attr('stroke-width', 3)

const resizeOffset = 10;

newSegments
Expand Down Expand Up @@ -1064,27 +1069,58 @@ export default Kapsule({
.attr('height', state.lineHeight)
.style('fill-opacity', .8);
})
.on('click', function (ev, s) {
if (state.onSegmentClick)
state.onSegmentClick(s);
.on('click', function (ev, d) {
if (ev.ctrlKey) {
if (d.selected) {
d.selected= false;
d3Select(this)
.attr('stroke', null)
} else {
d.selected = true;
d3Select(this)
.attr('stroke', "rgb(99,229,227)")
.attr('stroke-width', 3);
}
} else {
clearSelection();
if (state.onSegmentClick) {
state.onSegmentClick(d);
}
}
});

let startWidth;
let startX;
function getSelectedSegmentsOrPassedSegmentIfNoSelection(segment) {
if (segment.datum().selected) {
return state.graph.selectAll('rect.series-segment').filter(d => d.selected).nodes().map(d3Select);
} else {
return [segment]
}
}

function clearSelection() {
state.graph.selectAll('rect.series-segment').filter(d => d.selected).attr('stroke', null).each(function(d) {
d.selected = false;
});
}

let startWidths;
let startXs;
let startPointerX;
let dragBehavior;

newSegments.call(d3Drag()
.on('start', function (ev) {
const segment = d3Select(this);
startWidth = +segment.attr('width');
startX = +segment.attr('x');
const segmentWidth = +segment.attr('width');
const mouseX = ev.x - +segment.attr('x');

startWidths = getSelectedSegmentsOrPassedSegmentIfNoSelection(segment).map(s => +s.attr('width'));
startXs = getSelectedSegmentsOrPassedSegmentIfNoSelection(segment).map(s => +s.attr('x'));
startPointerX = d3Pointer(ev, this)[0];
const mouseX = ev.x - startX;

if (mouseX <= resizeOffset) { // Close to the left side
dragBehavior = 'resizeLeft';
} else if (mouseX >= startWidth - resizeOffset) { // Close to the right side
} else if (mouseX >= segmentWidth - resizeOffset) { // Close to the right side
dragBehavior = 'resizeRight';
} else {
dragBehavior = 'move';
Expand All @@ -1093,33 +1129,40 @@ export default Kapsule({
})
.on('drag', function (ev) {
const segment = d3Select(this);
const segments = getSelectedSegmentsOrPassedSegmentIfNoSelection(segment);
const currentPointerX = d3Pointer(ev, this)[0];
const xDiff = currentPointerX - startPointerX;

dragging = true;

if (dragBehavior === 'resizeLeft') {
segment.attr('width', startWidth - xDiff).attr('x', startX + xDiff);
} else if (dragBehavior === 'resizeRight') {
segment.attr('width', startWidth + xDiff);
} else {
segment.attr('x', startX + xDiff);
for (let i= 0; i < segments.length; i++) {
const segment = segments[i];
if (dragBehavior === 'resizeLeft') {
segment.attr('width', startWidths[i] - xDiff).attr('x', startXs[i] + xDiff);
} else if (dragBehavior === 'resizeRight') {
segment.attr('width', startWidths[i] + xDiff);
} else {
segment.attr('x', startXs[i] + xDiff);
}
}
})
.on('end', function (ev, d) {
.on('end', function () {
const segment = d3Select(this);
const segments = getSelectedSegmentsOrPassedSegmentIfNoSelection(segment);
for (const segment of segments) {
const newWidth = +segment.attr('width');
const newX = +segment.attr('x');
const timeRangeStart = state.xScale.invert(newX);
const timeRangeEnd = state.xScale.invert(newX + newWidth);

segment.datum({ ...segment.datum(), timeRange: [timeRangeStart, timeRangeEnd] });
}
if (state.onSegmentResize && dragging) {
state.onSegmentResize(segments.map(s => s.datum()));
}
segment.attr('cursor', 'pointer');
const newWidth = +segment.attr('width');
const newX = +segment.attr('x');
const timeRangeStart = state.xScale.invert(newX);
const timeRangeEnd = state.xScale.invert(newX + newWidth);

// Update the timeRange in the data object 'd'
d.timeRange = [timeRangeStart, timeRangeEnd];
if (state.onSegmentResize && dragging)
state.onSegmentResize(d);

startWidth = undefined;
startX = undefined;
startWidths = undefined;
startXs = undefined;
startPointerX = undefined;
dragBehavior = undefined;
dragging = false;
Expand Down

0 comments on commit f211673

Please sign in to comment.