1 | import EventEmitter from 'wolfy87-eventemitter';
|
2 | import {
|
3 | assign,
|
4 | clone,
|
5 | deepMix,
|
6 | find,
|
7 | forIn,
|
8 | isArray,
|
9 | isPlainObject,
|
10 | isMatch,
|
11 | isObject,
|
12 | isString,
|
13 | keys,
|
14 | pick,
|
15 | } from '@antv/util';
|
16 | import { DataSet } from './data-set';
|
17 | import { StatisticsApi } from './api/statistics';
|
18 | import { PartitionApi } from './api/partition';
|
19 | import { HierarchyApi } from './api/hierarchy';
|
20 | import { GeoApi } from './api/geo';
|
21 | import { TransformsParams } from './transform-params';
|
22 | import { ConnectorParams } from './connector-params';
|
23 |
|
24 | function cloneOptions(options: any): any {
|
25 | const result: any = {};
|
26 | forIn(options as any, (value: any, key: string) => {
|
27 | if (isObject(value) && (value as any).isView) {
|
28 | result[key] = value;
|
29 | } else if (isArray(value)) {
|
30 | result[key] = value.concat([]);
|
31 | } else if (isPlainObject(value)) {
|
32 | result[key] = clone(value);
|
33 | } else {
|
34 | result[key] = value;
|
35 | }
|
36 | });
|
37 | return result;
|
38 | }
|
39 |
|
40 | export interface ViewOptions {
|
41 | watchingStates?: string[];
|
42 | }
|
43 |
|
44 | type TransformOptions<T extends keyof TransformsParams = any> = { type: T } & TransformsParams[T];
|
45 | type ConnectorOptions<T extends keyof ConnectorParams = any> = { type: T } & ConnectorParams[T][1];
|
46 |
|
47 | interface CustomSource {
|
48 | source: any;
|
49 | options: any;
|
50 | }
|
51 |
|
52 |
|
53 |
|
54 |
|
55 |
|
56 | export class View extends EventEmitter {
|
57 | static DataSet: typeof DataSet;
|
58 | |
59 |
|
60 |
|
61 | dataSet: DataSet | null;
|
62 | |
63 |
|
64 |
|
65 | loose: boolean;
|
66 | |
67 |
|
68 |
|
69 | isView = true;
|
70 | |
71 |
|
72 |
|
73 | isDataView = true;
|
74 | |
75 |
|
76 |
|
77 | private watchingStates: string[] | null = null;
|
78 | |
79 |
|
80 |
|
81 | dataType = 'table';
|
82 | |
83 |
|
84 |
|
85 | transforms: TransformOptions[] = [];
|
86 | |
87 |
|
88 |
|
89 | origin: any[] = [];
|
90 | |
91 |
|
92 |
|
93 | rows: any[] = [];
|
94 | _source!: CustomSource;
|
95 |
|
96 |
|
97 | _tagCloud: any;
|
98 |
|
99 |
|
100 | graph!: {
|
101 | nodes: any[];
|
102 | edges: any[];
|
103 | };
|
104 | nodes!: any[];
|
105 | edges!: any[];
|
106 |
|
107 |
|
108 | _projectedAs!: string[];
|
109 |
|
110 | _gridRows!: any;
|
111 | _HexJSON: any;
|
112 | _GridHexJSON: any;
|
113 |
|
114 | constructor(options?: ViewOptions);
|
115 | constructor(dataSet?: DataSet, options?: ViewOptions);
|
116 | constructor(dataSet?: any, options?: any) {
|
117 | super();
|
118 | if (dataSet && dataSet.isDataSet) {
|
119 | this.dataSet = dataSet;
|
120 | } else {
|
121 | this.dataSet = null;
|
122 | options = dataSet;
|
123 | }
|
124 | this.loose = !this.dataSet;
|
125 |
|
126 |
|
127 | if (options) {
|
128 | this.watchingStates = options.watchingStates;
|
129 | }
|
130 | if (!this.loose) {
|
131 | const { watchingStates } = this;
|
132 | dataSet.on('statechange', (name: string) => {
|
133 | if (isArray(watchingStates)) {
|
134 | if (watchingStates.indexOf(name) > -1) {
|
135 | this._reExecute();
|
136 | }
|
137 | } else {
|
138 | this._reExecute();
|
139 | }
|
140 | });
|
141 | }
|
142 | }
|
143 |
|
144 | private _parseStateExpression(expr: string): string | undefined {
|
145 | const dataSet = this.dataSet;
|
146 | if (dataSet === null) return undefined;
|
147 | const matched = /^\$state\.(\w+)/.exec(expr);
|
148 | if (matched) {
|
149 | return dataSet.state[matched[1]];
|
150 | }
|
151 | return expr;
|
152 | }
|
153 |
|
154 | private _preparseOptions(options: any): any {
|
155 | const optionsCloned = cloneOptions(options);
|
156 | if (this.loose) {
|
157 | return optionsCloned;
|
158 | }
|
159 | forIn(optionsCloned, (value, key) => {
|
160 | if (isString(value) && /^\$state\./.test(value)) {
|
161 | optionsCloned[key] = this._parseStateExpression(value);
|
162 | }
|
163 | });
|
164 | return optionsCloned;
|
165 | }
|
166 |
|
167 |
|
168 | private _prepareSource(source: any, options?: any): View {
|
169 |
|
170 | this._source = { source, options };
|
171 | if (!options) {
|
172 | if (source instanceof View || isString(source)) {
|
173 | this.origin = View.DataSet.getConnector('default')(source, this.dataSet);
|
174 | } else if (isArray(source)) {
|
175 |
|
176 | this.origin = source;
|
177 | } else if (isObject(source) && (source as ConnectorOptions).type) {
|
178 | const opts = this._preparseOptions(source);
|
179 | this.origin = View.DataSet.getConnector(opts.type)(opts, this);
|
180 | } else {
|
181 | throw new TypeError('Invalid source');
|
182 | }
|
183 | } else {
|
184 | const opts = this._preparseOptions(options);
|
185 | this.origin = View.DataSet.getConnector(opts.type)(source, opts, this);
|
186 | }
|
187 | this.rows = deepMix([], this.origin);
|
188 | return this;
|
189 | }
|
190 |
|
191 | |
192 |
|
193 |
|
194 |
|
195 |
|
196 |
|
197 |
|
198 |
|
199 |
|
200 | source(source: string): View;
|
201 | source(source: any[]): View;
|
202 | source(source: View): View;
|
203 | source<T extends keyof ConnectorParams>(source: ConnectorParams[T][0], options: ConnectorOptions<T>): View;
|
204 | source(source: any, options?: any): View {
|
205 | this._prepareSource(source, options)._reExecuteTransforms();
|
206 | this.trigger('change', []);
|
207 | return this;
|
208 | }
|
209 |
|
210 | |
211 |
|
212 |
|
213 |
|
214 | transform<T extends keyof TransformsParams>(options?: TransformOptions<T>): View {
|
215 | if (options && options.type) {
|
216 | this.transforms.push(options);
|
217 | this._executeTransform(options);
|
218 | }
|
219 | return this;
|
220 | }
|
221 |
|
222 | private _executeTransform(options: TransformOptions): void {
|
223 | options = this._preparseOptions(options);
|
224 | const transform = View.DataSet.getTransform(options.type);
|
225 | transform(this, options);
|
226 | }
|
227 |
|
228 | private _reExecuteTransforms(): void {
|
229 | this.transforms.forEach((options) => {
|
230 | this._executeTransform(options);
|
231 | });
|
232 | }
|
233 |
|
234 | addRow(row: any): void {
|
235 | this.rows.push(row);
|
236 | }
|
237 |
|
238 | removeRow(index: number): void {
|
239 | this.rows.splice(index, 1);
|
240 | }
|
241 |
|
242 | updateRow(index: number, newRow: any): void {
|
243 | assign(this.rows[index], newRow);
|
244 | }
|
245 |
|
246 | findRows(query: any): any[] {
|
247 | return this.rows.filter((row) => isMatch(row, query));
|
248 | }
|
249 |
|
250 | findRow(query: any): any {
|
251 | return find(this.rows, query);
|
252 | }
|
253 |
|
254 |
|
255 | getColumnNames(): string[] {
|
256 | const firstRow = this.rows[0];
|
257 | if (firstRow) {
|
258 | return keys(firstRow);
|
259 | }
|
260 | return [];
|
261 | }
|
262 |
|
263 | getColumnName(index: number): string {
|
264 | return this.getColumnNames()[index];
|
265 | }
|
266 |
|
267 | getColumnIndex(columnName: string): number {
|
268 | const columnNames = this.getColumnNames();
|
269 | return columnNames.indexOf(columnName);
|
270 | }
|
271 |
|
272 | getColumn(columnName: string): any[] {
|
273 | return this.rows.map((row) => row[columnName]);
|
274 | }
|
275 |
|
276 | getColumnData(columnName: string): any[] {
|
277 | return this.getColumn(columnName);
|
278 | }
|
279 |
|
280 |
|
281 | getSubset(startRowIndex: number, endRowIndex: number, columnNames: string[]): any[] {
|
282 | const subset = [];
|
283 | for (let i = startRowIndex; i <= endRowIndex; i++) {
|
284 | subset.push(pick(this.rows[i], columnNames));
|
285 | }
|
286 | return subset;
|
287 | }
|
288 |
|
289 | toString(prettyPrint = false): string {
|
290 | if (prettyPrint) {
|
291 | return JSON.stringify(this.rows, null, 2);
|
292 | }
|
293 | return JSON.stringify(this.rows);
|
294 | }
|
295 |
|
296 | private _reExecute(): void {
|
297 | const { source, options } = this._source;
|
298 | this._prepareSource(source, options);
|
299 | this._reExecuteTransforms();
|
300 | this.trigger('change', []);
|
301 | }
|
302 | }
|
303 |
|
304 | export interface View extends StatisticsApi, PartitionApi, HierarchyApi, GeoApi {}
|