UNPKG

8.53 kBPlain TextView Raw
1import { JSX } from '../jsx/jsx-namespace';
2import { ScaleConfig } from '@antv/scale';
3import { each, findIndex, isArray } from '@antv/util';
4import Component from '../base/component';
5import equal from '../base/equal';
6import Layout from '../base/layout';
7import Coord from '../coord';
8import Children from '../children';
9// types
10import LayoutController from '../controller/layout';
11import CoordController from '../controller/coord';
12import ScaleController from '../controller/scale';
13
14interface Point {
15 x: number;
16 y: number;
17}
18
19export interface Props {
20 zIndex?: number;
21 data: any;
22 scale?: any;
23 coord?: any;
24 start?: Point;
25 end?: Point;
26 children: any;
27}
28
29export interface ChartChildProps {
30 data?: any;
31 chart?: Chart;
32 coord?: Coord;
33 layout?: Layout;
34 [k: string]: any;
35}
36
37// type Scales = {
38// [field: string]: Scale;
39// };
40
41interface IChart {
42 props: Props;
43}
44
45export interface PositionLayout {
46 position: 'top' | 'right' | 'bottom' | 'left';
47 width: number;
48 height: number;
49}
50
51export interface ComponentPosition {
52 component: Component;
53 layout: PositionLayout | PositionLayout[];
54}
55
56// 统计图表
57class Chart extends Component implements IChart {
58 data: any;
59
60 private layout: Layout;
61 // 坐标系
62 private coord: Coord;
63 private componentsPosition: ComponentPosition[] = [];
64
65 // controller
66 private layoutController: LayoutController;
67 private coordController: CoordController;
68 private scaleController: ScaleController;
69 scale: ScaleController;
70
71 constructor(props, context?, updater?) {
72 super(props, context, updater);
73
74 const { data, coord: coordOption, scale = [] } = props;
75
76 this.layoutController = new LayoutController();
77 this.coordController = new CoordController();
78 this.scaleController = new ScaleController(data);
79 this.scale = this.scaleController;
80
81 const { layoutController, coordController, scaleController } = this;
82
83 // 布局
84 const style = this.getStyle(props, context);
85 this.layout = layoutController.create(style);
86
87 // 坐标系
88 this.coord = coordController.create(coordOption, this.layout);
89
90 // scale
91 scaleController.create(scale);
92
93 this.data = data;
94
95 // state
96 this.state = {
97 filters: {},
98 };
99 }
100
101 // props 更新
102 willReceiveProps(nextProps, context) {
103 const { layoutController, coordController, scaleController, props: lastProps } = this;
104 const { style: nextStyle, data: nextData, scale: nextScale } = nextProps;
105 const { style: lastStyle, data: lastData, scale: lastScale } = lastProps;
106
107 // 布局
108 if (!equal(nextStyle, lastStyle) || context !== this.context) {
109 const style = this.getStyle(nextProps, context);
110 this.layout = layoutController.create(style);
111 coordController.updateLayout(this.layout);
112 }
113
114 if (nextData !== lastData) {
115 scaleController.changeData(nextData);
116 }
117
118 // scale
119 if (!equal(nextScale, lastScale)) {
120 scaleController.update(nextScale);
121 }
122 }
123
124 willUpdate() {
125 const { coordController, props } = this;
126 // render 时要重置 coord 范围,重置后需要让所有子组件都重新render
127 // 所以这里不比较是否有差异,每次都新建,让所有子组件重新render
128 this.coord = coordController.create(props.coord, this.layout);
129 }
130
131 private getStyle(props, context) {
132 const { theme, px2hd, left, top, width, height } = context;
133 const { style } = props;
134 return px2hd({
135 left,
136 top,
137 width,
138 height,
139 ...theme.chart,
140 ...style,
141 });
142 }
143
144 // 给需要显示的组件留空
145 layoutCoord(layout: PositionLayout) {
146 const { coord } = this;
147 const { position, width: boxWidth, height: boxHeight } = layout;
148 let { left, top, width, height } = coord;
149 switch (position) {
150 case 'left':
151 left += boxWidth;
152 width = Math.max(0, width - boxWidth);
153 break;
154 case 'right':
155 width = Math.max(0, width - boxWidth);
156 break;
157 case 'top':
158 top += boxHeight;
159 height = Math.max(0, height - boxHeight);
160 break;
161 case 'bottom':
162 height = Math.max(0, height - boxHeight);
163 break;
164 }
165 coord.update({ left, top, width, height });
166 }
167
168 resetCoordLayout() {
169 const { coord, layout } = this;
170 coord.update(layout);
171 }
172
173 updateCoordLayout(layout: PositionLayout | PositionLayout[]) {
174 if (isArray(layout)) {
175 layout.forEach((item) => {
176 this.layoutCoord(item);
177 });
178 return;
179 }
180 this.layoutCoord(layout);
181 }
182
183 updateCoordFor(component: Component, layout: PositionLayout | PositionLayout[]) {
184 if (!layout) return;
185 const { componentsPosition } = this;
186 const componentPosition = { component, layout };
187 const existIndex = findIndex(componentsPosition, (item) => {
188 return item.component === component;
189 });
190 // 说明是已经存在的组件
191 if (existIndex > -1) {
192 componentsPosition.splice(existIndex, 1, componentPosition);
193
194 // 先重置,然后整体重新算一次
195 this.resetCoordLayout();
196 componentsPosition.forEach((componentPosition) => {
197 const { layout } = componentPosition;
198 this.updateCoordLayout(layout);
199 });
200 return;
201 }
202
203 // 是新组件,直接添加
204 componentsPosition.push(componentPosition);
205 this.updateCoordLayout(layout);
206 }
207
208 getGeometrys() {
209 const { children } = this;
210 const geometrys: Component[] = [];
211 // @ts-ignore
212 Children.toArray(children).forEach((element) => {
213 if (!element) return false;
214 const { component } = element;
215 if (component && component.isGeometry) {
216 geometrys.push(component);
217 }
218 });
219 return geometrys;
220 }
221
222 /**
223 * calculate dataset's position on canvas
224 * @param {Object} record the dataset
225 * @return {Object} return the position
226 */
227 getPosition(record) {
228 const coord = this.getCoord();
229 const xScale = this.getXScales()[0];
230 const xField = xScale.field;
231 const yScales = this.getYScales();
232 // default first
233 let yScale = yScales[0];
234 let yField = yScale.field;
235 for (let i = 0, len = yScales.length; i < len; i++) {
236 const scale = yScales[i];
237 const field = scale.field;
238 if (record[field]) {
239 yScale = scale;
240 yField = field;
241 break;
242 }
243 }
244 const x = xScale.scale(record[xField]);
245 const y = yScale.scale(record[yField]);
246 return coord.convertPoint({ x, y });
247 }
248
249 getSnapRecords(point, inCoordRange?) {
250 const geometrys = this.getGeometrys();
251 if (!geometrys.length) return;
252 // @ts-ignore
253 return geometrys[0].getSnapRecords(point, inCoordRange);
254 }
255
256 getLegendItems(point?) {
257 const geometrys = this.getGeometrys();
258 if (!geometrys.length) return;
259 // @ts-ignore
260 return geometrys[0].getLegendItems(point);
261 }
262
263 setScale(field: string, option: ScaleConfig) {
264 this.scaleController.setScale(field, option);
265 }
266
267 getScale(field: string) {
268 return this.scaleController.getScale(field);
269 }
270
271 getScales() {
272 return this.scaleController.getScales();
273 }
274
275 getXScales() {
276 const geometrys = this.getGeometrys();
277 return geometrys.map((component) => {
278 // @ts-ignore
279 return component.getXScale();
280 });
281 }
282
283 getYScales() {
284 const geometrys = this.getGeometrys();
285 return geometrys.map((component) => {
286 // @ts-ignore
287 return component.getYScale();
288 });
289 }
290
291 getCoord() {
292 return this.coord;
293 }
294
295 filter(field: string, condition) {
296 const { filters } = this.state;
297 this.setState({
298 filters: {
299 ...filters,
300 [field]: condition,
301 },
302 });
303 }
304
305 _getRenderData() {
306 const { props, state } = this;
307 const { data } = props;
308 const { filters } = state;
309 if (!filters || !Object.keys(filters).length) {
310 return data;
311 }
312 let filteredData = data;
313 each(filters, (condition, field) => {
314 if (!condition) return;
315 filteredData = filteredData.filter((record) => {
316 return condition(record[field], record);
317 });
318 });
319 return filteredData;
320 }
321
322 render(): JSX.Element {
323 const { props, layout, coord } = this;
324 const { children, data: originData } = props;
325 if (!originData) return null;
326 const data = this._getRenderData();
327
328 return Children.map(children, (child) => {
329 return Children.cloneElement(child, {
330 chart: this,
331 coord,
332 data,
333 layout,
334 });
335 });
336 }
337}
338
339export default Chart;