UNPKG

8.75 kBPlain TextView Raw
1import { deepMix, each, every, get, isNil, isNumber } from '@antv/util';
2import { LAYER } from '../constant';
3import { IGroup } from '../dependents';
4import { AxisCfg, Condition, Datum, FacetCfg, FacetData, FacetDataFilter, Region } from '../interface';
5
6import View from '../chart/view';
7import { getAxisOption } from '../util/axis';
8
9/**
10 * facet 基类
11 * - 定义生命周期,方便自定义 facet
12 * - 提供基础的生命流程方法
13 *
14 * 生命周期:
15 *
16 * 初始化 init
17 * 1. 初始化容器
18 * 2. 数据分面,生成分面布局信息
19 *
20 * 渲染阶段 render
21 * 1. view 创建
22 * 2. title
23 * 3. axis
24 *
25 * 清除阶段 clear
26 * 1. 清除 view
27 *
28 * 销毁阶段 destroy
29 * 1. clear
30 * 2. 清除事件
31 * 3. 清除 group
32 */
33export abstract class Facet<C extends FacetCfg<FacetData> = FacetCfg<FacetData>, F extends FacetData = FacetData> {
34 /** 分面所在的 view */
35 public view: View;
36 /** 分面容器 */
37 public container: IGroup;
38 /** 是否销毁 */
39 public destroyed: boolean = false;
40
41 /** 分面的配置项 */
42 protected cfg: C;
43 /** 分面之后的所有分面数据结构 */
44 protected facets: F[] = [];
45
46 constructor(view: View, cfg: C) {
47 this.view = view;
48 this.cfg = deepMix({}, this.getDefaultCfg(), cfg);
49 }
50
51 /**
52 * 初始化过程
53 */
54 public init() {
55 // 初始化容器
56 if (!this.container) {
57 this.container = this.createContainer();
58 }
59
60 // 生成分面布局信息
61 const data = this.view.getData();
62 this.facets = this.generateFacets(data);
63 }
64
65 /**
66 * 渲染分面,由上层 view 调用。包括:
67 * - 分面 view
68 * - 轴
69 * - title
70 *
71 * 子类可以复写,添加一些其他组件,比如滚动条等
72 */
73 public render() {
74 this.renderViews();
75 }
76
77 /**
78 * 更新 facet
79 */
80 public update() {
81 // 其实不用做任何事情,因为 facet 最终生成的 View 和 Geometry 都在父 view 的更新中处理了
82 }
83
84 /**
85 * 清空,clear 之后如果还需要使用,需要重新调用 init 初始化过程
86 * 一般在数据有变更的时候调用,重新进行数据的分面逻辑
87 */
88 public clear() {
89 this.clearFacetViews();
90 }
91
92 /**
93 * 销毁
94 */
95 public destroy() {
96 this.clear();
97
98 if (this.container) {
99 this.container.remove(true);
100 this.container = undefined;
101 }
102
103 this.destroyed = true;
104 this.view = undefined;
105 this.facets = [];
106 }
107
108 /**
109 * 根据 facet 生成 view,可以给上层自定义使用
110 * @param facet
111 */
112 protected facetToView(facet: F): View {
113 const { region, data, padding = this.cfg.padding } = facet;
114
115 const view = this.view.createView({
116 region,
117 padding,
118 });
119
120 // 设置分面的数据
121 view.data(data || []);
122 facet.view = view;
123
124 // 前置钩子
125 this.beforeEachView(view, facet);
126
127 const { eachView } = this.cfg;
128 if (eachView) {
129 eachView(view, facet);
130 }
131
132 // 后置钩子
133 this.afterEachView(view, facet);
134
135 return view;
136 }
137
138 // 创建容器
139 private createContainer(): IGroup {
140 const foregroundGroup = this.view.getLayer(LAYER.FORE);
141 return foregroundGroup.addGroup();
142 }
143
144 /**
145 * 初始化 view
146 */
147 private renderViews() {
148 this.createFacetViews();
149 }
150
151 /**
152 * 创建 分面 view
153 */
154 private createFacetViews(): View[] {
155 // 使用分面数据 创建分面 view
156 return this.facets.map((facet): View => {
157 return this.facetToView(facet);
158 });
159 }
160
161 /**
162 * 从 view 中清除 facetView
163 */
164 private clearFacetViews() {
165 // 从 view 中移除分面 view
166 each(this.facets, (facet) => {
167 if (facet.view) {
168 this.view.removeView(facet.view);
169 facet.view = undefined;
170 }
171 });
172 }
173
174 /**
175 * 解析 spacing
176 */
177 private parseSpacing() {
178 /**
179 * @example
180 *
181 * // 仅使用百分比或像素值
182 * // 横向间隔为 10%,纵向间隔为 10%
183 * ['10%', '10%']
184 * // 横向间隔为 10px,纵向间隔为 10px
185 * [10, 10]
186 *
187 * // 同时使用百分比和像素值
188 * ['10%', 10]
189 * // 横向间隔为 10%,纵向间隔为 10px
190 */
191 const { width, height } = this.view.viewBBox;
192 const { spacing } = this.cfg;
193 return spacing.map((s: number, idx: number) => {
194 if (isNumber(s)) return s / (idx === 0 ? width : height);
195 else return parseFloat(s) / 100;
196 });
197 }
198
199 // 其他一些提供给子类使用的方法
200
201 /**
202 * 获取这个字段对应的所有值,数组
203 * @protected
204 * @param data 数据
205 * @param field 字段名
206 * @return 字段对应的值
207 */
208 protected getFieldValues(data: Datum[], field: string): string[] {
209 const rst = [];
210 const cache: Record<string, boolean> = {};
211
212 // 去重、去除 Nil 值
213 each(data, (d: Datum) => {
214 const value = d[field];
215 if (!isNil(value) && !cache[value]) {
216 rst.push(value);
217 cache[value] = true;
218 }
219 });
220
221 return rst;
222 }
223
224 /**
225 * 获得每个分面的 region,平分区域
226 * @param rows row 总数
227 * @param cols col 总数
228 * @param xIndex x 方向 index
229 * @param yIndex y 方向 index
230 */
231 protected getRegion(rows: number, cols: number, xIndex: number, yIndex: number): Region {
232 const [xSpacing, ySpacing] = this.parseSpacing();
233 // 每两个分面区域横向间隔xSPacing, 纵向间隔ySpacing
234 // 每个分面区域的横纵占比
235 /**
236 * ratio * num + spacing * (num - 1) = 1
237 * => ratio = (1 - (spacing * (num - 1))) / num
238 * = (1 + spacing) / num - spacing
239 *
240 * num 对应 cols/rows
241 * spacing 对应 xSpacing/ySpacing
242 */
243 const xRatio = (1 + xSpacing) / (cols === 0 ? 1 : cols) - xSpacing;
244 const yRatio = (1 + ySpacing) / (rows === 0 ? 1 : rows) - ySpacing;
245
246 // 得到第 index 个分面区域百分比位置
247 const start = {
248 x: (xRatio + xSpacing) * xIndex,
249 y: (yRatio + ySpacing) * yIndex,
250 };
251 const end = {
252 x: start.x + xRatio,
253 y: start.y + yRatio,
254 };
255 return { start, end };
256 }
257
258 protected getDefaultCfg() {
259 return {
260 eachView: undefined,
261 showTitle: true,
262 spacing: [0, 0],
263 padding: 10,
264 fields: [],
265 };
266 }
267
268 /**
269 * 默认的 title 样式,因为有的分面是 title,有的分面配置是 columnTitle、rowTitle
270 */
271 protected getDefaultTitleCfg() {
272 // @ts-ignore
273 const fontFamily = this.view.getTheme().fontFamily;
274 return {
275 style: {
276 fontSize: 14,
277 fill: '#666',
278 fontFamily,
279 },
280 };
281 }
282
283 /**
284 * 处理 axis 的默认配置
285 * @param view
286 * @param facet
287 */
288 protected processAxis(view: View, facet: F) {
289 const options = view.getOptions();
290
291 const coordinateOption = options.coordinate;
292 const geometries = view.geometries;
293
294 const coordinateType = get(coordinateOption, 'type', 'rect');
295
296 if (coordinateType === 'rect' && geometries.length) {
297 if (isNil(options.axes)) {
298 // @ts-ignore
299 options.axes = {};
300 }
301 const axes = options.axes;
302
303 const [x, y] = geometries[0].getXYFields();
304
305 const xOption = getAxisOption(axes, x);
306 const yOption = getAxisOption(axes, y);
307
308 if (xOption !== false) {
309 options.axes[x] = this.getXAxisOption(x, axes, xOption, facet);
310 }
311
312 if (yOption !== false) {
313 options.axes[y] = this.getYAxisOption(y, axes, yOption, facet);
314 }
315 }
316 }
317
318 /**
319 * 获取分面数据
320 * @param conditions
321 */
322 protected getFacetDataFilter(conditions: Condition[]): FacetDataFilter {
323 return (datum: Datum) => {
324 // 过滤出全部满足条件的数据
325 return every(conditions, (condition) => {
326 const { field, value } = condition;
327
328 if (!isNil(value) && field) {
329 return datum[field] === value;
330 }
331 return true;
332 });
333 };
334 }
335
336 /**
337 * @override 开始处理 eachView
338 * @param view
339 * @param facet
340 */
341 protected abstract beforeEachView(view: View, facet: F);
342
343 /**
344 * @override 处理 eachView 之后
345 * @param view
346 * @param facet
347 */
348 protected abstract afterEachView(view: View, facet: F);
349
350 /**
351 * @override 生成分面数据,包含布局
352 * @param data
353 */
354 protected abstract generateFacets(data: Datum[]): F[];
355
356 /**
357 * 获取 x 轴的配置
358 * @param x
359 * @param axes
360 * @param option
361 * @param facet
362 */
363 protected abstract getXAxisOption(x: string, axes: any, option: AxisCfg, facet: F): object;
364
365 /**
366 * 获取 y 轴的配置
367 * @param y
368 * @param axes
369 * @param option
370 * @param facet
371 */
372 protected abstract getYAxisOption(y: string, axes: any, option: AxisCfg, facet: F): object;
373}