@yanqirenshi/d3.sql 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,135 @@
1
+ import SqlModel, {
2
+ withDerivedColumns,
3
+ markFinalResult,
4
+ groupByJoin,
5
+ orderByJoinAB,
6
+ estimateLabelWidth,
7
+ buildLayout,
8
+ buildJoinLines,
9
+ buildLineageLines,
10
+ findRowY,
11
+ HEADER_H,
12
+ ROW_H,
13
+ BOX_W,
14
+ } from '../src/js/SqlModel.js';
15
+
16
+ describe('withDerivedColumns', () => {
17
+ it('columns 未指定なら lineage/join で参照される列を初出順に導出する', () => {
18
+ const entities = [{ id: 't1', kind: 'table', title: 'a' }, { id: 'r1', kind: 'result', title: 'Result 1' }];
19
+ const lineage = [
20
+ { from: { entity: 't1', column: 'x' }, to: { entity: 'r1', column: 'out_x' } },
21
+ { from: { entity: 't1', column: 'y' }, to: { entity: 'r1', column: 'out_y' } },
22
+ ];
23
+ const joins = [{ a: { entity: 't1', column: 'id' }, b: { entity: 'r1', column: 'ref' } }];
24
+
25
+ const [t1, r1] = withDerivedColumns(entities, lineage, joins);
26
+ expect(t1.columns).toEqual(['x', 'y', 'id']);
27
+ expect(r1.columns).toEqual(['out_x', 'out_y', 'ref']);
28
+ });
29
+
30
+ it('明示された columns を優先し、from が配列/null のエッジも扱える', () => {
31
+ const entities = [{ id: 't1', kind: 'table', title: 'a', columns: ['own'] }, { id: 'r1', kind: 'result', title: 'Result 1' }];
32
+ const lineage = [
33
+ { from: [{ entity: 't1', column: 'x' }, { entity: 't1', column: 'y' }], to: { entity: 'r1', column: 'joined' } },
34
+ { from: null, to: { entity: 'r1', column: 'lit' }, label: "'L'" },
35
+ ];
36
+
37
+ const [t1, r1] = withDerivedColumns(entities, lineage, []);
38
+ expect(t1.columns).toEqual(['own']); // 明示 columns 優先
39
+ expect(r1.columns).toEqual(['joined', 'lit']);
40
+ });
41
+ });
42
+
43
+ describe('markFinalResult', () => {
44
+ it('一番番号の大きい Result N を黒背景白文字の「Result」にする', () => {
45
+ const entities = [
46
+ { id: 'r1', kind: 'result', title: 'Result 1' },
47
+ { id: 'r2', kind: 'result', title: 'Result 2' },
48
+ { id: 't1', kind: 'table', title: 'a' },
49
+ ];
50
+ const marked = markFinalResult(entities);
51
+
52
+ expect(marked[0].title).toBe('Result 1'); // 中間はそのまま
53
+ expect(marked[1].title).toBe('Result');
54
+ expect(marked[1].headerColor).toBe('#000000');
55
+ expect(marked[1].headerTextColor).toBe('#ffffff');
56
+ expect(marked[2].title).toBe('a');
57
+ });
58
+ });
59
+
60
+ describe('groupByJoin / orderByJoinAB', () => {
61
+ it('JOIN で繋がるエンティティを同じグループにまとめる', () => {
62
+ const entities = [{ id: 'a' }, { id: 'b' }, { id: 'c' }];
63
+ const joins = [{ a: { entity: 'a', column: 'id' }, b: { entity: 'b', column: 'id' } }];
64
+
65
+ const { find, groups } = groupByJoin(entities, joins);
66
+ expect(find('a')).toBe(find('b'));
67
+ expect(find('a')).not.toBe(find('c'));
68
+ expect(groups[find('a')].map((e) => e.id).sort()).toEqual(['a', 'b']);
69
+ });
70
+
71
+ it('JOIN の a 側を上、b 側を下に並べる', () => {
72
+ const joins = [{ a: { entity: 'a', column: 'id' }, b: { entity: 'b', column: 'id' } }];
73
+ expect(orderByJoinAB(['b', 'a'], joins)).toEqual(['a', 'b']);
74
+ });
75
+ });
76
+
77
+ describe('estimateLabelWidth', () => {
78
+ it('全角文字は半角より広く見積もる', () => {
79
+ expect(estimateLabelWidth(null)).toBe(0);
80
+ expect(estimateLabelWidth('AB')).toBe(8 + 7 * 2);
81
+ expect(estimateLabelWidth('集約')).toBe(8 + 13 * 2);
82
+ });
83
+ });
84
+
85
+ describe('buildLayout / findRowY / 線の幾何', () => {
86
+ const entities = [
87
+ { id: 't1', kind: 'table', title: 'orders', columns: ['id', 'amount'] },
88
+ { id: 'r1', kind: 'result', title: 'Result', columns: ['total'] },
89
+ ];
90
+ const positions = { t1: { x: 400, y: 0 }, r1: { x: 0, y: 0 } };
91
+ const boxes = buildLayout(entities, positions, { orders: '#4e79a7' });
92
+
93
+ it('箱の寸法と行のYを計算する(ヘッダ + 行数 × 行高)', () => {
94
+ expect(boxes.t1.w).toBe(BOX_W);
95
+ expect(boxes.t1.h).toBe(HEADER_H + 2 * ROW_H);
96
+ expect(boxes.t1.headerColor).toBe('#4e79a7'); // table はテーブル色
97
+ expect(boxes.r1.headerColor).toBeNull(); // result は既定
98
+ expect(findRowY(boxes.t1, 'id')).toBe(HEADER_H + ROW_H / 2);
99
+ expect(findRowY(boxes.t1, 'amount')).toBe(HEADER_H + ROW_H + ROW_H / 2);
100
+ expect(findRowY(boxes.t1, 'missing')).toBeNull();
101
+ });
102
+
103
+ it('JOIN 線は a の左に分岐点(branchX)を持ち、索引に登録される', () => {
104
+ const { lines, branchPointByPort } = buildJoinLines(boxes, [
105
+ { a: { entity: 't1', column: 'id' }, b: { entity: 'r1', column: 'total' }, joinType: 'INNER' },
106
+ ]);
107
+
108
+ expect(lines).toHaveLength(1);
109
+ expect(lines[0].branchX).toBe(boxes.t1.x - 20);
110
+ expect(branchPointByPort['t1.id']).toBe(lines[0].branchX);
111
+ });
112
+
113
+ it('リネージ線は from → to の座標を持ち、from: null はラベルのみの矢印になる', () => {
114
+ const lines = buildLineageLines(boxes, [
115
+ { from: { entity: 't1', column: 'amount' }, to: { entity: 'r1', column: 'total' }, label: 'SUM' },
116
+ { from: null, to: { entity: 'r1', column: 'total' }, label: "'0'" },
117
+ ], {});
118
+
119
+ expect(lines).toHaveLength(2);
120
+ expect(lines[0].sources[0].y).toBe(findRowY(boxes.t1, 'amount'));
121
+ expect(lines[0].x2).toBe(boxes.r1.x + boxes.r1.w);
122
+ expect(lines[1].showPort).toBe(false); // リテラル値は由来なし
123
+ });
124
+ });
125
+
126
+ describe('SqlModel', () => {
127
+ it('データ無しでも空のモデルとして構築できる(描画側の null ガード用)', () => {
128
+ const model = new SqlModel(null);
129
+
130
+ expect(model.boxList()).toEqual([]);
131
+ expect(model.joinLines).toEqual([]);
132
+ expect(model.lineageLines).toEqual([]);
133
+ expect(model.initialTransform()).toEqual({ x: 0, y: 0, k: 1 });
134
+ });
135
+ });
@@ -0,0 +1,4 @@
1
+ // jest 用の d3-dag スタブ(ESM-only のため CJS の jest では読めない)。
2
+ // 自動レイアウト(buildAutoPositions)以外のデータ層は d3-dag を使わない。
3
+ // d3-dag を通るレイアウトはブラウザ検証で担保する。
4
+ module.exports = {};
@@ -0,0 +1,19 @@
1
+ // jest 用の d3 スタブ。
2
+ // d3 v7 は ESM-only のため CJS ランタイムの jest では読み込めない。
3
+ // テスト対象(データクラス)のうち色割り当て(scaleOrdinal)だけが d3 を
4
+ // 使うので、決定的な単純スタブを返す。
5
+ module.exports = {
6
+ scaleOrdinal: (scheme) => {
7
+ const assigned = {};
8
+ let i = 0;
9
+ const scale = (key) => {
10
+ if (!(key in assigned))
11
+ assigned[key] = scheme[i++ % scheme.length];
12
+ return assigned[key];
13
+ };
14
+ scale.domain = () => scale;
15
+ return scale;
16
+ },
17
+ schemeTableau10: ['#4e79a7', '#f28e2c', '#e15759', '#76b7b2', '#59a14f',
18
+ '#edc949', '#af7aa1', '#ff9da7', '#9c755f', '#bab0ab'],
19
+ };