@peter.prib/renderengine 0.2.7 → 0.2.11
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/README.md +66 -9
- package/REFERENCE.md +129 -0
- package/dist/renderengine-workspace.umd.js +5068 -2892
- package/dist/renderengine.umd.js +4874 -2888
- package/examples/data-model.html +40 -18
- package/package.json +1 -1
- package/src/charting/FormDialog.js +398 -385
- package/src/charting/RowsField.js +127 -0
- package/src/charting/datamodel/ClassificationLens.js +33 -0
- package/src/charting/datamodel/ClassificationScheme.js +98 -0
- package/src/charting/datamodel/DataModel.js +220 -4
- package/src/charting/datamodel/DataModelChecker.js +244 -2
- package/src/charting/datamodel/DataModelContext.js +40 -10
- package/src/charting/datamodel/DataModelEditCommand.js +47 -0
- package/src/charting/datamodel/DataModelPerspective.js +12 -2
- package/src/charting/datamodel/EnumerationType.js +64 -51
- package/src/charting/datamodel/LevelSchema.js +23 -0
- package/src/charting/datamodel/ServiceDomain.js +9 -0
- package/src/charting/datamodel/SqlDdl.js +85 -0
- package/src/charting/datamodel/SqlDdlFileFormat.js +23 -0
- package/src/charting/datamodel/index.js +21 -15
- package/src/charting/datamodel/naming/CamelCaseStyle.js +13 -0
- package/src/charting/datamodel/naming/LowerWordsStyle.js +13 -0
- package/src/charting/datamodel/naming/NameStyle.js +57 -0
- package/src/charting/datamodel/naming/NamingStandard.js +132 -0
- package/src/charting/datamodel/naming/PascalCaseStyle.js +13 -0
- package/src/charting/datamodel/naming/SnakeCaseStyle.js +13 -0
- package/src/charting/datamodel/naming/TitleCaseStyle.js +25 -0
- package/src/charting/datamodel/naming/UpperSnakeStyle.js +13 -0
- package/src/charting/datamodel/naming/index.js +8 -0
- package/src/charting/datamodel/scorecard/CompletenessCategory.js +30 -0
- package/src/charting/datamodel/scorecard/ConsistencyCategory.js +33 -0
- package/src/charting/datamodel/scorecard/DefinitionsCategory.js +33 -0
- package/src/charting/datamodel/scorecard/ModelScorecard.js +113 -0
- package/src/charting/datamodel/scorecard/ReadabilityCategory.js +37 -0
- package/src/charting/datamodel/scorecard/ScorecardCategory.js +46 -0
- package/src/charting/datamodel/scorecard/StandardsCategory.js +29 -0
- package/src/charting/datamodel/scorecard/StructureCategory.js +32 -0
- package/src/charting/datamodel/scorecard/index.js +8 -0
- package/src/charting/diagrams/DataModelDiagram.js +4 -11
- package/src/charting/diagrams/DataModelRelationshipStrategy.js +7 -1
- package/src/charting/diagrams/NodeGeometry.js +6 -2
- package/src/charting/toolbar/ToolbarSelect.js +6 -0
- package/src/datamodel.js +402 -4
- package/src/index.js +2666 -2666
- package/src/workspace/parts/DataModelPart.js +197 -6
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { ScorecardCategory } from './ScorecardCategory.js';
|
|
2
|
+
import { CompletenessCategory } from './CompletenessCategory.js';
|
|
3
|
+
import { StructureCategory } from './StructureCategory.js';
|
|
4
|
+
import { StandardsCategory } from './StandardsCategory.js';
|
|
5
|
+
import { DefinitionsCategory } from './DefinitionsCategory.js';
|
|
6
|
+
import { ConsistencyCategory } from './ConsistencyCategory.js';
|
|
7
|
+
import { ReadabilityCategory } from './ReadabilityCategory.js';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* The ten categories in the Scorecard's order: those the model measures, and
|
|
11
|
+
* those a reviewer scores (Correctness, Scheme, Abstraction, Data).
|
|
12
|
+
*/
|
|
13
|
+
const CATEGORIES = Object.freeze([
|
|
14
|
+
new ScorecardCategory('correctness', 'Correctness', 'Does the model capture the requirements?'),
|
|
15
|
+
new CompletenessCategory(),
|
|
16
|
+
new ScorecardCategory('scheme', 'Scheme', 'Is the scheme (relational, dimensional...) the right one for what it is for?'),
|
|
17
|
+
new StructureCategory(),
|
|
18
|
+
new ScorecardCategory('abstraction', 'Abstraction', 'Is it as general as it should be, and no more?'),
|
|
19
|
+
new StandardsCategory(),
|
|
20
|
+
new ReadabilityCategory(),
|
|
21
|
+
new DefinitionsCategory(),
|
|
22
|
+
new ConsistencyCategory(),
|
|
23
|
+
new ScorecardCategory('data', 'Data', 'Does the data in the database match what the model says of it?'),
|
|
24
|
+
]);
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* ModelScorecard — a data model scored as DMBOK2 has a model reviewed, on the
|
|
28
|
+
* Data Model Scorecard's ten categories (Steve Hoberman, 2015). Six the model
|
|
29
|
+
* measures (ScorecardCategory subclasses); four a reviewer scores; a reviewer's
|
|
30
|
+
* score, kept in the model (`model.scorecard.reviews[key]: { score, note }`),
|
|
31
|
+
* overrides a measured one too. The total is the weighted average of the
|
|
32
|
+
* categories with a score, out of 100. Weights are the model's own
|
|
33
|
+
* (`model.scorecard.weights`); by default equal, 10 each: the book gives its
|
|
34
|
+
* own weighting, and an organisation sets the one it reviews by.
|
|
35
|
+
*/
|
|
36
|
+
export class ModelScorecard {
|
|
37
|
+
static CATEGORIES = CATEGORIES;
|
|
38
|
+
static DEFAULT_WEIGHT = 10;
|
|
39
|
+
|
|
40
|
+
/** @param {DataModel} model */
|
|
41
|
+
constructor(model) {
|
|
42
|
+
this.model = model;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** What the model keeps of the scorecard: `{ weights, reviews }`. */
|
|
46
|
+
get kept() {
|
|
47
|
+
return this.model.scorecard ?? {};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** A category's weight: the model's, else the default. */
|
|
51
|
+
weight(key) {
|
|
52
|
+
const w = Number(this.kept.weights?.[key]);
|
|
53
|
+
return Number.isFinite(w) && w >= 0 ? w : ModelScorecard.DEFAULT_WEIGHT;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Each category: `{ category, weight, measured: {score, notes}, review: {score, note}|null,
|
|
58
|
+
* score (the review's, else the measured, or null), source: 'reviewed'|'measured'|null }`.
|
|
59
|
+
*/
|
|
60
|
+
results() {
|
|
61
|
+
const findings = this.model.checker().run();
|
|
62
|
+
return CATEGORIES.map(category => {
|
|
63
|
+
const measured = category.measure(this.model, findings);
|
|
64
|
+
const kept = this.kept.reviews?.[category.key];
|
|
65
|
+
const review = Number.isFinite(Number(kept?.score)) && kept?.score !== '' && kept?.score != null ? { score: Number(kept.score), note: kept.note ?? '' } : null;
|
|
66
|
+
const score = review ? review.score : measured.score;
|
|
67
|
+
return { category, weight: this.weight(category.key), measured, review, score, source: review ? 'reviewed' : (measured.score == null ? null : 'measured') };
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** The weighted average of the categories with a score (null: none has), and how much of the weight that is. */
|
|
72
|
+
total(results = this.results()) {
|
|
73
|
+
const scored = results.filter(r => r.score != null && r.weight > 0);
|
|
74
|
+
const weight = scored.reduce((s, r) => s + r.weight, 0);
|
|
75
|
+
const all = results.reduce((s, r) => s + r.weight, 0);
|
|
76
|
+
return {
|
|
77
|
+
score: weight ? Math.round(scored.reduce((s, r) => s + r.score * r.weight, 0) / weight) : null,
|
|
78
|
+
coverage: all ? Math.round((100 * weight) / all) : 0,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Its dialog's fields: a heading per category (its question as the hint),
|
|
84
|
+
* what the model measured (shown), the reviewer's score and note, its weight.
|
|
85
|
+
* The score and weight are text, not number, fields: a number field reads
|
|
86
|
+
* empty as 0, and an empty score means "not scored", not nought.
|
|
87
|
+
*/
|
|
88
|
+
fields(results = this.results()) {
|
|
89
|
+
return results.flatMap(({ category, measured, review, weight }) => [
|
|
90
|
+
{ type: 'heading', label: category.label },
|
|
91
|
+
{
|
|
92
|
+
key: `${category.key}.measured`, label: 'Measured', type: 'text', readOnly: true,
|
|
93
|
+
value: measured.score == null ? '—' : `${measured.score}`, hint: [category.question, ...measured.notes].join(' · '),
|
|
94
|
+
},
|
|
95
|
+
{ key: `${category.key}.score`, label: 'Reviewer\'s score', type: 'text', value: review?.score ?? '', placeholder: category.measured ? 'the measured one' : '0–100' },
|
|
96
|
+
{ key: `${category.key}.note`, label: 'Note', type: 'text', value: review?.note ?? '' },
|
|
97
|
+
{ key: `${category.key}.weight`, label: 'Weight', type: 'text', value: weight },
|
|
98
|
+
]);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** What the model keeps from those fields: weights other than the default, and the reviews given. */
|
|
102
|
+
static specFrom(values) {
|
|
103
|
+
const weights = {}, reviews = {};
|
|
104
|
+
for (const { key } of CATEGORIES) {
|
|
105
|
+
const weight = values[`${key}.weight`];
|
|
106
|
+
if (String(weight ?? '').trim() !== '' && Number(weight) !== ModelScorecard.DEFAULT_WEIGHT && Number.isFinite(Number(weight))) weights[key] = Number(weight);
|
|
107
|
+
const score = values[`${key}.score`];
|
|
108
|
+
const note = String(values[`${key}.note`] ?? '').trim();
|
|
109
|
+
if (String(score ?? '').trim() !== '' && Number.isFinite(Number(score))) reviews[key] = { score: Math.max(0, Math.min(100, Number(score))), ...(note && { note }) };
|
|
110
|
+
}
|
|
111
|
+
return { ...(Object.keys(weights).length && { weights }), ...(Object.keys(reviews).length && { reviews }) };
|
|
112
|
+
}
|
|
113
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { MeasuredCategory, percent } from './ScorecardCategory.js';
|
|
2
|
+
import { DataModelDiagram } from '../../diagrams/DataModelDiagram.js';
|
|
3
|
+
import { DataModelNotation } from '../../../datamodel.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Readability: can it be read as drawn? Measured as the average of the share
|
|
7
|
+
* of placed boxes, at each level, clear of every other box, and the share of
|
|
8
|
+
* associations with a name (a generalization needs none).
|
|
9
|
+
*/
|
|
10
|
+
export class ReadabilityCategory extends MeasuredCategory {
|
|
11
|
+
constructor() {
|
|
12
|
+
super('readability', 'Readability', 'Can it be read as drawn: boxes clear of each other, relationships named?');
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
measure(model) {
|
|
16
|
+
let placed = 0, clear = 0, associations = 0, named = 0;
|
|
17
|
+
for (const level of model.constructor.LEVELS) {
|
|
18
|
+
const notation = DataModelNotation.notation('crowsfoot');
|
|
19
|
+
const boxes = model.level(level).entities
|
|
20
|
+
.map(e => DataModelDiagram.boxOf(DataModelDiagram.buildNode({ ...e, [level]: { attributes: e.attributes ?? [] } }, { level, notation })))
|
|
21
|
+
.filter(Boolean);
|
|
22
|
+
placed += boxes.length;
|
|
23
|
+
clear += boxes.filter((a, i) => !boxes.some((b, j) => j !== i && a.x0 < b.x1 && b.x0 < a.x1 && a.y0 < b.y1 && b.y0 < a.y1)).length;
|
|
24
|
+
for (const r of model.level(level).relationships) {
|
|
25
|
+
if (!DataModelNotation.relationshipKind(r).hasCardinality) continue;
|
|
26
|
+
associations++;
|
|
27
|
+
if (String(r.name ?? '').trim()) named++;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
const shares = [['boxes clear of the others', clear, placed], ['relationships named', named, associations]].filter(([, , whole]) => whole);
|
|
31
|
+
if (!shares.length) return { score: null, notes: ['nothing drawn yet'] };
|
|
32
|
+
return {
|
|
33
|
+
score: Math.round(shares.reduce((sum, [, part, whole]) => sum + percent(part, whole), 0) / shares.length),
|
|
34
|
+
notes: shares.map(([what, part, whole]) => `${part} of ${whole} ${what}`),
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/** A ratio as a whole score out of 100 (none: null). */
|
|
2
|
+
export const percent = (part, whole) => (whole ? Math.round((100 * part) / whole) : null);
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* ScorecardCategory — one of the Data Model Scorecard's ten categories (Steve
|
|
6
|
+
* Hoberman, *Data Model Scorecard*, 2015): its `key`, `label` and the question
|
|
7
|
+
* it asks. This base is one a reviewer scores (`measure` says nothing); a
|
|
8
|
+
* subclass measures it from the model, a score out of 100 and notes on why,
|
|
9
|
+
* or null when the model cannot say. A reviewer's score overrides either.
|
|
10
|
+
*/
|
|
11
|
+
export class ScorecardCategory {
|
|
12
|
+
/**
|
|
13
|
+
* @param {string} key - 'correctness'
|
|
14
|
+
* @param {string} label - 'Correctness'
|
|
15
|
+
* @param {string} question - What it asks of the model
|
|
16
|
+
*/
|
|
17
|
+
constructor(key, label, question) {
|
|
18
|
+
Object.assign(this, { key, label, question });
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Whether the model says how it scores (else a reviewer must). */
|
|
22
|
+
get measured() {
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Its score from the model: `{ score: 0..100 | null, notes: string[] }`.
|
|
28
|
+
* @param {DataModel} model
|
|
29
|
+
* @param {Array<Object>} findings - The model check's findings (DataModelChecker#run)
|
|
30
|
+
*/
|
|
31
|
+
measure(model, findings) {
|
|
32
|
+
return { score: null, notes: ['for a reviewer to score'] };
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** A category measured from the model: a subclass says how. */
|
|
37
|
+
export class MeasuredCategory extends ScorecardCategory {
|
|
38
|
+
get measured() {
|
|
39
|
+
return true;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** @abstract */
|
|
43
|
+
measure(model, findings) {
|
|
44
|
+
throw new Error(`${this.constructor.name} must implement measure()`);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { MeasuredCategory, percent } from './ScorecardCategory.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Standards: are names as the model's naming standard says? Measured as the
|
|
5
|
+
* share of entity and attribute names that keep it; without a standard the
|
|
6
|
+
* model cannot say (a reviewer may).
|
|
7
|
+
*/
|
|
8
|
+
export class StandardsCategory extends MeasuredCategory {
|
|
9
|
+
constructor() {
|
|
10
|
+
super('standards', 'Standards', 'Are names written as the naming standard says?');
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
measure(model) {
|
|
14
|
+
const standard = model.namingStandard;
|
|
15
|
+
if (!standard) return { score: null, notes: ['no naming standard: set one to measure this'] };
|
|
16
|
+
let names = 0, kept = 0;
|
|
17
|
+
for (const level of model.constructor.LEVELS) {
|
|
18
|
+
for (const e of model.level(level).entities) {
|
|
19
|
+
names++;
|
|
20
|
+
if (!standard.check(level, 'entities', e.name).length) kept++;
|
|
21
|
+
for (const a of e.attributes ?? []) {
|
|
22
|
+
names++;
|
|
23
|
+
if (!standard.check(level, 'attributes', a.name).length) kept++;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return names ? { score: percent(kept, names), notes: [`${kept} of ${names} names keep the naming standard`] } : { score: null, notes: ['no names'] };
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { MeasuredCategory, percent } from './ScorecardCategory.js';
|
|
2
|
+
|
|
3
|
+
/** The model check's findings that are about structure. */
|
|
4
|
+
const STRUCTURAL = new Set([
|
|
5
|
+
'no-primary-key', 'reference-missing', 'foreign-key-without-relationship', 'foreign-key-unrelated',
|
|
6
|
+
'many-to-many', 'repeating-group', 'generalization-cycle',
|
|
7
|
+
]);
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Structure: is the model sound? Measured as the share of logical entities and
|
|
11
|
+
* tables the model check finds nothing structural on (keys, foreign keys,
|
|
12
|
+
* many-to-many, repeating groups, generalizations going round).
|
|
13
|
+
*/
|
|
14
|
+
export class StructureCategory extends MeasuredCategory {
|
|
15
|
+
static STRUCTURAL = STRUCTURAL;
|
|
16
|
+
|
|
17
|
+
constructor() {
|
|
18
|
+
super('structure', 'Structure', 'Is it sound: keys, foreign keys, no repeating groups or unresolved many-to-many?');
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
measure(model, findings) {
|
|
22
|
+
const entities = ['logical', 'physical'].flatMap(level => model.level(level).entities.map(e => `${level}:${e.id}`));
|
|
23
|
+
if (!entities.length) return { score: null, notes: ['no logical entities or tables'] };
|
|
24
|
+
const structural = findings.filter(f => STRUCTURAL.has(f.code));
|
|
25
|
+
const touched = new Set(structural.flatMap(f => (f.ids ?? []).map(id => `${f.level}:${id}`)).filter(k => entities.includes(k)));
|
|
26
|
+
const counts = structural.reduce((m, f) => ((m[f.code] = (m[f.code] ?? 0) + 1), m), {});
|
|
27
|
+
return {
|
|
28
|
+
score: percent(entities.length - touched.size, entities.length),
|
|
29
|
+
notes: [`${entities.length - touched.size} of ${entities.length} logical entities and tables sound`, ...Object.entries(counts).map(([code, n]) => `${n} ${code}`)],
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export { ScorecardCategory, MeasuredCategory } from './ScorecardCategory.js';
|
|
2
|
+
export { CompletenessCategory } from './CompletenessCategory.js';
|
|
3
|
+
export { StructureCategory } from './StructureCategory.js';
|
|
4
|
+
export { StandardsCategory } from './StandardsCategory.js';
|
|
5
|
+
export { DefinitionsCategory } from './DefinitionsCategory.js';
|
|
6
|
+
export { ConsistencyCategory } from './ConsistencyCategory.js';
|
|
7
|
+
export { ReadabilityCategory } from './ReadabilityCategory.js';
|
|
8
|
+
export { ModelScorecard } from './ModelScorecard.js';
|
|
@@ -94,8 +94,8 @@ export class DataModelDiagram extends Diagram {
|
|
|
94
94
|
* note to what it is about, or (the `links` option) a mapping between levels.
|
|
95
95
|
* @param {{id?: string, source: string, target: string, label?: string}} link
|
|
96
96
|
*/
|
|
97
|
-
static buildLink({ id, source, target, label }) {
|
|
98
|
-
return { ...(id && { id }), source, target, ...(label && { label }), lineStyle: 'dotted', arrowhead: 'none', arrowheadStart: 'none' };
|
|
97
|
+
static buildLink({ id, source, target, label, color }) {
|
|
98
|
+
return { ...(id && { id }), source, target, ...(label && { label }), ...(color && { color }), lineStyle: 'dotted', arrowhead: 'none', arrowheadStart: 'none' };
|
|
99
99
|
}
|
|
100
100
|
|
|
101
101
|
static typeGroup(type) {
|
|
@@ -146,15 +146,8 @@ export class DataModelDiagram extends Diagram {
|
|
|
146
146
|
};
|
|
147
147
|
}
|
|
148
148
|
|
|
149
|
+
/** A relationship's edge in a notation, as its kind draws it (DataModelNotation.relationshipKind). */
|
|
149
150
|
static buildEdge(relationship, notation) {
|
|
150
|
-
return
|
|
151
|
-
...relationship,
|
|
152
|
-
label: relationship.name,
|
|
153
|
-
lineStyle: relationship.identifying === false ? 'dashed' : 'solid',
|
|
154
|
-
arrowheadStart: notation.cardinalityArrowhead(relationship.sourceCardinality) || 'none',
|
|
155
|
-
arrowhead: notation.cardinalityArrowhead(relationship.targetCardinality) || 'none',
|
|
156
|
-
sourceLabel: notation.cardinalityLabel(relationship.sourceCardinality),
|
|
157
|
-
targetLabel: notation.cardinalityLabel(relationship.targetCardinality),
|
|
158
|
-
};
|
|
151
|
+
return DataModelNotation.relationshipKind(relationship).edge(relationship, notation);
|
|
159
152
|
}
|
|
160
153
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { DataModelNotation } from '../../datamodel.js';
|
|
1
2
|
/**
|
|
2
3
|
* Strategy interface for turning data-model relationships into diagram nodes
|
|
3
4
|
* and edges. Crow's Foot and UML keep relationships as direct edges; Chen
|
|
@@ -68,12 +69,17 @@ export class RelationshipAsNodeStrategy extends DataModelRelationshipStrategy {
|
|
|
68
69
|
return best; // none clear: the least covered
|
|
69
70
|
}
|
|
70
71
|
|
|
71
|
-
apply(relationships, { notation, entity = () => undefined, occupied = [] }) {
|
|
72
|
+
apply(relationships, { notation, buildEdge, entity = () => undefined, occupied = [] }) {
|
|
72
73
|
const nodes = [];
|
|
73
74
|
const edges = [];
|
|
74
75
|
const taken = [...occupied];
|
|
75
76
|
const { width, height } = RelationshipAsNodeStrategy.DIAMOND;
|
|
76
77
|
relationships.forEach((relationship, i) => {
|
|
78
|
+
// A kind drawn as a line in this notation (a generalization) is one.
|
|
79
|
+
if (!DataModelNotation.relationshipKind(relationship).drawnAsNode(notation)) {
|
|
80
|
+
edges.push(buildEdge(relationship, notation));
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
77
83
|
const relId = relationship.id || `${relationship.source}__${relationship.target}__rel${i}`;
|
|
78
84
|
const lineStyle = relationship.identifying === false ? 'dashed' : 'solid';
|
|
79
85
|
const [from, to] = [entity(relationship.source), entity(relationship.target)];
|
|
@@ -55,7 +55,11 @@ export class NodeGeometry {
|
|
|
55
55
|
return posEntry.y - size.h / 2 + headerH + idx * rowH + rowH / 2;
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
-
/**
|
|
58
|
+
/**
|
|
59
|
+
* Draws a table-card node's header + one row per column into wrapper `g`;
|
|
60
|
+
* the node's own `color` tints the card's body (a lens, a marked control
|
|
61
|
+
* record), the header staying as it is so its name reads.
|
|
62
|
+
*/
|
|
59
63
|
static buildTableNodeContent(renderer, g, node) {
|
|
60
64
|
const { headerH, rowH } = NodeGeometry.TABLE_NODE;
|
|
61
65
|
const { w, h } = NodeGeometry.tableNodeSize(node);
|
|
@@ -65,7 +69,7 @@ export class NodeGeometry {
|
|
|
65
69
|
const borderWidth = node.selected ? 2 : 1;
|
|
66
70
|
|
|
67
71
|
const TYPE_COLORS = { NUMBER: '#74b9ff', TEXT: '#a29bfe', TIMESTAMP: '#55efc4', BOOLEAN: '#ffeaa7', JSON: '#fd79a8', UNKNOWN: '#dfe6e9' };
|
|
68
|
-
g.appendChild(renderer.rect(x0, y0, w, h, '#ffffff', { color: borderColor, width: borderWidth }));
|
|
72
|
+
g.appendChild(renderer.rect(x0, y0, w, h, node.color || '#ffffff', { color: borderColor, width: borderWidth }));
|
|
69
73
|
g.appendChild(renderer.rect(x0, y0, w, headerH, '#1976d2', null));
|
|
70
74
|
g.appendChild(renderer.text(LabelHelper.resolveNodeLabel(node), x0 + 8, y0 + headerH / 2, {
|
|
71
75
|
fill: '#fff', textAnchor: 'start', verticalAlign: 'middle', fontSize: '11px', fontWeight: 'bold', 'pointer-events': 'none',
|
|
@@ -35,4 +35,10 @@ export class ToolbarSelect extends ToolbarItem {
|
|
|
35
35
|
],
|
|
36
36
|
};
|
|
37
37
|
}
|
|
38
|
+
|
|
39
|
+
/** Show the value as it is now (it may have been chosen other than through this select). */
|
|
40
|
+
refresh(node) {
|
|
41
|
+
const select = [...(node?.children ?? [])].find(c => c.tagName === 'SELECT');
|
|
42
|
+
if (select) select.value = this.value();
|
|
43
|
+
}
|
|
38
44
|
}
|