@datagrok/eda 1.4.9 → 1.4.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/CHANGELOG.md +15 -0
- package/README.md +2 -0
- package/css/pareto.css +5 -0
- package/dist/package.js +1 -1
- package/dist/package.js.map +1 -1
- package/icons/pareto-front-viewer.svg +15 -0
- package/package.json +2 -2
- package/src/package-api.ts +14 -0
- package/src/package.g.ts +16 -0
- package/src/package.ts +221 -244
- package/src/pareto-optimization/defs.ts +45 -0
- package/src/pareto-optimization/pareto-computations.ts +65 -0
- package/src/pareto-optimization/pareto-front-viewer.ts +490 -0
- package/src/pareto-optimization/pareto-optimizer.ts +272 -0
- package/src/pareto-optimization/utils.ts +39 -0
- package/test-console-output-1.log +49 -49
- package/test-record-1.mp4 +0 -0
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
// Pareto front constants & definitions
|
|
2
|
+
|
|
3
|
+
export enum OPT_TYPE {
|
|
4
|
+
MIN = 'minimize',
|
|
5
|
+
MAX = 'maximize',
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
export enum LABEL {
|
|
9
|
+
OPTIMAL = 'optimal',
|
|
10
|
+
NON_OPT = 'non-optimal',
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export type NumericFeature = {
|
|
14
|
+
toOptimize: boolean,
|
|
15
|
+
optType: OPT_TYPE,
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export type NumericArray = Float32Array | Float64Array | Int32Array | Uint32Array;
|
|
19
|
+
|
|
20
|
+
export const DIFFERENCE = 2;
|
|
21
|
+
export enum RATIO {
|
|
22
|
+
FORM = 0.15,
|
|
23
|
+
VIEWER = 0.5,
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export enum COL_NAME {
|
|
27
|
+
OPT = 'Pareto optimality',
|
|
28
|
+
SIZE = 'Pareto size',
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export const PC_MAX_COLS = 10;
|
|
32
|
+
export enum SIZE {
|
|
33
|
+
OPTIMAL = 8,
|
|
34
|
+
NON_OPT = 4,
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export const AXIS_NAMES = ['xColumnName', 'yColumnName'];
|
|
38
|
+
export const AXIS_NAMES_3D = ['xColumnName', 'yColumnName', 'zColumnName'];
|
|
39
|
+
export type ColorOpt = Record<string, string | undefined>;
|
|
40
|
+
|
|
41
|
+
export const SCATTER_ROW_LIM = 5000;
|
|
42
|
+
export const SCATTER3D_ROW_LIM = 1000;
|
|
43
|
+
|
|
44
|
+
export const AUTO_AXES_SELECTION = true;
|
|
45
|
+
export const AUTO_LABELS_SELECTION = true;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
// Pareto front computations
|
|
2
|
+
|
|
3
|
+
import {NumericArray, OPT_TYPE} from './defs';
|
|
4
|
+
|
|
5
|
+
export function getParetoMask(rawData: NumericArray[], sense: OPT_TYPE[], nPoints: number,
|
|
6
|
+
nullIndices?: Set<number>): boolean[] {
|
|
7
|
+
if (nPoints === 0)
|
|
8
|
+
return [];
|
|
9
|
+
|
|
10
|
+
const nDims = rawData.length;
|
|
11
|
+
if (sense.length !== nDims)
|
|
12
|
+
throw new Error('Sense array length must match number of dimensions');
|
|
13
|
+
|
|
14
|
+
const pointIndices = new Uint32Array(nPoints);
|
|
15
|
+
for (let i = 0; i < nPoints; i++)
|
|
16
|
+
pointIndices[i] = i;
|
|
17
|
+
|
|
18
|
+
pointIndices.sort((i1: number, i2: number) => {
|
|
19
|
+
return sense[0] === OPT_TYPE.MIN ? rawData[0][i1] - rawData[0][i2] : rawData[0][i2] - rawData[0][i1];
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
const mask: boolean[] = Array(nPoints).fill(true);
|
|
23
|
+
const paretoFrontIndices: number[] = [];
|
|
24
|
+
|
|
25
|
+
// Set missing values to non-optimal
|
|
26
|
+
nullIndices?.forEach((idx) => mask[idx] = false);
|
|
27
|
+
|
|
28
|
+
for (const index of pointIndices) {
|
|
29
|
+
if (!mask[index])
|
|
30
|
+
continue;
|
|
31
|
+
|
|
32
|
+
let dominated = false;
|
|
33
|
+
|
|
34
|
+
for (const frontPointIndex of paretoFrontIndices) {
|
|
35
|
+
let dominates = true;
|
|
36
|
+
let strictlyBetter = false;
|
|
37
|
+
|
|
38
|
+
for (let d = 0; d < nDims; d++) {
|
|
39
|
+
const a = rawData[d][frontPointIndex];
|
|
40
|
+
const b = rawData[d][index];
|
|
41
|
+
const s = sense[d];
|
|
42
|
+
|
|
43
|
+
if (s === OPT_TYPE.MIN) {
|
|
44
|
+
if (a > b) dominates = false;
|
|
45
|
+
if (a < b) strictlyBetter = true;
|
|
46
|
+
} else {
|
|
47
|
+
if (a < b) dominates = false;
|
|
48
|
+
if (a > b) strictlyBetter = true;
|
|
49
|
+
}
|
|
50
|
+
} // for d
|
|
51
|
+
|
|
52
|
+
if (dominates && strictlyBetter) {
|
|
53
|
+
dominated = true;
|
|
54
|
+
break;
|
|
55
|
+
}
|
|
56
|
+
} // for frontPointIndex
|
|
57
|
+
|
|
58
|
+
if (dominated)
|
|
59
|
+
mask[index] = false;
|
|
60
|
+
else
|
|
61
|
+
paretoFrontIndices.push(index);
|
|
62
|
+
} // for index
|
|
63
|
+
|
|
64
|
+
return mask;
|
|
65
|
+
} // getParetoMask
|
|
@@ -0,0 +1,490 @@
|
|
|
1
|
+
import * as grok from 'datagrok-api/grok';
|
|
2
|
+
import * as ui from 'datagrok-api/ui';
|
|
3
|
+
import * as DG from 'datagrok-api/dg';
|
|
4
|
+
import {AUTO_LABELS_SELECTION, AUTO_AXES_SELECTION, SCATTER_ROW_LIM, SIZE, COL_NAME, OPT_TYPE,
|
|
5
|
+
NumericArray, LABEL, DIFFERENCE} from './defs';
|
|
6
|
+
import {getParetoMask} from './pareto-computations';
|
|
7
|
+
import {getMissingValsIndices} from '../missing-values-imputation/knn-imputer';
|
|
8
|
+
|
|
9
|
+
export class ParetoFrontViewer extends DG.JsViewer {
|
|
10
|
+
private title: string;
|
|
11
|
+
private showTitle: boolean;
|
|
12
|
+
private description: string;
|
|
13
|
+
private descriptionPosition: string;
|
|
14
|
+
private descriptionVisibilityMode: string;
|
|
15
|
+
private minimizeColumnNames: string[];
|
|
16
|
+
private maximizeColumnNames: string[];
|
|
17
|
+
private labelColumnsColumnNames: string[];
|
|
18
|
+
private autoLabelColNames: string[] = [];
|
|
19
|
+
private autoLabelsSelection: boolean = true;
|
|
20
|
+
private displayLabels: string;
|
|
21
|
+
private toChangeAutoLabelsSelection: boolean = true;
|
|
22
|
+
private xAxisColumnName: string;
|
|
23
|
+
private yAxisColumnName: string;
|
|
24
|
+
private autoAxesSelection: boolean;
|
|
25
|
+
private toChangeAutoAxesSelection: boolean = true;
|
|
26
|
+
private legendVisibility: string;
|
|
27
|
+
private legendPosition: string;
|
|
28
|
+
private toChangeScatterMarkerSize = false;
|
|
29
|
+
private colorColumnName: string | null = null;
|
|
30
|
+
|
|
31
|
+
private scatter: DG.ScatterPlotViewer | null = null;
|
|
32
|
+
|
|
33
|
+
private numCols: DG.Column[] = [];
|
|
34
|
+
private numColNames: string[] = [];
|
|
35
|
+
private numColsCount: number = 0;
|
|
36
|
+
private rowCount: number = 0;
|
|
37
|
+
private isApplicable: boolean = false;
|
|
38
|
+
private errMsg: string = '';
|
|
39
|
+
private resultColName: string = '';
|
|
40
|
+
private sizeColName: string = '';
|
|
41
|
+
private optimizedColNames: string[] = [];
|
|
42
|
+
private hasCommonMinMaxNames = false;
|
|
43
|
+
|
|
44
|
+
private errDiv: HTMLDivElement | null = null;
|
|
45
|
+
|
|
46
|
+
private missingValsIndices: Map<string, number[]> = new Map<string, number[]>();
|
|
47
|
+
|
|
48
|
+
private toChangeScatterOptions = true;
|
|
49
|
+
|
|
50
|
+
get type(): string {
|
|
51
|
+
return 'ParetoFrontViewer';
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
constructor() {
|
|
55
|
+
super();
|
|
56
|
+
|
|
57
|
+
this.title = this.string('title', 'Pareto front');
|
|
58
|
+
this.showTitle = this.bool('showTitle', false, {category: 'Description'});
|
|
59
|
+
|
|
60
|
+
this.description = this.string('description');
|
|
61
|
+
|
|
62
|
+
this.descriptionPosition = this.string('descriptionPosition', 'Top', {choices: ['Left', 'Right', 'Top', 'Bottom']});
|
|
63
|
+
|
|
64
|
+
this.descriptionVisibilityMode = this.string('descriptionVisibilityMode', 'Auto', {
|
|
65
|
+
choices: ['Auto', 'Always', 'Never']},
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
this.minimizeColumnNames = this.addProperty('minimizeColumnNames', DG.TYPE.COLUMN_LIST, null, {
|
|
69
|
+
columnTypeFilter: DG.TYPE.NUMERICAL,
|
|
70
|
+
category: 'Objectives',
|
|
71
|
+
description: 'Columns with features to be minimized during Pareto optimization.',
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
this.maximizeColumnNames = this.addProperty('maximizeColumnNames', DG.TYPE.COLUMN_LIST, null, {
|
|
75
|
+
columnTypeFilter: DG.TYPE.NUMERICAL,
|
|
76
|
+
category: 'Objectives',
|
|
77
|
+
description: 'Columns with features to be maximized during Pareto optimization.',
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
this.xAxisColumnName = this.string('xAxisColumnName', null, {
|
|
81
|
+
category: 'Axes',
|
|
82
|
+
description: 'A column to be used on the X axis of the scatter plot.',
|
|
83
|
+
nullable: false,
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
this.yAxisColumnName = this.string('yAxisColumnName', null, {
|
|
87
|
+
category: 'Axes',
|
|
88
|
+
description: 'A column to be used on the Y axis of the scatter plot.',
|
|
89
|
+
nullable: false,
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
this.autoAxesSelection = this.bool('autoAxesSelection', AUTO_AXES_SELECTION, {
|
|
93
|
+
defaultValue: AUTO_AXES_SELECTION,
|
|
94
|
+
category: 'Axes',
|
|
95
|
+
// eslint-disable-next-line max-len
|
|
96
|
+
description: 'If checked, axes are selected automatically based on the optimized features. If unchecked, custom coordinate axes are used.',
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
this.labelColumnsColumnNames = this.addProperty('labelColumnsColumnNames', DG.TYPE.COLUMN_LIST, null, {
|
|
100
|
+
category: 'Labels',
|
|
101
|
+
description: 'Label columns to show next to the markers.',
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
this.autoLabelsSelection = this.bool('autoLabelsSelection', AUTO_LABELS_SELECTION, {
|
|
105
|
+
category: 'Labels',
|
|
106
|
+
description: 'Select legend columns automatically; labels show unique categories.',
|
|
107
|
+
defaultValue: AUTO_LABELS_SELECTION,
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
this.displayLabels = this.string('displayLabels', 'Auto', {
|
|
111
|
+
choices: ['Auto', 'Always', 'Never'],
|
|
112
|
+
category: 'Labels',
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
this.legendVisibility = this.string('legendVisibility', 'Auto', {choices: ['Auto', 'Always', 'Never']});
|
|
116
|
+
this.legendPosition = this.string('legendPosition', 'Top', {
|
|
117
|
+
choices: ['Auto', 'Left', 'Right', 'Top', 'Bottom', 'RightTop', 'RightBottom', 'LeftTop', 'LeftBottom'],
|
|
118
|
+
});
|
|
119
|
+
} // constructor
|
|
120
|
+
|
|
121
|
+
private initializeData() {
|
|
122
|
+
this.rowCount = this.dataFrame.rowCount;
|
|
123
|
+
const cols = this.dataFrame.columns;
|
|
124
|
+
const colList = cols.toList();
|
|
125
|
+
|
|
126
|
+
// Extract numerical columns: empty columns are skipped
|
|
127
|
+
this.numCols = colList.filter((col) => col.isNumerical && (col.stats.missingValueCount < this.rowCount));
|
|
128
|
+
this.numColNames = this.numCols.map((col) => col.name);
|
|
129
|
+
this.numColsCount = this.numCols.length;
|
|
130
|
+
|
|
131
|
+
// Check applicability
|
|
132
|
+
this.isApplicable = this._testColumns();
|
|
133
|
+
if (!this.isApplicable) {
|
|
134
|
+
this._showErrorMessage(this.errMsg);
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
this.toChangeScatterMarkerSize = this.rowCount > SCATTER_ROW_LIM;
|
|
139
|
+
this.resultColName = cols.getUnusedName(COL_NAME.OPT);
|
|
140
|
+
this.sizeColName = cols.getUnusedName(COL_NAME.SIZE);
|
|
141
|
+
this.missingValsIndices = getMissingValsIndices(this.numCols);
|
|
142
|
+
} // initializeData
|
|
143
|
+
|
|
144
|
+
private computeParetoFront(): void {
|
|
145
|
+
if (!this.isApplicable)
|
|
146
|
+
return;
|
|
147
|
+
|
|
148
|
+
const data: NumericArray[] = [];
|
|
149
|
+
const sense: OPT_TYPE[] = [];
|
|
150
|
+
const nullValsIndices = new Set<number>();
|
|
151
|
+
|
|
152
|
+
const pushCols = (names: string[] | null, optType: OPT_TYPE) => {
|
|
153
|
+
if (names == null)
|
|
154
|
+
return;
|
|
155
|
+
|
|
156
|
+
names.forEach((name) => {
|
|
157
|
+
data.push(this.dataFrame.col(name)!.getRawData());
|
|
158
|
+
sense.push(optType);
|
|
159
|
+
|
|
160
|
+
const curMisValsInds = this.missingValsIndices.get(name);
|
|
161
|
+
if (curMisValsInds != null)
|
|
162
|
+
curMisValsInds.forEach((val) => nullValsIndices.add(val));
|
|
163
|
+
});
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
pushCols(this.minimizeColumnNames, OPT_TYPE.MIN);
|
|
167
|
+
pushCols(this.maximizeColumnNames, OPT_TYPE.MAX);
|
|
168
|
+
|
|
169
|
+
const sizeCol = this.dataFrame.col(this.sizeColName);
|
|
170
|
+
let resCol = this.dataFrame.col(this.resultColName);
|
|
171
|
+
|
|
172
|
+
if (data.length > 0) {
|
|
173
|
+
const mask = getParetoMask(data, sense, this.rowCount, nullValsIndices);
|
|
174
|
+
const colOpt = DG.Column.fromStrings(this.resultColName, mask.map((res) => res ? LABEL.OPTIMAL : LABEL.NON_OPT));
|
|
175
|
+
|
|
176
|
+
if (resCol == null) {
|
|
177
|
+
this.dataFrame.columns.add(colOpt);
|
|
178
|
+
resCol = colOpt;
|
|
179
|
+
this.hideCol(this.resultColName);
|
|
180
|
+
} else {
|
|
181
|
+
const newCats = colOpt.categories;
|
|
182
|
+
const newRaw = colOpt.getRawData();
|
|
183
|
+
|
|
184
|
+
const prevRaw = resCol.getRawData();
|
|
185
|
+
const prevCats = resCol.categories;
|
|
186
|
+
const indeces = newCats.map((cat) => prevCats.indexOf(cat));
|
|
187
|
+
|
|
188
|
+
for (let k = 0; k < this.rowCount; ++k)
|
|
189
|
+
prevRaw[k] = indeces[newRaw[k]];
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
this.markResColWithColor(resCol);
|
|
193
|
+
|
|
194
|
+
if (this.toChangeScatterMarkerSize) {
|
|
195
|
+
if (sizeCol == null) {
|
|
196
|
+
this.dataFrame.columns.add(DG.Column.fromInt32Array(
|
|
197
|
+
this.sizeColName,
|
|
198
|
+
new Int32Array(mask.map((res) => res ? SIZE.OPTIMAL : SIZE.NON_OPT))),
|
|
199
|
+
);
|
|
200
|
+
|
|
201
|
+
this.hideCol(this.sizeColName);
|
|
202
|
+
} else {
|
|
203
|
+
const raw = sizeCol.getRawData();
|
|
204
|
+
for (let k = 0; k < this.rowCount; ++k)
|
|
205
|
+
raw[k] = mask[k] ? SIZE.OPTIMAL : SIZE.NON_OPT;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
} else {
|
|
209
|
+
if (sizeCol != null) {
|
|
210
|
+
const raw = sizeCol.getRawData();
|
|
211
|
+
for (let k = 0; k < this.rowCount; ++k)
|
|
212
|
+
raw[k] = SIZE.NON_OPT;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
if (resCol != null) {
|
|
216
|
+
const prevRaw = resCol.getRawData();
|
|
217
|
+
const prevCats = resCol.categories;
|
|
218
|
+
const index = prevCats.indexOf(LABEL.NON_OPT as string);
|
|
219
|
+
|
|
220
|
+
for (let k = 0; k < this.rowCount; ++k)
|
|
221
|
+
prevRaw[k] = index;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
} // computeParetoFront
|
|
225
|
+
|
|
226
|
+
private markResColWithColor(col: DG.Column): void {
|
|
227
|
+
col.colors.setCategorical({
|
|
228
|
+
'optimal': '#2ca02c',
|
|
229
|
+
'non-optimal': '#e3e3e3',
|
|
230
|
+
});
|
|
231
|
+
} // markResColWithColor
|
|
232
|
+
|
|
233
|
+
private removeErrDiv(): void {
|
|
234
|
+
if (this.errDiv != null) {
|
|
235
|
+
this.root.removeChild(this.errDiv);
|
|
236
|
+
this.errDiv = null;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
_showErrorMessage(msg: string) {
|
|
241
|
+
this.removeErrDiv();
|
|
242
|
+
this.errDiv = this.root.appendChild(ui.divText(msg, 'd4-viewer-error'));
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
_testColumns(): boolean {
|
|
246
|
+
if (this.rowCount < 1) {
|
|
247
|
+
this.errMsg = 'Cannot compute Pareto front: the table is empty.';
|
|
248
|
+
return false;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
if (this.numColsCount < 2) {
|
|
252
|
+
this.errMsg = 'Cannot compute Pareto front: at least two non-empty numeric columns are required.';
|
|
253
|
+
return false;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
return true;
|
|
257
|
+
} // _testColumns
|
|
258
|
+
|
|
259
|
+
private setScatterOptions() {
|
|
260
|
+
if ((this.scatter == null) || !this.toChangeScatterOptions)
|
|
261
|
+
return;
|
|
262
|
+
|
|
263
|
+
if (this.toChangeScatterMarkerSize)
|
|
264
|
+
this.scatter.setOptions({markerMinSize: SIZE.NON_OPT, markerMaxSize: SIZE.OPTIMAL});
|
|
265
|
+
|
|
266
|
+
this.scatter.setOptions({
|
|
267
|
+
title: this.title,
|
|
268
|
+
showTitle: this.showTitle,
|
|
269
|
+
legendVisibility: this.legendVisibility,
|
|
270
|
+
legendPosition: this.legendPosition,
|
|
271
|
+
colorColumnName: this.colorColumnName,
|
|
272
|
+
displayLabels: this.displayLabels,
|
|
273
|
+
sizeColumnName: this.toChangeScatterMarkerSize ? this.sizeColName : null,
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
if (this.labelColumnsColumnNames != null)
|
|
277
|
+
this.scatter!.setOptions({labelColumnNames: this.labelColumnsColumnNames});
|
|
278
|
+
} // setScatterOptions
|
|
279
|
+
|
|
280
|
+
onTableAttached() {
|
|
281
|
+
this.initializeData();
|
|
282
|
+
if (this.isApplicable) {
|
|
283
|
+
this.scatter = DG.Viewer.scatterPlot(this.dataFrame, {
|
|
284
|
+
showColorSelector: false,
|
|
285
|
+
showSizeSelector: false,
|
|
286
|
+
autoLayout: false,
|
|
287
|
+
showLabels: 'Always',
|
|
288
|
+
markerType: DG.MARKER_TYPE.CIRCLE,
|
|
289
|
+
});
|
|
290
|
+
this.root.append(this.scatter.root);
|
|
291
|
+
this.autoLabelColNames = this.getLabelColNames();
|
|
292
|
+
|
|
293
|
+
this.subs.push(this.scatter.onDartPropertyChanged.subscribe(() => this.checkScatterAxes()));
|
|
294
|
+
|
|
295
|
+
const initColNames = this.numColNames.filter((_, idx) => this.numColsCount - idx - 1 < DIFFERENCE);
|
|
296
|
+
this.setOptions({
|
|
297
|
+
maximizeColumnNames: [],
|
|
298
|
+
minimizeColumnNames: initColNames,
|
|
299
|
+
xAxisColumnName: initColNames[0],
|
|
300
|
+
yAxisColumnName: initColNames[1],
|
|
301
|
+
autoAxesSelection: AUTO_AXES_SELECTION,
|
|
302
|
+
autoLabelsSelection: AUTO_LABELS_SELECTION,
|
|
303
|
+
});
|
|
304
|
+
} // if
|
|
305
|
+
|
|
306
|
+
this.subs.push(this.onDetached.subscribe(() => this.removeResultingCols()));
|
|
307
|
+
} // onTableAttached
|
|
308
|
+
|
|
309
|
+
private checkScatterAxes(): void {
|
|
310
|
+
if (this.scatter == null)
|
|
311
|
+
return;
|
|
312
|
+
|
|
313
|
+
let axis = this.scatter.getOptions().look['xColumnName'];
|
|
314
|
+
if (axis !== this.xAxisColumnName) {
|
|
315
|
+
this.toChangeScatterOptions = false;
|
|
316
|
+
this.setOptions({xAxisColumnName: axis});
|
|
317
|
+
this.toChangeScatterOptions = true;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
axis = this.scatter.getOptions().look['yColumnName'];
|
|
321
|
+
if (axis !== this.yAxisColumnName) {
|
|
322
|
+
this.toChangeScatterOptions = false;
|
|
323
|
+
this.setOptions({yAxisColumnName: axis});
|
|
324
|
+
this.toChangeScatterOptions = true;
|
|
325
|
+
}
|
|
326
|
+
} //checkScatterAxes
|
|
327
|
+
|
|
328
|
+
private removeResultingCols(): void {
|
|
329
|
+
this.dataFrame.columns.remove(this.resultColName);
|
|
330
|
+
this.dataFrame.columns.remove(this.sizeColName);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
private hideCol(name: string): void {
|
|
334
|
+
const tv = this.tableView;
|
|
335
|
+
|
|
336
|
+
if (tv == null)
|
|
337
|
+
return;
|
|
338
|
+
|
|
339
|
+
const gridCol = tv.grid.columns.byName(name);
|
|
340
|
+
|
|
341
|
+
if (gridCol !== null)
|
|
342
|
+
gridCol.visible = false;
|
|
343
|
+
} // hideCol
|
|
344
|
+
|
|
345
|
+
// Cancel subscriptions when the viewer is detached
|
|
346
|
+
detach() {
|
|
347
|
+
this.subs.forEach((sub) => sub.unsubscribe());
|
|
348
|
+
super.detach();
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
private updateAutoAxesSelection(): void {
|
|
352
|
+
if (this.toChangeAutoAxesSelection)
|
|
353
|
+
this.setOptions({autoAxesSelection: null});
|
|
354
|
+
this.toChangeAutoAxesSelection = true;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// Override to handle property changes
|
|
358
|
+
onPropertyChanged(property: DG.Property) {
|
|
359
|
+
if (!this.isApplicable)
|
|
360
|
+
return;
|
|
361
|
+
|
|
362
|
+
switch (property.name) {
|
|
363
|
+
case 'minimizeColumnNames':
|
|
364
|
+
case 'maximizeColumnNames':
|
|
365
|
+
this.updateCommonMinMaxFeatures();
|
|
366
|
+
break;
|
|
367
|
+
|
|
368
|
+
case 'xAxisColumnName':
|
|
369
|
+
this.scatter?.setOptions({xColumnName: this.xAxisColumnName});
|
|
370
|
+
this.updateAutoAxesSelection();
|
|
371
|
+
break;
|
|
372
|
+
|
|
373
|
+
case 'yAxisColumnName':
|
|
374
|
+
this.scatter?.setOptions({yColumnName: this.yAxisColumnName});
|
|
375
|
+
this.updateAutoAxesSelection();
|
|
376
|
+
break;
|
|
377
|
+
|
|
378
|
+
case 'autoAxesSelection':
|
|
379
|
+
this.updateAxesColumnOptions();
|
|
380
|
+
break;
|
|
381
|
+
|
|
382
|
+
case 'autoLabelsSelection':
|
|
383
|
+
this.updateLabelColumnOptions();
|
|
384
|
+
break;
|
|
385
|
+
|
|
386
|
+
case 'labelColumnsColumnNames':
|
|
387
|
+
if (this.toChangeAutoLabelsSelection)
|
|
388
|
+
this.setOptions({autoLabelsSelection: null});
|
|
389
|
+
this.toChangeAutoLabelsSelection = true;
|
|
390
|
+
break;
|
|
391
|
+
} // switch
|
|
392
|
+
|
|
393
|
+
this.render((property.name === 'minimizeColumnNames') || (property.name === 'maximizeColumnNames'));
|
|
394
|
+
} // onPropertyChanged
|
|
395
|
+
|
|
396
|
+
render(computeData = false) {
|
|
397
|
+
if (!this.isApplicable || this.hasCommonMinMaxNames) {
|
|
398
|
+
if (this.scatter != null)
|
|
399
|
+
this.scatter.root.hidden = true;
|
|
400
|
+
|
|
401
|
+
this._showErrorMessage(this.errMsg);
|
|
402
|
+
return;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
this.removeErrDiv();
|
|
406
|
+
|
|
407
|
+
if (computeData) {
|
|
408
|
+
this.computeParetoFront();
|
|
409
|
+
this.updateOptimizedColNames();
|
|
410
|
+
this.updateAxesColumnOptions();
|
|
411
|
+
this.colorColumnName = (this.optimizedColNames.length < 1) ? null : COL_NAME.OPT;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
this.setScatterOptions();
|
|
415
|
+
if (this.scatter != null)
|
|
416
|
+
this.scatter.root.hidden = false;
|
|
417
|
+
} // render
|
|
418
|
+
|
|
419
|
+
private updateCommonMinMaxFeatures(): void {
|
|
420
|
+
if ((this.minimizeColumnNames == null) || (this.maximizeColumnNames == null)) {
|
|
421
|
+
this.hasCommonMinMaxNames = false;
|
|
422
|
+
return;
|
|
423
|
+
}
|
|
424
|
+
const commonMinMaxNames = this.minimizeColumnNames.filter((name) => this.maximizeColumnNames.includes(name));
|
|
425
|
+
|
|
426
|
+
if (commonMinMaxNames.length > 0) {
|
|
427
|
+
this.hasCommonMinMaxNames = true;
|
|
428
|
+
const namesLine = commonMinMaxNames.map((name) => `"${name}"`).join(', ');
|
|
429
|
+
this.errMsg = `Cannot minimize and maximize features at the same time: ${namesLine}.`;
|
|
430
|
+
} else
|
|
431
|
+
this.hasCommonMinMaxNames = false;
|
|
432
|
+
} // updateCommonMinMaxFeatures
|
|
433
|
+
|
|
434
|
+
private updateOptimizedColNames() {
|
|
435
|
+
this.optimizedColNames = [];
|
|
436
|
+
|
|
437
|
+
if (this.minimizeColumnNames != null)
|
|
438
|
+
this.optimizedColNames.push(...this.minimizeColumnNames);
|
|
439
|
+
|
|
440
|
+
if (this.maximizeColumnNames != null)
|
|
441
|
+
this.optimizedColNames.push(...this.maximizeColumnNames);
|
|
442
|
+
} // updateOptimizedColNames
|
|
443
|
+
|
|
444
|
+
private updateAxesColumnOptions(): void {
|
|
445
|
+
if (!this.autoAxesSelection)
|
|
446
|
+
return;
|
|
447
|
+
|
|
448
|
+
const length = this.optimizedColNames.length;
|
|
449
|
+
|
|
450
|
+
if (length < 1)
|
|
451
|
+
return;
|
|
452
|
+
|
|
453
|
+
const xIdx = this.optimizedColNames.indexOf(this.xAxisColumnName);
|
|
454
|
+
const yIdx = this.optimizedColNames.indexOf(this.yAxisColumnName);
|
|
455
|
+
|
|
456
|
+
if (length > 1) {
|
|
457
|
+
if (xIdx < 0) {
|
|
458
|
+
this.toChangeAutoAxesSelection = false;
|
|
459
|
+
this.setOptions({xAxisColumnName: this.optimizedColNames[yIdx !== 0 ? 0 : 1]});
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
if (yIdx < 0) {
|
|
463
|
+
this.toChangeAutoAxesSelection = false;
|
|
464
|
+
this.setOptions({yAxisColumnName: this.optimizedColNames[xIdx !== 1 ? 1 : 0]});
|
|
465
|
+
}
|
|
466
|
+
} else {
|
|
467
|
+
if ((xIdx < 0) && (yIdx < 0)) {
|
|
468
|
+
this.toChangeAutoAxesSelection = false;
|
|
469
|
+
this.setOptions({xAxisColumnName: this.optimizedColNames[0]});
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
} // updateAxesColumnOptions
|
|
473
|
+
|
|
474
|
+
private updateLabelColumnOptions(): void {
|
|
475
|
+
if (!this.autoLabelsSelection)
|
|
476
|
+
return;
|
|
477
|
+
|
|
478
|
+
this.toChangeAutoLabelsSelection = false;
|
|
479
|
+
this.setOptions({labelColumnsColumnNames: [...this.autoLabelColNames]});
|
|
480
|
+
} // updateLabelColumnOptions
|
|
481
|
+
|
|
482
|
+
private getLabelColNames(): string[] {
|
|
483
|
+
return this.dataFrame.columns.toList().filter((col) => {
|
|
484
|
+
if (col.type !== DG.COLUMN_TYPE.STRING)
|
|
485
|
+
return false;
|
|
486
|
+
|
|
487
|
+
return col.categories.length === this.rowCount;
|
|
488
|
+
}).map((col) => col.name);
|
|
489
|
+
} // getLabelColNames
|
|
490
|
+
} // ParetoFrontViewer
|