Skip to content

refactor: improve layout #191

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Dec 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion rollup.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ const plugins = (minify) => [
? terser({
output: { preamble: banner },
})
: cleanup({ comments: ['some', /__PURE__/] }),
: cleanup({ comments: ['some', /__PURE__/], extensions: ['js', 'ts'] }),
]

export default [
Expand Down
20 changes: 12 additions & 8 deletions src/lib/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,17 @@ const flowSort = (a: FromToElement, b: FromToElement) => {
return b.flow - a.flow
}

const setSizes = (nodes: Map<string, SankeyNode>, size: SankeyControllerDatasetOptions['size']) => {
const sizeMethod = validateSizeValue(size)

for (const node of nodes.values()) {
node.from.sort(flowSort)
node.to.sort(flowSort)
// Fallbacks with || are for zero values and min method to avoid zero size.
node.size = Math[sizeMethod](node.in || node.out, node.out || node.in)
}
}

const setPriorities = (nodes: Map<string, SankeyNode>, priority: SankeyControllerDatasetOptions['priority']) => {
if (!priority) return

Expand Down Expand Up @@ -76,14 +87,7 @@ export function buildNodesFromData(
}
}

const sizeMethod = validateSizeValue(size)

for (const node of nodes.values()) {
node.from.sort(flowSort)
node.to.sort(flowSort)
node.size = Math[sizeMethod](node.in || node.out, node.out || node.in)
}

setSizes(nodes, size)
setPriorities(nodes, priority)
setColumns(nodes, column)

Expand Down
288 changes: 283 additions & 5 deletions src/lib/layout.test.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,288 @@
import { describe, expect, test } from '@jest/globals'
import { describe, expect, it } from '@jest/globals'

import { addPadding } from './layout'
import { buildNodesFromData } from './core'
import { addPadding, calculateX } from './layout'

describe('lib/layout', () => {
describe('calculateX', () => {
it('should work with empty chart', () => {
expect(calculateX(new Map(), [], 'edge')).toEqual(0)
expect(calculateX(new Map(), [], 'even')).toEqual(0)
})
it.each([
[
'1x2',
[{ from: 'a', to: 'b', flow: 1 }],
'edge' as const,
[
{ key: 'a', x: 0 },
{ key: 'b', x: 1 },
],
1,
],
[
'1x3',
[
{ from: 'a', to: 'b', flow: 2 },
{ from: 'b', to: 'c', flow: 1 },
],
'edge' as const,
[
{ key: 'a', x: 0 },
{ key: 'b', x: 1 },
{ key: 'c', x: 2 },
],
2,
],
[
'2x2',
[
{ from: 'a', to: 'b', flow: 1 },
{ from: 'c', to: 'd', flow: 1 },
],
'edge' as const,
[
{ key: 'a', x: 0 },
{ key: 'b', x: 1 },
{ key: 'c', x: 0 },
{ key: 'd', x: 1 },
],
1,
],
[
'2x3 edge',
[
{ from: 'a', to: 'b', flow: 1 },
{ from: 'c', to: 'd', flow: 1 },
{ from: 'd', to: 'e', flow: 1 },
],
'edge' as const,
[
{ key: 'a', x: 0 },
{ key: 'b', x: 2 },
{ key: 'c', x: 0 },
{ key: 'd', x: 1 },
{ key: 'e', x: 2 },
],
2,
],
[
'2x3 even',
[
{ from: 'a', to: 'b', flow: 1 },
{ from: 'c', to: 'd', flow: 1 },
{ from: 'd', to: 'e', flow: 1 },
],
'even' as const,
[
{ key: 'a', x: 0 },
{ key: 'b', x: 1 },
{ key: 'c', x: 0 },
{ key: 'd', x: 1 },
{ key: 'e', x: 2 },
],
2,
],
])('should map nodes to columns: %p', (_test, data, mode, expected, maxX) => {
const nodes = buildNodesFromData(data, {})
expect(calculateX(nodes, data, mode)).toEqual(maxX)
expect([...nodes.values()].map(({ key, x }) => ({ key, x }))).toEqual(expected)
})

it.each([
['1x1 circular', [{ from: 'a', to: 'a', flow: 1 }], 'even' as const, [{ key: 'a', x: 0 }], 0],
[
'2x1 circular',
[
{ from: 'a', to: 'a', flow: 1 },
{ from: 'b', to: 'b', flow: 1 },
],
'even' as const,
[
{ key: 'a', x: 0 },
{ key: 'b', x: 0 },
],
0,
],
[
'1x2 circular',
[
{ from: 'a', to: 'b', flow: 1 },
{ from: 'b', to: 'b', flow: 1 },
],
'even' as const,
[
{ key: 'a', x: 0 },
{ key: 'b', x: 1 },
],
1,
],

[
'2x2 circular',
[
{ from: 'a', to: 'b', flow: 1 },
{ from: 'c', to: 'd', flow: 1 },
{ from: 'd', to: 'd', flow: 1 },
],
'even' as const,
[
{ key: 'a', x: 0 },
{ key: 'b', x: 1 },
{ key: 'c', x: 0 },
{ key: 'd', x: 1 },
],
1,
],
])('should map nodes with simple circular flows to columns: %p', (_test, data, mode, expected, maxX) => {
const nodes = buildNodesFromData(data, {})
expect(calculateX(nodes, data, mode)).toEqual(maxX)
expect([...nodes.values()].map(({ key, x }) => ({ key, x }))).toEqual(expected)
})

it.each([
[
'1x2 circular variant',
[
{ from: 'a', to: 'b', flow: 1 },
{ from: 'b', to: 'a', flow: 1 },
],
'even' as const,
[
{ key: 'a', x: 0 },
{ key: 'b', x: 1 },
],
1,
],
[
'1x3 circular variant',
[
{ from: 'a', to: 'b', flow: 1 },
{ from: 'b', to: 'c', flow: 1 },
{ from: 'c', to: 'a', flow: 1 },
],
'even' as const,
[
{ key: 'a', x: 0 },
{ key: 'b', x: 1 },
{ key: 'c', x: 2 },
],
2,
],
[
'3x1,2x1 circular',
[
{ from: 'a', to: 'b', flow: 1 },
{ from: 'b', to: 'c', flow: 1 },
{ from: 'd', to: 'b', flow: 1 },
{ from: 'd', to: 'e', flow: 1 },
{ from: 'e', to: 'c', flow: 1 },
],
'even' as const,
[
{ key: 'a', x: 0 },
{ key: 'b', x: 1 },
{ key: 'c', x: 2 },
{ key: 'd', x: 0 },
{ key: 'e', x: 1 },
],
2,
],
[
'3x1,2x1 circular variant',
[
{ from: 'a', to: 'b', flow: 1 },
{ from: 'b', to: 'c', flow: 1 },
{ from: 'd', to: 'b', flow: 1 },
{ from: 'e', to: 'c', flow: 1 },
{ from: 'd', to: 'e', flow: 1 },
],
'even' as const,
[
{ key: 'a', x: 0 },
{ key: 'b', x: 1 },
{ key: 'c', x: 2 },
{ key: 'd', x: 0 },
{ key: 'e', x: 1 },
],
2,
],
[
'complex circular',
[
{ from: 'a', to: 'b', flow: 1 },
{ from: 'b', to: 'b', flow: 1 },
{ from: 'b', to: 'c', flow: 1 },
{ from: 'b', to: 'd', flow: 1 },
{ from: 'c', to: 'c', flow: 1 },
{ from: 'd', to: 'e', flow: 1 },
{ from: 'e', to: 'b', flow: 1 },
],
'edge' as const,
[
{ key: 'a', x: 0 },
{ key: 'b', x: 1 },
{ key: 'c', x: 2 },
{ key: 'd', x: 2 },
{ key: 'e', x: 3 },
],
3,
],
[
'complex circular 2',
[
{ from: 'a', to: 'b', flow: 1 },
{ from: 'b', to: 'c', flow: 1 },
{ from: 'b', to: 'd', flow: 1 },
{ from: 'c', to: 'd', flow: 1 },
{ from: 'd', to: 'e', flow: 1 },
{ from: 'f', to: 'c', flow: 1 },
],
'edge' as const,
[
{ key: 'a', x: 0 },
{ key: 'b', x: 1 },
{ key: 'c', x: 2 },
{ key: 'd', x: 3 },
{ key: 'e', x: 4 },
{ key: 'f', x: 0 },
],
4,
],
])('should map nodes with circular flows to columns: %p', (_test, data, mode, expected, maxX) => {
const nodes = buildNodesFromData(data, {})
expect(calculateX(nodes, data, mode)).toEqual(maxX)
expect([...nodes.values()].map(({ key, x }) => ({ key, x }))).toEqual(expected)
})

it.each([
[
'2x2 circular variant',
[
{ from: 'a', to: 'b', flow: 1 },
{ from: 'c', to: 'd', flow: 1 },
{ from: 'd', to: 'c', flow: 1 },
],
'even' as const,
[
{ key: 'a', x: 0 },
{ key: 'b', x: 1 },
{ key: 'c', x: 0 },
{ key: 'd', x: 1 },
],
1,
],
])(
'should map nodes with multiple entries and circular flows to columns: %p',
(_test, data, mode, expected, maxX) => {
const nodes = buildNodesFromData(data, {})
expect(calculateX(nodes, data, mode)).toEqual(maxX)
expect([...nodes.values()].map(({ key, x }) => ({ key, x }))).toEqual(expected)
}
)
})
describe('addPadding', () => {
test('when there is a single row of nodes, it should not add any paddings', () => {
it('when there is a single row of nodes, it should not add any paddings', () => {
const nodes = [
{ x: 0, y: 0, in: 0, out: 8, size: 8 },
{ x: 1, y: 0, in: 8, out: 10, size: 10 },
Expand All @@ -18,7 +296,7 @@ describe('lib/layout', () => {
expect(nodes.map((node) => node.y)).toEqual([0, 0, 0])
})

test('when there are multiple rows of nodes, it should add paddings', () => {
it('when there are multiple rows of nodes, it should add paddings', () => {
const nodes = [
{ x: 0, y: 0, in: 0, out: 8, size: 8 },
{ x: 0, y: 8, in: 0, out: 5, size: 5 },
Expand All @@ -34,7 +312,7 @@ describe('lib/layout', () => {
expect(nodes.map((node) => node.y)).toEqual([0, 9, 15, 0, 15])
})

test('it should consider previous columns, when node has input', () => {
it('it should consider previous columns, when node has input', () => {
const nodes = [
{ x: 0, y: 0, in: 0, out: 1, size: 1 },
{ x: 0, y: 1, in: 0, out: 1, size: 1 },
Expand Down
Loading
Loading