Skip to content

Commit

Permalink
feat: view
Browse files Browse the repository at this point in the history
  • Loading branch information
manyyuri committed May 27, 2024
1 parent f1d8ad4 commit 4b4f7c1
Show file tree
Hide file tree
Showing 7 changed files with 297 additions and 1 deletion.
2 changes: 1 addition & 1 deletion .eslintcache

Large diffs are not rendered by default.

186 changes: 186 additions & 0 deletions __tests__/view/view.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
import { createViews } from '../../src/view/view';
import { createRenderer } from '../../src/renderer/renderer';
import { mount, createDiv } from '../utils';

function renderViews(views, width = 640, height = 480) {
const renderer = createRenderer(width, height);
mount(createDiv(), renderer.node());
return views.map(([{
x, y, width, height,
}]) => renderer.rect({
x, y, width, height, stroke: 'black', fill: 'none',
}));
}

describe('createViews', () => {
test('basic container', () => {
const views = createViews({});
renderViews(views);

expect(views.length).toBe(1);
const [[view, [node]]] = views;
expect(view).toEqual({
x: 0, y: 0, width: 640, height: 480,
});
expect(node).toEqual({});
});

test('layer container', () => {
const views = createViews({
type: 'layer',
children: [{}, {}],
});
renderViews(views);

expect(views.length).toBe(1);
const [[view, nodes]] = views;
expect(view).toEqual({
x: 0, y: 0, width: 640, height: 480,
});
expect(nodes.length).toBe(3);
});

test('row container', () => {
const views = createViews({
type: 'row',
children: [{}, {}],
});
renderViews(views);
expect(views.length).toBe(3);

const [, [view, [node]]] = views;
expect(view).toEqual({
height: 480, width: 300, x: 0, y: 0,
});
expect(node).toEqual({});
});

test('col container', () => {
const views = createViews({
type: 'col',
padding: 20,
flex: [1, 2, 1],
children: [
{}, {}, {},
],
});
renderViews(views);

expect(views.length).toBe(4);

const [, [view, [node]]] = views;
expect(view).toEqual({
height: 110, width: 640, x: 0, y: 0,
});
expect(node).toEqual({});
});

test('flex container', () => {
const views = createViews({
type: 'row',
children: [
{},
{ type: 'col', children: [{}, {}] },
],
});
renderViews(views);

expect(views.length).toBe(5);
const [, , , [view, [node]]] = views;
expect(view).toEqual({
height: 220, width: 300, x: 340, y: 0,
});
expect(node).toEqual({});
});

test('facet container with specified x', () => {
const data = [
{ sex: 'male', skin: 'white' },
{ sex: 'male', skin: 'black' },
{ sex: 'female', skin: 'white' },
{ sex: 'female', skin: 'yellow' },
];
const views = createViews({
type: 'facet',
encodings: {
x: 'sex',
},
data,
children: [{}],
});
renderViews(views);

expect(views.length).toBe(3);
const [, [view, [node]]] = views;
const { transform, ...rest } = view;
expect(rest).toEqual({
height: 375, width: 275, x: 45, y: 45,
});
expect(node).toEqual({});
expect(transform(data)).toEqual([
{ sex: 'male', skin: 'white' },
{ sex: 'male', skin: 'black' },
]);
});

test('facet container with specified y', () => {
const data = [
{ sex: 'male', skin: 'white' },
{ sex: 'male', skin: 'black' },
{ sex: 'female', skin: 'white' },
{ sex: 'female', skin: 'yellow' },
];
const views = createViews({
type: 'facet',
encodings: {
y: 'skin',
},
data,
children: [{}, {}],
});
renderViews(views);

expect(views.length).toBe(4);
const [, [view, [node]]] = views;
const { transform, ...rest } = view;
expect(rest).toEqual({
height: 125, width: 550, x: 45, y: 45,
});
expect(node).toEqual({});
expect(transform(data)).toEqual([
{ sex: 'male', skin: 'white' },
{ sex: 'female', skin: 'white' },
]);
});

test('facet container', () => {
const data = [
{ sex: 'male', skin: 'white' },
{ sex: 'male', skin: 'black' },
{ sex: 'female', skin: 'white' },
{ sex: 'female', skin: 'yellow' },
];
const views = createViews({
type: 'facet',
encodings: {
x: 'sex',
y: 'skin',
},
data,
padding: 20,
children: [{}, {}],
});
renderViews(views);

expect(views.length).toBe(7);
const [, [view, [node]]] = views;
const { transform, ...rest } = view;
expect(rest).toEqual({
height: 111.66666666666667, width: 265, x: 45, y: 45,
});
expect(node).toEqual({});
expect(transform(data)).toEqual([
{ sex: 'male', skin: 'white' },
]);
});
});
34 changes: 34 additions & 0 deletions src/view/facet.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { group } from '../utils';

export function computeFacetViews(box, {
data, encodings = {}, padding = 0,
paddingLeft = 45, paddingRight = 45, paddingBottom = 45, paddingTop = 60,
}) {
const { x, y } = encodings;
const cols = x ? Array.from(group(data, (d) => d[x]).keys()) : [undefined];
const rows = y ? Array.from(group(data, (d) => d[y]).keys()) : [undefined];
const n = cols.length;
const m = rows.length;
const views = [];
const width = box.width - paddingLeft - paddingRight;
const height = box.height - paddingTop - paddingBottom;
const boxWidth = (width - padding * (n - 1)) / n;
const boxHeight = (height - padding * (m - 1)) / m;
for (let i = 0; i < n; i += 1) {
for (let j = 0; j < m; j += 1) {
const transform = (data) => {
const inRow = (d) => d[x] === cols[i] || cols[i] === undefined;
const inCol = (d) => d[y] === rows[j] || rows[j] === undefined;
return data.filter((d) => inRow(d) && inCol(d));
};
views.push({
x: paddingLeft + box.x + padding * i + i * boxWidth,
y: paddingRight + box.y + padding * j + j * boxHeight,
width: boxWidth,
height: boxHeight,
transform,
});
}
}
return views;
}
23 changes: 23 additions & 0 deletions src/view/flex.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
export function computeFlexViews(box, node) {
const {
type, children, flex = children.map(() => 1), padding = 40,
} = node;
const [mainStart, mainSize, crossSize, crossStart] = type === 'col'
? ['y', 'height', 'width', 'x']
: ['x', 'width', 'height', 'y'];

const sum = flex.reduce((total, value) => total + value);
const totalSize = box[mainSize] - padding * (children.length - 1);
const sizes = flex.map((value) => totalSize * (value / sum));

const childrenViews = [];
for (let next = box[mainStart], i = 0; i < sizes.length; next += sizes[i] + padding, i += 1) {
childrenViews.push({
[mainStart]: next,
[mainSize]: sizes[i],
[crossStart]: box[crossStart],
[crossSize]: box[crossSize],
});
}
return childrenViews;
}
1 change: 1 addition & 0 deletions src/view/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { createViews } from './view';
4 changes: 4 additions & 0 deletions src/view/layer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export function computeLayerViews(box, node) {
const { children = [] } = node;
return new Array(children.length).fill(0).map(() => ({ ...box }));
}
48 changes: 48 additions & 0 deletions src/view/view.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { computeFlexViews } from './flex';
import { computeFacetViews } from './facet';
import { computeLayerViews } from './layer';
import { descendants, group } from '../utils';

export function createViews(root, computes = {
layer: computeLayerViews,
col: computeFlexViews,
row: computeFlexViews,
facet: computeFacetViews,
}) {
const nodes = descendants(root);
const {
width = 640, height = 480, x = 0, y = 0,
} = root;
const rootView = {
width, height, x, y,
};
const nodeView = new Map([[root, rootView]]);

for (const node of nodes) {
const view = nodeView.get(node);
const { children = [], type } = node;
const computeChildrenViews = computes[type];
if (computeChildrenViews) {
const childrenViews = computeChildrenViews(view, node);
if (computeChildrenViews !== computeFacetViews) {
for (const [i, child] of Object.entries(children)) {
nodeView.set(child, childrenViews[i]);
}
} else {
for (const child of children) {
for (const view of childrenViews) {
nodeView.set({ ...child }, view);
}
}
}
}
}

const key = (d) => `${d.x}-${d.y}-${d.width}-${d.height}`;
const keyViews = group(Array.from(nodeView.entries()), ([, view]) => key(view));
return Array.from(keyViews.values()).map((views) => {
const view = views[0][1];
const nodes = views.map((d) => d[0]);
return [view, nodes];
});
}

0 comments on commit 4b4f7c1

Please sign in to comment.