Refactored journey rendering.

This commit is contained in:
Mike Cao 2024-06-14 20:44:47 -07:00
parent b97ac23432
commit 2204ffa9c3
2 changed files with 182 additions and 148 deletions

View file

@ -104,10 +104,6 @@
background: var(--primary400); background: var(--primary400);
} }
.item.active .count {
background: var(--primary600);
}
.behind { .behind {
color: var(--base400); color: var(--base400);
} }
@ -130,11 +126,15 @@
background: var(--base200); background: var(--base200);
} }
.selected .count { .item.selected .count {
color: var(--base50); color: var(--base50);
background: var(--base800); background: var(--base800);
} }
.item.selected.active .count {
background: var(--primary600);
}
.line { .line {
position: absolute; position: absolute;
bottom: 0; bottom: 0;

View file

@ -28,66 +28,114 @@ export default function JourneyView() {
const selectedPaths = selectedNode?.paths ?? []; const selectedPaths = selectedNode?.paths ?? [];
const activePaths = activeNode?.paths ?? []; const activePaths = activeNode?.paths ?? [];
const columns = [];
return Array(Number(parameters.steps)) for (let columnIndex = 0; columnIndex < +parameters.steps; columnIndex++) {
.fill(undefined) const nodes = {};
.map((nodes = {}, index) => {
data.forEach(({ items, count }) => {
const name = items[index];
if (name) { data.forEach(({ items, count }: any, nodeIndex: any) => {
const selected = !!selectedPaths.find(path => path.items[index] === name); const name = items[columnIndex];
const active = selected && !!activePaths.find(path => path.items[index] === name);
if (!nodes[name]) { if (name) {
const paths = data.filter((d, i) => { const selected = !!selectedPaths.find(path => path.items[columnIndex] === name);
return i !== index && d.items[index] === name; const active = selected && !!activePaths.find(path => path.items[columnIndex] === name);
});
const from = if (!nodes[name]) {
index > 0 && const paths = data.filter((d, i) => {
selected && return i !== columnIndex && d.items[columnIndex] === name;
paths.reduce((obj, path) => { });
const { items, count } = path;
const name = items[index - 1];
if (!obj[name]) { const from =
obj[name] = { name, count }; columnIndex > 0 &&
} else { selected &&
obj[name].count += count; paths.reduce((obj, path) => {
} const { items, count } = path;
const name = items[columnIndex - 1];
return obj; if (!obj[name]) {
}, {}); obj[name] = { name, count };
} else {
obj[name].count += count;
}
nodes[name] = { return obj;
name, }, {});
count,
total: count, nodes[name] = {
columnIndex: index, name,
selected, count,
active, totalCount: count,
paths, nodeIndex,
from: objectToArray(from), columnIndex,
}; selected,
} else { active,
nodes[name].total += count; paths,
} from: objectToArray(from),
};
} else {
nodes[name].totalCount += count;
} }
}); }
const nodesArray = objectToArray(nodes).sort(firstBy('total', -1));
return {
nodes: nodesArray,
visitors: nodesArray.reduce((sum, { selected, total }) => {
if (!selectedNode || (selectedNode && selected)) {
sum += total;
}
return sum;
}, 0),
};
}); });
columns.push({
nodes: objectToArray(nodes).sort(firstBy('total', -1)),
});
}
columns.forEach((column, columnIndex) => {
const nodes = column.nodes.map((node, nodeIndex) => {
const { from, totalCount } = node;
const previousNodes = columns[columnIndex - 1]?.nodes;
let selectedCount = from?.length ? 0 : totalCount;
let activeCount = selectedCount;
const lines = from?.reduce((arr: any[][], { name, count }: any) => {
const fromIndex = previousNodes.findIndex((node: { name: any; selected: any }) => {
return node.name === name && node.selected;
});
if (fromIndex > -1) {
arr.push([fromIndex, nodeIndex]);
selectedCount += count;
}
if (
previousNodes.findIndex(node => {
return node.name === name && node.active;
}) > -1
) {
activeCount += count;
}
return arr;
}, []);
return { ...node, selectedCount, activeCount, lines };
});
const visitorCount = nodes.reduce(
(sum: number, { selected, selectedCount, active, activeCount, totalCount }) => {
if (!selectedNode) {
sum += totalCount;
} else if (!activeNode && selected) {
sum += selectedCount;
} else if (active) {
sum += activeCount;
}
return sum;
},
0,
);
const previousTotal = columns[columnIndex - 1]?.visitorCount ?? 0;
const dropOff =
previousTotal > 0 ? ((visitorCount - previousTotal) / previousTotal) * 100 : 0;
Object.assign(column, { nodes, visitorCount, dropOff });
});
return columns;
}, [data, selectedNode, activeNode]); }, [data, selectedNode, activeNode]);
const handleClick = (name: string, index: number, paths: any[]) => { const handleClick = (name: string, index: number, paths: any[]) => {
@ -95,8 +143,8 @@ export default function JourneyView() {
setSelectedNode({ name, index, paths }); setSelectedNode({ name, index, paths });
} else { } else {
setSelectedNode(null); setSelectedNode(null);
setActiveNode(null);
} }
setActiveNode(null);
}; };
if (!data) { if (!data) {
@ -107,110 +155,96 @@ export default function JourneyView() {
<div className={styles.container}> <div className={styles.container}>
<div className={styles.view}> <div className={styles.view}>
{columns.map((column, columnIndex) => { {columns.map((column, columnIndex) => {
const previousTotal = columns[columnIndex - 1]?.visitors ?? 0; const dropOffPercent = `${~~column.dropOff}%`;
const dropOff =
previousTotal > 0 ? ((column.visitors - previousTotal) / previousTotal) * 100 : 0;
return ( return (
<div <div
key={columnIndex} key={columnIndex}
className={classNames(styles.column, { [styles.active]: activeNode })} className={classNames(styles.column, {
[styles.selected]: selectedNode,
[styles.active]: activeNode,
})}
> >
<div className={styles.header}> <div className={styles.header}>
<div className={styles.num}>{columnIndex + 1}</div> <div className={styles.num}>{columnIndex + 1}</div>
<div className={styles.stats}> <div className={styles.stats}>
<div className={styles.visitors}> <div className={styles.visitors}>
{column.visitors} {formatMessage(labels.visitors)} {column.visitorCount} {formatMessage(labels.visitors)}
</div> </div>
{columnIndex > 0 && <div className={styles.dropoff}>{`${~~dropOff}%`}</div>} {columnIndex > 0 && <div className={styles.dropoff}>{dropOffPercent}</div>}
</div> </div>
</div> </div>
<div className={styles.nodes}> <div className={styles.nodes}>
{column.nodes.map(({ name, total, selected, active, paths, from }, nodeIndex) => { {column.nodes.map(
const previousNodes = columns[columnIndex - 1]?.nodes; ({
let selectedCount = from?.length ? 0 : total; name,
let activeCount = selectedCount; totalCount,
selected,
const lines = from?.reduce((arr, { name, count }: any) => { active,
const fromIndex = previousNodes.findIndex(node => { paths,
return node.name === name && node.selected; activeCount,
}); selectedCount,
lines,
if (fromIndex > -1) { }) => {
arr.push([fromIndex, nodeIndex]); return (
selectedCount += count; <div
} key={name}
className={classNames(styles.item, {
if ( [styles.selected]: selected,
previousNodes.findIndex(node => { [styles.active]: active,
return node.name === name && node.active;
}) > -1
) {
activeCount += count;
}
return arr;
}, []);
return (
<div
key={name}
className={classNames(styles.item, {
[styles.selected]: selected,
[styles.active]: active,
})}
onClick={() => handleClick(name, columnIndex, paths)}
onMouseEnter={() => selected && setActiveNode({ name, columnIndex, paths })}
onMouseLeave={() => selected && setActiveNode(null)}
>
<div className={styles.name}>{name}</div>
<TooltipPopup label="hi" disabled={!selected}>
<div className={styles.count}>
{selected ? (active ? activeCount : selectedCount) : total}
</div>
</TooltipPopup>
{columnIndex < columns.length &&
lines.map(([fromIndex, nodeIndex], i) => {
const height =
(Math.abs(nodeIndex - fromIndex) + 1) * (NODE_HEIGHT + NODE_GAP) -
NODE_GAP;
const midHeight =
(Math.abs(nodeIndex - fromIndex) - 1) * (NODE_HEIGHT + NODE_GAP) +
NODE_GAP +
LINE_WIDTH;
const nodeName = columns[columnIndex - 1]?.nodes[fromIndex].name;
return (
<div
key={`${fromIndex}${nodeIndex}${i}`}
className={classNames(styles.line, {
[styles.active]:
active &&
activeNode?.paths.find(
path =>
path.items[columnIndex] === name &&
path.items[columnIndex - 1] === nodeName,
),
[styles.up]: fromIndex < nodeIndex,
[styles.down]: fromIndex > nodeIndex,
[styles.flat]: fromIndex === nodeIndex,
})}
style={{ height }}
>
<div className={classNames(styles.segment, styles.start)} />
<div
className={classNames(styles.segment, styles.mid)}
style={{
height: midHeight,
}}
/>
<div className={classNames(styles.segment, styles.end)} />
</div>
);
})} })}
</div> onClick={() => handleClick(name, columnIndex, paths)}
); onMouseEnter={() => selected && setActiveNode({ name, columnIndex, paths })}
})} onMouseLeave={() => selected && setActiveNode(null)}
>
<div className={styles.name}>{name}</div>
<TooltipPopup label={dropOffPercent} disabled={!selected}>
<div className={styles.count}>
{selected ? (active ? activeCount : selectedCount) : totalCount}
</div>
</TooltipPopup>
{columnIndex < columns.length &&
lines.map(([fromIndex, nodeIndex], i) => {
const height =
(Math.abs(nodeIndex - fromIndex) + 1) * (NODE_HEIGHT + NODE_GAP) -
NODE_GAP;
const midHeight =
(Math.abs(nodeIndex - fromIndex) - 1) * (NODE_HEIGHT + NODE_GAP) +
NODE_GAP +
LINE_WIDTH;
const nodeName = columns[columnIndex - 1]?.nodes[fromIndex].name;
return (
<div
key={`${fromIndex}${nodeIndex}${i}`}
className={classNames(styles.line, {
[styles.active]:
active &&
activeNode?.paths.find(
path =>
path.items[columnIndex] === name &&
path.items[columnIndex - 1] === nodeName,
),
[styles.up]: fromIndex < nodeIndex,
[styles.down]: fromIndex > nodeIndex,
[styles.flat]: fromIndex === nodeIndex,
})}
style={{ height }}
>
<div className={classNames(styles.segment, styles.start)} />
<div
className={classNames(styles.segment, styles.mid)}
style={{
height: midHeight,
}}
/>
<div className={classNames(styles.segment, styles.end)} />
</div>
);
})}
</div>
);
},
)}
</div> </div>
</div> </div>
); );