2024-06-01 18:45:06 +00:00
|
|
|
import { useContext, useMemo, useState } from 'react';
|
|
|
|
|
import { firstBy } from 'thenby';
|
|
|
|
|
import classNames from 'classnames';
|
|
|
|
|
import { useEscapeKey } from 'components/hooks';
|
2024-06-07 05:30:58 +00:00
|
|
|
import { objectToArray } from 'lib/data';
|
|
|
|
|
import { ReportContext } from '../[reportId]/Report';
|
|
|
|
|
// eslint-disable-next-line css-modules/no-unused-class
|
2024-06-05 02:53:49 +00:00
|
|
|
import styles from './JourneyView.module.css';
|
2024-05-17 08:42:36 +00:00
|
|
|
|
2024-06-07 05:30:58 +00:00
|
|
|
const NODE_HEIGHT = 60;
|
|
|
|
|
const NODE_GAP = 10;
|
|
|
|
|
const BAR_OFFSET = 3;
|
|
|
|
|
|
2024-05-17 08:42:36 +00:00
|
|
|
export default function JourneyView() {
|
2024-06-05 02:53:49 +00:00
|
|
|
const [selectedNode, setSelectedNode] = useState(null);
|
2024-06-07 05:30:58 +00:00
|
|
|
const [activeNode, setActiveNode] = useState(null);
|
2024-05-17 08:42:36 +00:00
|
|
|
const { report } = useContext(ReportContext);
|
2024-06-04 06:40:38 +00:00
|
|
|
const { data, parameters } = report || {};
|
2024-06-05 02:53:49 +00:00
|
|
|
|
|
|
|
|
useEscapeKey(() => setSelectedNode(null));
|
|
|
|
|
|
2024-06-01 18:45:06 +00:00
|
|
|
const columns = useMemo(() => {
|
|
|
|
|
if (!data) {
|
|
|
|
|
return [];
|
|
|
|
|
}
|
2024-06-04 06:40:38 +00:00
|
|
|
return Array(Number(parameters.steps))
|
2024-06-01 18:45:06 +00:00
|
|
|
.fill(undefined)
|
2024-06-07 05:30:58 +00:00
|
|
|
.map((column = {}, index) => {
|
2024-06-01 18:45:06 +00:00
|
|
|
data.forEach(({ items, count }) => {
|
2024-06-07 05:30:58 +00:00
|
|
|
const name = items[index];
|
|
|
|
|
const selectedNodes = selectedNode?.paths ?? [];
|
|
|
|
|
|
|
|
|
|
if (name) {
|
|
|
|
|
if (!column[name]) {
|
|
|
|
|
const selected = !!selectedNodes.find(a => a.items[index] === name);
|
|
|
|
|
const paths = data.filter((d, i) => {
|
|
|
|
|
return i !== index && d.items[index] === name;
|
|
|
|
|
});
|
|
|
|
|
const from =
|
|
|
|
|
index > 0 &&
|
|
|
|
|
selected &&
|
|
|
|
|
paths.reduce((obj, path) => {
|
|
|
|
|
const { items, count } = path;
|
|
|
|
|
const name = items[index - 1];
|
|
|
|
|
if (!obj[name]) {
|
|
|
|
|
obj[name] = { name, count: +count };
|
|
|
|
|
} else {
|
|
|
|
|
obj[name].count += +count;
|
|
|
|
|
}
|
|
|
|
|
return obj;
|
|
|
|
|
}, {});
|
|
|
|
|
const to =
|
|
|
|
|
selected &&
|
|
|
|
|
paths.reduce((obj, path) => {
|
|
|
|
|
const { items, count } = path;
|
|
|
|
|
const name = items[index + 1];
|
|
|
|
|
if (name) {
|
|
|
|
|
if (!obj[name]) {
|
|
|
|
|
obj[name] = { name, count: +count };
|
|
|
|
|
} else {
|
|
|
|
|
obj[name].count += +count;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return obj;
|
|
|
|
|
}, {});
|
2024-06-01 18:45:06 +00:00
|
|
|
|
2024-06-07 05:30:58 +00:00
|
|
|
column[name] = {
|
|
|
|
|
name,
|
2024-06-01 18:45:06 +00:00
|
|
|
total: +count,
|
2024-06-07 05:30:58 +00:00
|
|
|
columnIndex: index,
|
|
|
|
|
selected,
|
|
|
|
|
paths,
|
|
|
|
|
from: objectToArray(from),
|
|
|
|
|
to: objectToArray(to),
|
2024-06-01 18:45:06 +00:00
|
|
|
};
|
|
|
|
|
} else {
|
2024-06-07 05:30:58 +00:00
|
|
|
column[name].total += +count;
|
2024-06-01 18:45:06 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
2024-06-07 05:30:58 +00:00
|
|
|
return {
|
|
|
|
|
nodes: objectToArray(column).sort(firstBy('total', -1)),
|
|
|
|
|
};
|
2024-06-01 18:45:06 +00:00
|
|
|
});
|
2024-06-05 02:53:49 +00:00
|
|
|
}, [data, selectedNode]);
|
2024-06-01 18:45:06 +00:00
|
|
|
|
|
|
|
|
const handleClick = (item: string, index: number, paths: any[]) => {
|
2024-06-05 02:53:49 +00:00
|
|
|
if (item !== selectedNode?.item || index !== selectedNode?.index) {
|
|
|
|
|
setSelectedNode({ item, index, paths });
|
2024-06-01 18:45:06 +00:00
|
|
|
} else {
|
2024-06-05 02:53:49 +00:00
|
|
|
setSelectedNode(null);
|
2024-06-01 18:45:06 +00:00
|
|
|
}
|
|
|
|
|
};
|
2024-05-17 08:42:36 +00:00
|
|
|
|
|
|
|
|
if (!data) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
2024-06-07 05:30:58 +00:00
|
|
|
//console.log({ columns, selectedNode, activeNode });
|
|
|
|
|
|
2024-06-01 18:45:06 +00:00
|
|
|
return (
|
|
|
|
|
<div className={styles.container}>
|
|
|
|
|
<div className={styles.view}>
|
2024-06-07 05:30:58 +00:00
|
|
|
{columns.map((column, columnIndex) => {
|
|
|
|
|
const current = columnIndex === selectedNode?.index;
|
|
|
|
|
const behind = columnIndex <= selectedNode?.index - 1;
|
|
|
|
|
const ahead = columnIndex > selectedNode?.index;
|
2024-06-01 18:45:06 +00:00
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div
|
2024-06-07 05:30:58 +00:00
|
|
|
key={columnIndex}
|
2024-06-01 18:45:06 +00:00
|
|
|
className={classNames(styles.column, {
|
|
|
|
|
[styles.current]: current,
|
|
|
|
|
[styles.behind]: behind,
|
|
|
|
|
[styles.ahead]: ahead,
|
|
|
|
|
})}
|
|
|
|
|
>
|
|
|
|
|
<div className={styles.header}>
|
2024-06-07 05:30:58 +00:00
|
|
|
<div className={styles.num}>{columnIndex + 1}</div>
|
2024-06-01 18:45:06 +00:00
|
|
|
</div>
|
2024-06-05 02:53:49 +00:00
|
|
|
<div className={styles.nodes}>
|
2024-06-07 05:30:58 +00:00
|
|
|
{column.nodes.map(({ name, total, selected, paths, from, to }, nodeIndex) => {
|
|
|
|
|
const active =
|
|
|
|
|
selected && activeNode?.paths.find(path => path.items[columnIndex] === name);
|
|
|
|
|
const bars = [];
|
|
|
|
|
const lines = from?.reduce(
|
|
|
|
|
(obj: { flat: boolean; fromUp: boolean; fromDown: boolean }, { name }: any) => {
|
|
|
|
|
const fromIndex = columns[columnIndex - 1]?.nodes.findIndex(node => {
|
|
|
|
|
return node.name === name && node.selected;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (fromIndex > -1) {
|
|
|
|
|
if (nodeIndex === fromIndex) {
|
|
|
|
|
obj.flat = true;
|
|
|
|
|
} else if (nodeIndex > fromIndex) {
|
|
|
|
|
obj.fromUp = true;
|
|
|
|
|
bars.push([fromIndex, nodeIndex, 1]);
|
|
|
|
|
} else if (nodeIndex < fromIndex) {
|
|
|
|
|
obj.fromDown = true;
|
|
|
|
|
bars.push([nodeIndex, fromIndex, 0]);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return obj;
|
|
|
|
|
},
|
|
|
|
|
{},
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
to?.reduce((obj: { toUp: boolean; toDown: boolean }, { name }: any) => {
|
|
|
|
|
const toIndex = columns[columnIndex + 1]?.nodes.findIndex(node => {
|
|
|
|
|
return node.name === name && node.selected;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (toIndex > -1) {
|
|
|
|
|
if (nodeIndex > toIndex) {
|
|
|
|
|
obj.toUp = true;
|
|
|
|
|
} else if (nodeIndex < toIndex) {
|
|
|
|
|
obj.toDown = true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return obj;
|
|
|
|
|
}, lines);
|
|
|
|
|
|
2024-06-05 02:53:49 +00:00
|
|
|
return (
|
|
|
|
|
<div
|
2024-06-07 05:30:58 +00:00
|
|
|
key={name}
|
2024-06-05 02:53:49 +00:00
|
|
|
className={classNames(styles.item, {
|
|
|
|
|
[styles.selected]: selected,
|
2024-06-07 05:30:58 +00:00
|
|
|
[styles.active]: active,
|
2024-06-05 02:53:49 +00:00
|
|
|
})}
|
2024-06-07 05:30:58 +00:00
|
|
|
onClick={() => handleClick(name, columnIndex, paths)}
|
|
|
|
|
onMouseEnter={() => selected && setActiveNode({ name, columnIndex, paths })}
|
|
|
|
|
onMouseLeave={() => selected && setActiveNode(null)}
|
2024-06-05 02:53:49 +00:00
|
|
|
>
|
2024-06-07 05:30:58 +00:00
|
|
|
<div className={styles.name}>{name}</div>
|
|
|
|
|
<div className={styles.count}>{total}</div>
|
|
|
|
|
{Object.keys(lines).map(key => {
|
|
|
|
|
return <div key={key} className={classNames(styles.line, styles[key])} />;
|
|
|
|
|
})}
|
|
|
|
|
{columnIndex < columns.length &&
|
|
|
|
|
bars.map(([a, b, d], i) => {
|
|
|
|
|
const height = (b - a - 1) * (NODE_HEIGHT + NODE_GAP) + NODE_GAP;
|
|
|
|
|
return (
|
|
|
|
|
<div
|
|
|
|
|
key={i}
|
|
|
|
|
className={styles.bar}
|
|
|
|
|
style={{
|
|
|
|
|
height: height + BAR_OFFSET,
|
|
|
|
|
top: d ? -height : NODE_HEIGHT,
|
|
|
|
|
}}
|
|
|
|
|
/>
|
|
|
|
|
);
|
|
|
|
|
})}
|
2024-06-05 02:53:49 +00:00
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
})}
|
|
|
|
|
</div>
|
2024-06-01 18:45:06 +00:00
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
})}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
2024-05-17 08:42:36 +00:00
|
|
|
}
|