UNPKG

7.63 kBPlain TextView Raw
1import EventEmitter from 'wolfy87-eventemitter';
2import {
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';
16import { DataSet } from './data-set';
17import { StatisticsApi } from './api/statistics';
18import { PartitionApi } from './api/partition';
19import { HierarchyApi } from './api/hierarchy';
20import { GeoApi } from './api/geo';
21import { TransformsParams } from './transform-params';
22import { ConnectorParams } from './connector-params';
23
24function 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
40export interface ViewOptions {
41 watchingStates?: string[];
42}
43
44type TransformOptions<T extends keyof TransformsParams = any> = { type: T } & TransformsParams[T];
45type ConnectorOptions<T extends keyof ConnectorParams = any> = { type: T } & ConnectorParams[T][1];
46
47interface CustomSource {
48 source: any;
49 options: any;
50}
51
52/**
53 * 数据视图
54 * @public
55 */
56export class View extends EventEmitter {
57 static DataSet: typeof DataSet;
58 /**
59 * 关联的数据集
60 */
61 dataSet: DataSet | null;
62 /**
63 * 是否关联了数据集
64 */
65 loose: boolean;
66 /**
67 * 是否是View
68 */
69 isView = true;
70 /**
71 * 是否是View
72 */
73 isDataView = true; // alias
74 /**
75 *
76 */
77 private watchingStates: string[] | null = null;
78 /**
79 * 数据视图类型
80 */
81 dataType = 'table';
82 /**
83 * 已应用的 transform
84 */
85 transforms: TransformOptions[] = [];
86 /**
87 * 原始数据
88 */
89 origin: any[] = [];
90 /**
91 * 存储处理后的数据
92 */
93 rows: any[] = [];
94 _source!: CustomSource;
95
96 // tag cloud
97 _tagCloud: any;
98
99 // graph
100 graph!: {
101 nodes: any[];
102 edges: any[];
103 };
104 nodes!: any[];
105 edges!: any[];
106
107 // geo
108 _projectedAs!: string[];
109 // hexjson
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 // TODO:
126 // assign(me, options);
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 // connectors
168 private _prepareSource(source: any, options?: any): View {
169 // warning me.origin is protected
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 // TODO branch: if source is like ['dataview1', 'dataview2']
176 this.origin = source;
177 } else if (isObject(source) && (source as ConnectorOptions).type) {
178 const opts = this._preparseOptions(source); // connector without 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 * @remarks
195 * data 是原始数据,可能是字符串,也可能是数组、对象,或者另一个数据视图实例。options 里指定了载入数据使用的 connector 和载入时使用的配置项。
196 *
197 * @param source - 数据
198 * @param options- 数据解析配置
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 * 执行数据处理数据。执行完这个函数后,transform 会被存储
212 * @param options - 某种类型的transform
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 // columns
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 // data process
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
304export interface View extends StatisticsApi, PartitionApi, HierarchyApi, GeoApi {}