@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.
package/.babelrc ADDED
@@ -0,0 +1,14 @@
1
+ {
2
+ "presets": [
3
+ ["@babel/preset-env", { "modules": false }],
4
+ "@babel/react"
5
+ ],
6
+ "env": {
7
+ "test": {
8
+ "presets": [
9
+ ["@babel/preset-env", { "modules": "commonjs" }],
10
+ "@babel/react"
11
+ ]
12
+ }
13
+ }
14
+ }
package/README.md ADDED
@@ -0,0 +1,64 @@
1
+ # @yanqirenshi/d3.sql
2
+
3
+ SQLのリネージ(列の値がどこから来たか)を、パン/ズーム可能なSVG図として
4
+ 描画するReactコンポーネント。レイアウトは d3-dag(sugiyama)、パン/ズームは
5
+ d3.zoom を使う。
6
+
7
+ [foodata](https://github.com/ni-hama/foodata) プロジェクトの
8
+ `hub/src/contents/SqlGraph.js`([sql-sitter](https://github.com/ni-hama/foodata/tree/main/sql-sitter)
9
+ というRust製SQLパーサーが`extract_graph()`で抽出するリネージグラフを
10
+ 表示するために作られたもの)を、モノレポの規約(データクラス/描画クラスの
11
+ 2系統・d3 メタパッケージ・MUI 非依存)に合わせて取り込んだもの。
12
+
13
+ ## クラス構成
14
+
15
+ - `SqlModel`(データクラス) — 入力の正規化(列の自動導出・最終Resultの
16
+ 判定)・自動レイアウト(d3-dag)・線の幾何計算。DOM/React 非依存で、
17
+ テストはこの層に対して行う。
18
+ - `D3Sql`(描画クラス / React ラッパ) — SqlModel の結果を SVG に写す。
19
+ パン/ズーム(d3.zoom)・ラベルboxの実測幅差し替え・ラベルクリックの
20
+ ポップアップを持つ。
21
+
22
+ ## 入力データの形
23
+
24
+ ```js
25
+ {
26
+ entities: [
27
+ {
28
+ id: 't1', // エンティティ内で一意なID(他の要素からの参照キー)
29
+ kind: 'table', // 'table' | 'result'
30
+ title: 'my_table', // 箱の見出し
31
+ alias: null, // FROM句の `AS alias`(箱の上に表示)
32
+ whereText: null, // WHERE/HAVINGの条件テキスト(箱の下に表示)
33
+ columns: ['col_a', 'col_b'], // 箱に表示する列(省略時はedgeから自動導出)
34
+ orderByColumns: [], // ORDER BYのキーに対応する列名(並び順)
35
+ distinct: false, // trueなら箱の右辺にdistinct帯を付ける
36
+ },
37
+ ],
38
+ lineageEdges: [
39
+ { from: { entity: 't1', column: 'col_a' }, to: { entity: 'result1', column: 'out_a' }, label: null },
40
+ ],
41
+ joinEdges: [
42
+ { a: { entity: 't1', column: 'id' }, b: { entity: 't2', column: 'id' }, joinType: 'INNER' },
43
+ ],
44
+ }
45
+ ```
46
+
47
+ データモデルの詳細は `src/js/SqlModel.js` 冒頭のコメントを参照。
48
+
49
+ ## 使い方
50
+
51
+ ```jsx
52
+ import D3Sql from '@yanqirenshi/d3.sql';
53
+
54
+ <div style={{ width: '100%', height: 600 }}>
55
+ <D3Sql data={graph}/>
56
+ </div>
57
+ ```
58
+
59
+ ## 開発
60
+
61
+ ```bash
62
+ npm run transpile -w @yanqirenshi/d3.sql # Babel で dist/ へ(dist はコミット対象)
63
+ npm test -w @yanqirenshi/d3.sql # jest(データクラスのテスト)
64
+ ```
@@ -0,0 +1,538 @@
1
+ var _excluded = ["key"],
2
+ _excluded2 = ["key"];
3
+ function _extends() { return _extends = Object.assign ? Object.assign.bind() : function (n) { for (var e = 1; e < arguments.length; e++) { var t = arguments[e]; for (var r in t) ({}).hasOwnProperty.call(t, r) && (n[r] = t[r]); } return n; }, _extends.apply(null, arguments); }
4
+ function _objectWithoutProperties(e, t) { if (null == e) return {}; var o, r, i = _objectWithoutPropertiesLoose(e, t); if (Object.getOwnPropertySymbols) { var n = Object.getOwnPropertySymbols(e); for (r = 0; r < n.length; r++) o = n[r], -1 === t.indexOf(o) && {}.propertyIsEnumerable.call(e, o) && (i[o] = e[o]); } return i; }
5
+ function _objectWithoutPropertiesLoose(r, e) { if (null == r) return {}; var t = {}; for (var n in r) if ({}.hasOwnProperty.call(r, n)) { if (-1 !== e.indexOf(n)) continue; t[n] = r[n]; } return t; }
6
+ function _slicedToArray(r, e) { return _arrayWithHoles(r) || _iterableToArrayLimit(r, e) || _unsupportedIterableToArray(r, e) || _nonIterableRest(); }
7
+ function _nonIterableRest() { throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); }
8
+ function _unsupportedIterableToArray(r, a) { if (r) { if ("string" == typeof r) return _arrayLikeToArray(r, a); var t = {}.toString.call(r).slice(8, -1); return "Object" === t && r.constructor && (t = r.constructor.name), "Map" === t || "Set" === t ? Array.from(r) : "Arguments" === t || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(t) ? _arrayLikeToArray(r, a) : void 0; } }
9
+ function _arrayLikeToArray(r, a) { (null == a || a > r.length) && (a = r.length); for (var e = 0, n = Array(a); e < a; e++) n[e] = r[e]; return n; }
10
+ function _iterableToArrayLimit(r, l) { var t = null == r ? null : "undefined" != typeof Symbol && r[Symbol.iterator] || r["@@iterator"]; if (null != t) { var e, n, i, u, a = [], f = !0, o = !1; try { if (i = (t = t.call(r)).next, 0 === l) { if (Object(t) !== t) return; f = !1; } else for (; !(f = (e = i.call(t)).done) && (a.push(e.value), a.length !== l); f = !0); } catch (r) { o = !0, n = r; } finally { try { if (!f && null != t["return"] && (u = t["return"](), Object(u) !== u)) return; } finally { if (o) throw n; } } return a; } }
11
+ function _arrayWithHoles(r) { if (Array.isArray(r)) return r; }
12
+ // D3Sql — SQL リネージグラフの React ラッパ(描画側)。
13
+ // データの正規化・レイアウト・線の幾何は SqlModel(データクラス)が持ち、
14
+ // ここでは model の結果を SVG に写すことに徹する。
15
+ // パン/ズームは d3.zoom に任せ(カーソル位置基準のズーム計算とイベント
16
+ // 付与)、React はその結果(transform)を <g> の translate/scale として
17
+ // 描画するだけ(DOM 操作自体は d3 に持たせない)。
18
+
19
+ import React, { useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';
20
+ import * as d3 from 'd3';
21
+ import SqlModel, { HEADER_H, ROW_H, BRANCH_GAP, DISTINCT_BAND_WIDTH } from '../js/SqlModel.js';
22
+ var ZOOM_MIN = 0.2;
23
+ var ZOOM_MAX = 4;
24
+ var INITIAL_PADDING = 40;
25
+
26
+ // select句の線のラベルboxをクリックした時のポップアップ内容。内容は
27
+ // 単純なテキストなので通常のReact/HTMLで表示する。
28
+ function LineageLabelPopup(_ref) {
29
+ var info = _ref.info,
30
+ onClose = _ref.onClose;
31
+ if (!info) return null;
32
+ return /*#__PURE__*/React.createElement("div", {
33
+ onClick: onClose,
34
+ style: {
35
+ position: 'fixed',
36
+ inset: 0,
37
+ zIndex: 1000,
38
+ background: 'rgba(0,0,0,0.35)',
39
+ display: 'flex',
40
+ alignItems: 'center',
41
+ justifyContent: 'center'
42
+ }
43
+ }, /*#__PURE__*/React.createElement("div", {
44
+ onClick: function onClick(e) {
45
+ return e.stopPropagation();
46
+ },
47
+ style: {
48
+ background: '#ffffff',
49
+ borderRadius: 6,
50
+ padding: '16px 20px',
51
+ minWidth: 280,
52
+ maxWidth: 420,
53
+ boxShadow: '0 4px 24px rgba(0,0,0,0.3)'
54
+ }
55
+ }, /*#__PURE__*/React.createElement("div", {
56
+ style: {
57
+ display: 'flex',
58
+ justifyContent: 'space-between',
59
+ alignItems: 'baseline'
60
+ }
61
+ }, /*#__PURE__*/React.createElement("h3", {
62
+ style: {
63
+ margin: 0,
64
+ fontSize: 16
65
+ }
66
+ }, info.label), /*#__PURE__*/React.createElement("button", {
67
+ onClick: onClose,
68
+ style: {
69
+ border: 'none',
70
+ background: 'none',
71
+ fontSize: 18,
72
+ cursor: 'pointer',
73
+ lineHeight: 1
74
+ }
75
+ }, "\xD7")), /*#__PURE__*/React.createElement("table", {
76
+ style: {
77
+ marginTop: 12,
78
+ width: '100%',
79
+ borderCollapse: 'collapse',
80
+ fontSize: 13
81
+ }
82
+ }, /*#__PURE__*/React.createElement("tbody", null, /*#__PURE__*/React.createElement("tr", null, /*#__PURE__*/React.createElement("td", {
83
+ style: {
84
+ padding: '4px 6px',
85
+ color: '#999999'
86
+ }
87
+ }, "\u5909\u63DB\u5143"), /*#__PURE__*/React.createElement("td", {
88
+ style: {
89
+ padding: '4px 6px'
90
+ }
91
+ }, info.fromDisplay || (info.fromTitle ? "".concat(info.fromTitle, ".").concat(info.fromColumn) : 'リテラル値/由来なし'))), /*#__PURE__*/React.createElement("tr", null, /*#__PURE__*/React.createElement("td", {
92
+ style: {
93
+ padding: '4px 6px',
94
+ color: '#999999'
95
+ }
96
+ }, "\u5909\u63DB\u5148"), /*#__PURE__*/React.createElement("td", {
97
+ style: {
98
+ padding: '4px 6px'
99
+ }
100
+ }, info.toTitle, ".", info.toColumn)))), info.sql && /*#__PURE__*/React.createElement("pre", {
101
+ style: {
102
+ marginTop: 12,
103
+ padding: '8px 10px',
104
+ background: '#f5f5f5',
105
+ borderRadius: 4,
106
+ fontSize: 12,
107
+ whiteSpace: 'pre-wrap',
108
+ wordBreak: 'break-all'
109
+ }
110
+ }, info.sql)));
111
+ }
112
+
113
+ // 「Result N」(番号付き、中間サブクエリの出力)は通常の太さ、それ以外
114
+ // (実テーブル名や番号無しの最終「Result」)は太字のままにする。
115
+ var RESULT_N_TITLE_RE = /^Result \d+$/;
116
+
117
+ // エンティティ1件の箱(ヘッダ・列行・DISTINCT帯・ORDER BY番号)を描く。
118
+ function EntityBox(_ref2) {
119
+ var box = _ref2.box;
120
+ var titleWeight = RESULT_N_TITLE_RE.test(box.title) ? 'normal' : 'bold';
121
+ return /*#__PURE__*/React.createElement("g", null, box.alias && /*#__PURE__*/React.createElement("text", {
122
+ x: box.x,
123
+ y: box.y - 8,
124
+ fontSize: 12,
125
+ fill: "#666666"
126
+ }, box.alias), /*#__PURE__*/React.createElement("rect", {
127
+ x: box.x,
128
+ y: box.y,
129
+ width: box.w,
130
+ height: box.h,
131
+ fill: "#ffffff",
132
+ stroke: "#333333",
133
+ strokeWidth: 1
134
+ }), /*#__PURE__*/React.createElement("rect", {
135
+ x: box.x,
136
+ y: box.y,
137
+ width: box.w,
138
+ height: HEADER_H,
139
+ fill: box.headerColor || '#e8e8e8',
140
+ stroke: "#333333",
141
+ strokeWidth: 1
142
+ }), /*#__PURE__*/React.createElement("text", {
143
+ x: box.x + box.w / 2,
144
+ y: box.y + HEADER_H / 2,
145
+ textAnchor: "middle",
146
+ dominantBaseline: "central",
147
+ fontWeight: titleWeight,
148
+ fontSize: 14,
149
+ fill: box.headerTextColor || '#000000'
150
+ }, box.title), box.rows.map(function (row, i) {
151
+ return /*#__PURE__*/React.createElement("g", {
152
+ key: row.name
153
+ }, i > 0 && /*#__PURE__*/React.createElement("line", {
154
+ x1: box.x,
155
+ x2: box.x + box.w,
156
+ y1: box.y + HEADER_H + i * ROW_H,
157
+ y2: box.y + HEADER_H + i * ROW_H,
158
+ stroke: "#cccccc",
159
+ strokeWidth: 1
160
+ }), /*#__PURE__*/React.createElement("text", {
161
+ x: box.x + 10,
162
+ y: row.y,
163
+ dominantBaseline: "central",
164
+ fontSize: 13
165
+ }, row.name));
166
+ }), box.distinct && /*#__PURE__*/React.createElement("g", null, /*#__PURE__*/React.createElement("rect", {
167
+ x: box.x + box.w + BRANCH_GAP,
168
+ y: box.y + HEADER_H,
169
+ width: DISTINCT_BAND_WIDTH,
170
+ height: box.h - HEADER_H,
171
+ fill: "#ffffff",
172
+ stroke: "#333333",
173
+ strokeWidth: 1
174
+ }), /*#__PURE__*/React.createElement("text", {
175
+ x: box.x + box.w + BRANCH_GAP + DISTINCT_BAND_WIDTH / 2,
176
+ y: box.y + HEADER_H + (box.h - HEADER_H) / 2,
177
+ textAnchor: "middle",
178
+ dominantBaseline: "central",
179
+ fontSize: 11,
180
+ fill: "#666666",
181
+ transform: "rotate(-90 ".concat(box.x + box.w + BRANCH_GAP + DISTINCT_BAND_WIDTH / 2, " ").concat(box.y + HEADER_H + (box.h - HEADER_H) / 2, ")")
182
+ }, box.bandLabel)), box.orderByColumns && box.rows.map(function (row) {
183
+ var seq = box.orderByColumns.indexOf(row.name);
184
+ if (seq === -1) return null;
185
+ // ORDER BYのキー列は、その行内・箱の右端寄りに順序番号のbox
186
+ // を付ける。box自体の見た目(fontSize/高さ)はリネージ線の
187
+ // ラベルbox(LineageLabelBox)と揃える。
188
+ var width = 20;
189
+ var height = 16;
190
+ var rightMargin = 6;
191
+ var cx = box.x + box.w - rightMargin - width / 2;
192
+ return /*#__PURE__*/React.createElement("g", {
193
+ key: "orderby:".concat(row.name)
194
+ }, /*#__PURE__*/React.createElement("rect", {
195
+ x: cx - width / 2,
196
+ y: row.y - height / 2,
197
+ width: width,
198
+ height: height,
199
+ fill: "#ffffff",
200
+ stroke: "#999999",
201
+ strokeWidth: 1
202
+ }), /*#__PURE__*/React.createElement("text", {
203
+ x: cx,
204
+ y: row.y,
205
+ textAnchor: "middle",
206
+ dominantBaseline: "central",
207
+ fontSize: 11,
208
+ fill: "#333333"
209
+ }, seq + 1));
210
+ }));
211
+ }
212
+
213
+ // ラベルboxの幅は、描画してみないと正確な文字幅が分からない(全角/半角の
214
+ // 混在や実際のフォントメトリクスに依存するため)。SVGの<text>は描画後に
215
+ // `getComputedTextLength()`で実測できるので、初期描画は仮の幅で出し、
216
+ // レイアウト確定後(ペイント前)に実測値へ差し替える。
217
+ // `sources`({x,y}の配列)が渡された場合は、box右辺からそれぞれへ斜めの
218
+ // 線を引く。複数列を1つの式(例: ||連結)で結合した場合は、1つのラベル
219
+ // boxから複数本の斜め線が出る形になる。
220
+ function LineageLabelBox(_ref3) {
221
+ var x = _ref3.x,
222
+ y = _ref3.y,
223
+ label = _ref3.label,
224
+ isGroupBy = _ref3.isGroupBy,
225
+ sources = _ref3.sources,
226
+ onClick = _ref3.onClick;
227
+ var textRef = useRef(null);
228
+ var _useState = useState(label.length * 10 + 8),
229
+ _useState2 = _slicedToArray(_useState, 2),
230
+ width = _useState2[0],
231
+ setWidth = _useState2[1];
232
+ useLayoutEffect(function () {
233
+ if (textRef.current) {
234
+ setWidth(textRef.current.getComputedTextLength() + 8);
235
+ }
236
+ }, [label]);
237
+ return /*#__PURE__*/React.createElement(React.Fragment, null, sources && sources.map(function (s, i) {
238
+ return /*#__PURE__*/React.createElement("line", {
239
+ key: i,
240
+ x1: x + width,
241
+ y1: y,
242
+ x2: s.x,
243
+ y2: s.y,
244
+ stroke: "#666666",
245
+ strokeWidth: 1.5
246
+ });
247
+ }), /*#__PURE__*/React.createElement("g", {
248
+ onClick: onClick,
249
+ style: {
250
+ cursor: onClick ? 'pointer' : 'default'
251
+ }
252
+ }, /*#__PURE__*/React.createElement("rect", {
253
+ x: x,
254
+ y: y - 8,
255
+ width: width,
256
+ height: 16,
257
+ fill: "#ffffff",
258
+ stroke: isGroupBy ? 'none' : '#999999',
259
+ strokeWidth: 1
260
+ }), /*#__PURE__*/React.createElement("text", {
261
+ ref: textRef,
262
+ x: x + 4,
263
+ y: y,
264
+ textAnchor: "start",
265
+ dominantBaseline: "central",
266
+ fontSize: 11,
267
+ fill: "#333333"
268
+ }, label)));
269
+ }
270
+
271
+ // リネージ矢印: 常にsource(右)からResult側(左)へ向く。ラベルが無い
272
+ // (単純な値渡し)場合はsourceからdestinationへの直線1本、sourceの左辺に
273
+ // ポート(小さい丸)、destination側に矢印。ラベルがある場合(GROUP BYキー
274
+ // 番号や集約/関数名)は、ラベルboxを境にResult側は水平な矢印、source側は
275
+ // 斜めの線という2本の折れ線にする。
276
+ // GROUP BYキー番号(例: G01)は枠線無し、集約/関数名などその他のラベルは
277
+ // 枠線付きのboxにする。
278
+ // リテラル値・列を取らない関数呼び出し(由来となる実テーブルが無い列)は
279
+ // `from`が無いエッジとして表し、ラベルboxからResultへの水平な矢印だけを
280
+ // 描く(ポートの丸・source側の線は無し)。
281
+ var GROUP_BY_LABEL_RE = /^G\d+$/;
282
+ function LineageEdge(_ref4) {
283
+ var x1 = _ref4.x1,
284
+ y1 = _ref4.y1,
285
+ x2 = _ref4.x2,
286
+ y2 = _ref4.y2,
287
+ sources = _ref4.sources,
288
+ label = _ref4.label,
289
+ labelX = _ref4.labelX,
290
+ labelInfo = _ref4.labelInfo,
291
+ onLabelClick = _ref4.onLabelClick,
292
+ _ref4$showPort = _ref4.showPort,
293
+ showPort = _ref4$showPort === void 0 ? true : _ref4$showPort;
294
+ var lx = labelX !== undefined ? labelX : (x1 + x2) / 2;
295
+ var isGroupBy = label && GROUP_BY_LABEL_RE.test(label);
296
+ if (!label) {
297
+ return /*#__PURE__*/React.createElement("g", null, /*#__PURE__*/React.createElement("line", {
298
+ x1: x1,
299
+ y1: y1,
300
+ x2: x2,
301
+ y2: y2,
302
+ stroke: "#666666",
303
+ strokeWidth: 1.5,
304
+ markerEnd: "url(#d3-sql-arrow)"
305
+ }), showPort && /*#__PURE__*/React.createElement("circle", {
306
+ cx: x1,
307
+ cy: y1,
308
+ r: 3,
309
+ fill: "#666666"
310
+ }));
311
+ }
312
+
313
+ // `sources`(複数列を1つの式で結合した場合の由来一覧)が無ければ、
314
+ // 単一source(x1,y1)を1件だけの配列として扱う。
315
+ var sourcePoints = sources || (showPort ? [{
316
+ x: x1,
317
+ y: y1
318
+ }] : []);
319
+ return /*#__PURE__*/React.createElement("g", null, /*#__PURE__*/React.createElement("line", {
320
+ x1: lx,
321
+ y1: y2,
322
+ x2: x2,
323
+ y2: y2,
324
+ stroke: "#666666",
325
+ strokeWidth: 1.5,
326
+ markerEnd: "url(#d3-sql-arrow)"
327
+ }), sourcePoints.map(function (p, i) {
328
+ return /*#__PURE__*/React.createElement("circle", {
329
+ key: i,
330
+ cx: p.x,
331
+ cy: p.y,
332
+ r: 3,
333
+ fill: "#666666"
334
+ });
335
+ }), /*#__PURE__*/React.createElement(LineageLabelBox, {
336
+ x: lx,
337
+ y: y2,
338
+ label: label,
339
+ isGroupBy: isGroupBy,
340
+ sources: sourcePoints,
341
+ onClick: function onClick() {
342
+ return onLabelClick && onLabelClick(labelInfo);
343
+ }
344
+ }));
345
+ }
346
+
347
+ // JOIN線・IN線用の破線パターン(JOINとINは間隔を変えて見分けられるように
348
+ // する)。"線の長さ 隙間の長さ"の順。
349
+ var JOIN_DASH = '8 5';
350
+ var IN_DASH = '3 3';
351
+
352
+ // JOIN条件の線: データの変換ではなく「一致させる列同士」なので、aの左辺から
353
+ // 一度左に出てから下へ、bの左辺へ横に入るエルボー型の折れ線で描く。
354
+ // 破線にすることでリネージ矢印(直線・実線)と区別する。
355
+ // 矢印の向きはJOIN種別を表す: LEFT=b側のみ矢印、RIGHT=a側のみ矢印、
356
+ // INNER=矢印無し。
357
+ // `WHERE col IN (サブクエリ)`のような、FROM句のJOINではない「列の一致」も
358
+ // 同じ形の線で描くが、JOINと見分けられるよう`label`(例:"IN")と、破線の間隔
359
+ // (`kind:'in'`のときはJOINより細かい間隔)で区別する。
360
+ // エルボーの4点を通る折れ線のd属性はd3のline generatorで作る。
361
+ var elbowLine = d3.line();
362
+ function JoinEdge(_ref5) {
363
+ var x1 = _ref5.x1,
364
+ y1 = _ref5.y1,
365
+ x2 = _ref5.x2,
366
+ y2 = _ref5.y2,
367
+ joinType = _ref5.joinType,
368
+ branchX = _ref5.branchX,
369
+ label = _ref5.label,
370
+ kind = _ref5.kind;
371
+ var d = elbowLine([[x1, y1], [branchX, y1], [branchX, y2], [x2, y2]]);
372
+ var dashArray = kind === 'in' ? IN_DASH : JOIN_DASH;
373
+ var markerStart = joinType === 'RIGHT' ? 'url(#d3-sql-arrow)' : undefined;
374
+ var markerEnd = joinType === 'LEFT' ? 'url(#d3-sql-arrow)' : undefined;
375
+ var my = (y1 + y2) / 2;
376
+ return /*#__PURE__*/React.createElement("g", null, /*#__PURE__*/React.createElement("path", {
377
+ d: d,
378
+ fill: "none",
379
+ stroke: "#999999",
380
+ strokeWidth: 1.5,
381
+ strokeDasharray: dashArray,
382
+ markerStart: markerStart,
383
+ markerEnd: markerEnd
384
+ }), label && /*#__PURE__*/React.createElement("g", null, /*#__PURE__*/React.createElement("rect", {
385
+ x: branchX - 12,
386
+ y: my - 9,
387
+ width: 24,
388
+ height: 16,
389
+ fill: "#ffffff"
390
+ }), /*#__PURE__*/React.createElement("text", {
391
+ x: branchX,
392
+ y: my,
393
+ textAnchor: "middle",
394
+ dominantBaseline: "central",
395
+ fontSize: 11,
396
+ fill: "#333333"
397
+ }, label)));
398
+ }
399
+
400
+ // マウスホイールでのズームとドラッグでの視点移動をd3.zoomに任せるフック。
401
+ // `initialTransform`({x,y,k})が渡された場合、マウント時にd3.zoom自身の
402
+ // 内部状態もそれで初期化する(d3の内部状態も合わせないと、最初の操作で
403
+ // transformが(0,0,1)に巻き戻ってしまう)。
404
+ function usePanZoom(initialTransform) {
405
+ var containerRef = useRef(null);
406
+ var _useState3 = useState(initialTransform || {
407
+ x: 0,
408
+ y: 0,
409
+ k: 1
410
+ }),
411
+ _useState4 = _slicedToArray(_useState3, 2),
412
+ transform = _useState4[0],
413
+ setTransform = _useState4[1];
414
+ var _useState5 = useState(false),
415
+ _useState6 = _slicedToArray(_useState5, 2),
416
+ isDragging = _useState6[0],
417
+ setIsDragging = _useState6[1];
418
+ useEffect(function () {
419
+ var el = containerRef.current;
420
+ if (!el) return;
421
+ var zoomBehavior = d3.zoom().scaleExtent([ZOOM_MIN, ZOOM_MAX]).on('start', function () {
422
+ return setIsDragging(true);
423
+ }).on('zoom', function (event) {
424
+ return setTransform({
425
+ x: event.transform.x,
426
+ y: event.transform.y,
427
+ k: event.transform.k
428
+ });
429
+ }).on('end', function () {
430
+ return setIsDragging(false);
431
+ });
432
+ var selection = d3.select(el);
433
+ selection.call(zoomBehavior);
434
+ if (initialTransform) {
435
+ zoomBehavior.transform(selection, d3.zoomIdentity.translate(initialTransform.x, initialTransform.y).scale(initialTransform.k));
436
+ }
437
+ return function () {
438
+ return selection.on('.zoom', null);
439
+ };
440
+ // eslint-disable-next-line react-hooks/exhaustive-deps -- 初期transformは
441
+ // マウント時に1回だけ適用する(依存に入れるとズーム操作のたびに戻ってしまう)。
442
+ }, []);
443
+ return {
444
+ containerRef: containerRef,
445
+ transform: transform,
446
+ isDragging: isDragging
447
+ };
448
+ }
449
+ export default function D3Sql(_ref6) {
450
+ var data = _ref6.data;
451
+ // 正規化・レイアウト・線の幾何はデータクラス(SqlModel)に任せる。
452
+ var model = useMemo(function () {
453
+ return new SqlModel(data);
454
+ }, [data]);
455
+ var _usePanZoom = usePanZoom(model.initialTransform(INITIAL_PADDING)),
456
+ containerRef = _usePanZoom.containerRef,
457
+ transform = _usePanZoom.transform,
458
+ isDragging = _usePanZoom.isDragging;
459
+ var _useState7 = useState(null),
460
+ _useState8 = _slicedToArray(_useState7, 2),
461
+ selectedLabel = _useState8[0],
462
+ setSelectedLabel = _useState8[1];
463
+ return /*#__PURE__*/React.createElement("div", {
464
+ ref: containerRef,
465
+ style: {
466
+ width: '100%',
467
+ height: '100%',
468
+ overflow: 'hidden',
469
+ cursor: isDragging ? 'grabbing' : 'grab',
470
+ userSelect: 'none'
471
+ }
472
+ }, /*#__PURE__*/React.createElement("svg", {
473
+ width: "100%",
474
+ height: "100%"
475
+ }, /*#__PURE__*/React.createElement("defs", null, /*#__PURE__*/React.createElement("marker", {
476
+ id: "d3-sql-arrow",
477
+ viewBox: "0 0 10 10",
478
+ refX: "9",
479
+ refY: "5",
480
+ markerWidth: 7,
481
+ markerHeight: 7,
482
+ orient: "auto-start-reverse"
483
+ }, /*#__PURE__*/React.createElement("path", {
484
+ d: "M 0 0 L 10 5 L 0 10 z",
485
+ fill: "#666666"
486
+ }))), /*#__PURE__*/React.createElement("g", {
487
+ transform: "translate(".concat(transform.x, ",").concat(transform.y, ") scale(").concat(transform.k, ")")
488
+ }, model.joinLines.map(function (_ref7) {
489
+ var key = _ref7.key,
490
+ edge = _objectWithoutProperties(_ref7, _excluded);
491
+ return /*#__PURE__*/React.createElement(JoinEdge, _extends({
492
+ key: key
493
+ }, edge));
494
+ }), model.lineageLines.map(function (_ref8) {
495
+ var key = _ref8.key,
496
+ edge = _objectWithoutProperties(_ref8, _excluded2);
497
+ return /*#__PURE__*/React.createElement(LineageEdge, _extends({
498
+ key: key
499
+ }, edge, {
500
+ onLabelClick: setSelectedLabel
501
+ }));
502
+ }), model.boxList().map(function (box) {
503
+ return /*#__PURE__*/React.createElement(EntityBox, {
504
+ key: box.id,
505
+ box: box
506
+ });
507
+ }), model.boxList().filter(function (box) {
508
+ return box.whereText;
509
+ }).map(function (box) {
510
+ // WHERE/ORDER BYなど、そのエンティティのSELECTスコープに
511
+ // 付随する句のテキストは、そのエンティティ自身のxに揃え、
512
+ // その箱の下端から22px下に配置する(サブクエリごとに別々の
513
+ // WHEREを持つ場合は、それぞれのResult/実テーブルの直下に
514
+ // 表示する)。
515
+ var textX = box.x;
516
+ var textY = box.y + box.h + 22;
517
+ return /*#__PURE__*/React.createElement("text", {
518
+ key: "clause:".concat(box.id),
519
+ x: textX,
520
+ y: textY,
521
+ fontFamily: "monospace",
522
+ fontSize: 13,
523
+ fill: "#333333",
524
+ xmlSpace: "preserve"
525
+ }, box.whereText.split('\n').map(function (line, i) {
526
+ return /*#__PURE__*/React.createElement("tspan", {
527
+ key: i,
528
+ x: textX,
529
+ dy: i === 0 ? 0 : 18
530
+ }, line);
531
+ }));
532
+ }))), /*#__PURE__*/React.createElement(LineageLabelPopup, {
533
+ info: selectedLabel,
534
+ onClose: function onClose() {
535
+ return setSelectedLabel(null);
536
+ }
537
+ }));
538
+ }
package/dist/index.js ADDED
@@ -0,0 +1,4 @@
1
+ import D3Sql from './components/D3Sql.js';
2
+ import SqlModel from './js/SqlModel.js';
3
+ export { SqlModel };
4
+ export default D3Sql;