Skip to content

Commit

Permalink
Merge pull request #201 from melfore/198-connection-between-tasks
Browse files Browse the repository at this point in the history
[TimeLine] connection between tasks
  • Loading branch information
CrisGrud committed Feb 29, 2024
2 parents fc3deab + 9f01d48 commit c734112
Show file tree
Hide file tree
Showing 13 changed files with 1,318 additions and 33 deletions.
3 changes: 1 addition & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
## [1.32.3](https://github.com/melfore/konva-timeline/compare/v1.32.2...v1.32.3) (2024-02-27)


### Bug Fixes

* 🐛 [Task] Time return ([7f321d2](https://github.com/melfore/konva-timeline/commit/7f321d27846449837c82cb4bd6cdbdff77408f59)), closes [#199](https://github.com/melfore/konva-timeline/issues/199)
- 🐛 [Task] Time return ([7f321d2](https://github.com/melfore/konva-timeline/commit/7f321d27846449837c82cb4bd6cdbdff77408f59)), closes [#199](https://github.com/melfore/konva-timeline/issues/199)

## [1.32.2](https://github.com/melfore/konva-timeline/compare/v1.32.1...v1.32.2) (2024-02-19)

Expand Down
30 changes: 15 additions & 15 deletions src/KonvaTimeline/index.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,16 @@ export const CompletedPercentage: Story = {
end: 1702095200000,
},
tasks: [
{
id: "4",
label: "Task4",
resourceId: "2",
completedPercentage: 28,
time: {
start: 1698047900000,
end: 1698557900000,
},
},
{
id: "1",
label: "Task1",
Expand All @@ -161,16 +171,6 @@ export const CompletedPercentage: Story = {
end: 1699434800000,
},
},
{
id: "2",
label: "Task2",
resourceId: "2",
completedPercentage: 19,
time: {
start: 1700434800000,
end: 1700934800000,
},
},
{
id: "3",
label: "Task3",
Expand All @@ -182,13 +182,13 @@ export const CompletedPercentage: Story = {
},
},
{
id: "4",
label: "Task4",
id: "2",
label: "Task2",
resourceId: "2",
completedPercentage: 28,
completedPercentage: 19,
time: {
start: 1698047900000,
end: 1698557900000,
start: 1700434800000,
end: 1700934800000,
},
},
{
Expand Down
100 changes: 100 additions & 0 deletions src/KonvaTimeline/line-scenario.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import type { Meta, StoryObj } from "@storybook/react";

import DecoratoGantt from "../utils/decorator";

import { generateStoryData } from "./stories-data";
import KonvaTimeline from ".";

const meta = {
title: "Scenario/Gantt",
component: KonvaTimeline,
decorators: [DecoratoGantt],
tags: ["autodocs"],
argTypes: {
onTaskClick: {
type: "function",
},
onTaskChange: {
type: "function",
},
},
} satisfies Meta<typeof KonvaTimeline>;

export default meta;
type Story = StoryObj<typeof meta>;

const { resources } = generateStoryData({
averageTaskDurationInMinutes: 200,
resourcesCount: 3,
tasksCount: 5,
timeRangeInDays: 1,
});

export const Line: Story = {
args: {
onAreaSelect: undefined,
resources,
resolution: "2weeks",
enableLines: true,
toolTip: false,
onTaskClick: (task) => task,
initialDateTime: 1698516000000,
range: {
start: 1698357600000,
end: 1702095200000,
},
tasks: [
{
id: "4",
label: "Task4",
resourceId: "2",
time: {
start: 1698411600000,
end: 1698613200000,
},
relatedTasks: ["1"],
},
{
id: "1",
label: "Task1",
resourceId: "1",
time: {
start: 1698793200000,
end: 1699434800000,
},
relatedTasks: ["3", "2"],
},
{
id: "3",
label: "Task3",
resourceId: "3",
time: {
start: 1699734800000,
end: 1700234800000,
},
},
{
id: "2",
label: "Task2",
resourceId: "2",
time: {
start: 1700434800000,
end: 1700934800000,
},
relatedTasks: ["5"],
},
{
id: "5",
label: "Task5",
resourceId: "1",
time: {
start: 1701505200000,
end: 1702105200000,
},
},
],
onTaskChange: (task, opts) => {
task.id, opts;
},
},
};
1 change: 0 additions & 1 deletion src/grid/CellGroup/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,6 @@ const GridCellGroup = ({ column, index, dayInfo, hourInfo }: GridCellGroupProps)
}
return DEFAULT_STROKE_DARK_MODE;
}, [themeColor]);

return (
<KonvaGroup key={`timeslot-${index}`}>
<KonvaLine x={xPos} y={0} points={points} stroke={stroke} strokeWidth={1} />
Expand Down
157 changes: 157 additions & 0 deletions src/tasks/components/LayerLine/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
import React, { FC, useCallback, useState } from "react";
import { Layer } from "react-konva";
import { DateTime } from "luxon";

import { useTimelineContext } from "../../../timeline/TimelineContext";
import { KonvaPoint } from "../../../utils/konva";
import { InternalTimeRange } from "../../../utils/time";
import { LINE_OFFSET } from "../../utils/line";
import { getTaskYCoordinate, TASK_HEIGHT_OFFSET } from "../../utils/tasks";
import LineKonva from "../Line";
import TaskLine from "../TaskLine";
import TaskTooltip, { TaskTooltipProps } from "../Tooltip";

interface TasksLayerProps {
taskTooltip: TaskTooltipProps | null;
setTaskTooltip: (tooltip: TaskTooltipProps | null) => void;
create?: boolean;
onTaskEvent: (value: boolean) => void;
}

/**
* This component renders a set of tasks as a Konva Layer.
* Tasks are displayed accordingly to their assigned resource (different vertical / row position) and their timing (different horizontal / column position)
* `TasksLayer` is also responsible of handling callback for task components offering base implementation for click, leave and over.
*
* The playground has a canvas that simulates 1 day of data with 1 hour resolution.
* Depending on your screen size you might be able to test also the horizontal scrolling behaviour.
*/
const LayerLine: FC<TasksLayerProps> = ({ setTaskTooltip, taskTooltip, create, onTaskEvent }) => {
const { columnWidth, drawRange, interval, resolution, resources, rowHeight, tasks, toolTip, validLine } =
useTimelineContext();

const [workLine, setWorkLine] = useState([""]);
const { start: intervalStart, end: intervalEnd } = interval;

const getResourceById = useCallback(
(resourceId: string) => resources.findIndex(({ id }) => resourceId === id),
[resources]
);

const getTaskById = useCallback((taskId: string) => tasks.find(({ id }) => taskId === id), [tasks]);

const onTaskLeave = useCallback(() => setTaskTooltip(null), [setTaskTooltip]);

const onTaskOver = useCallback(
(taskId: string, point: KonvaPoint) => {
const task = getTaskById(taskId);
if (!task) {
return setTaskTooltip(null);
}

const { x, y } = point;
setTaskTooltip({ task, x, y });
},
[getTaskById, setTaskTooltip]
);

const getXCoordinate = useCallback(
(offset: number) => (offset * columnWidth) / resolution.sizeInUnits,
[columnWidth, resolution.sizeInUnits]
);

const getTaskXCoordinate = useCallback(
(startTime: number) => {
const timeStart = DateTime.fromMillis(startTime);
const startOffsetInUnit = timeStart.diff(intervalStart!).as(resolution.unit);
return getXCoordinate(startOffsetInUnit);
},
[getXCoordinate, intervalStart, resolution.unit]
);

const getTaskWidth = useCallback(
({ start, end }: InternalTimeRange) => {
const timeStart = DateTime.fromMillis(start);
const timeEnd = DateTime.fromMillis(end);
const widthOffsetInUnit = timeEnd.diff(timeStart).as(resolution.unit);
return getXCoordinate(widthOffsetInUnit);
},
[getXCoordinate, resolution]
);

if (!intervalStart || !intervalEnd) {
return null;
}

if (drawRange.end - drawRange.start <= 0) {
return null;
}
return (
<Layer>
{validLine &&
validLine.map((data) => {
const startResourceIndex = getResourceById(data.startResId);
if (startResourceIndex < 0 || workLine.includes(data.id)) {
return null;
}
//line
const yCoordinate = getTaskYCoordinate(startResourceIndex, rowHeight) + (rowHeight * TASK_HEIGHT_OFFSET) / 2;
const endResourceIndex = getResourceById(data.endResId);
const endY = getTaskYCoordinate(endResourceIndex, rowHeight) + (rowHeight * TASK_HEIGHT_OFFSET) / 2;
const startLine = getTaskXCoordinate(data.start);
const endLine = getTaskXCoordinate(data.end);

return (
<LineKonva
key={`Layer${data.id}`}
points={[
startLine,
yCoordinate,
startLine + LINE_OFFSET,
yCoordinate,
endLine - LINE_OFFSET,
endY,
endLine,
endY,
]}
/>
);
})}
{tasks.map((taskData) => {
const { resourceId, time } = taskData;
const resourceIndex = getResourceById(resourceId);
if (resourceIndex < 0) {
return null;
}

const { color: resourceColor, toCompleteColor } = resources[resourceIndex];
const xCoordinate = getTaskXCoordinate(time.start);
const yCoordinate = getTaskYCoordinate(resourceIndex, rowHeight);
const width = getTaskWidth(time);
if (xCoordinate > drawRange.end || xCoordinate + width < drawRange.start) {
return null;
}

return (
<TaskLine
key={`task-${taskData.id}`}
data={taskData}
fill={resourceColor}
fillToComplete={toCompleteColor}
onLeave={onTaskLeave}
onOver={onTaskOver}
x={xCoordinate}
y={yCoordinate}
width={width}
disabled={create}
onTaskEvent={onTaskEvent}
workLine={setWorkLine}
/>
);
})}
{toolTip && taskTooltip && <TaskTooltip {...taskTooltip} />}
</Layer>
);
};

export default LayerLine;
38 changes: 38 additions & 0 deletions src/tasks/components/Line/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import React from "react";
import { Circle, Group, Line } from "react-konva";

import {
CIRCLE_POINT_COLOR,
CIRCLE_POINT_OFFSET,
CIRCLE_POINT_STROKE,
LINE_COLOR,
LINE_TENSION,
LINE_WIDTH,
LineType,
} from "../../utils/line";

const LineKonva = ({ points }: LineType) => {
return (
<Group>
<Line strokeWidth={LINE_WIDTH} lineJoin="round" stroke={LINE_COLOR} points={points} tension={LINE_TENSION} />
<Circle
x={points[0] + CIRCLE_POINT_OFFSET}
y={points[1]}
radius={4}
stroke={CIRCLE_POINT_STROKE}
fill={CIRCLE_POINT_COLOR}
strokeWidth={1}
/>
<Circle
x={points[6] - CIRCLE_POINT_OFFSET}
y={points[7]}
radius={4}
stroke={CIRCLE_POINT_STROKE}
fill={CIRCLE_POINT_COLOR}
strokeWidth={1}
/>
</Group>
);
};

export default LineKonva;
Loading

0 comments on commit c734112

Please sign in to comment.