UNPKG

7.47 kBTypeScriptView Raw
1import { jsx } from '../../jsx';
2import { renderShape } from '../../base/diff';
3import Component from '../../base/component';
4import Chart from '../../chart';
5import { find, isFunction } from '@antv/util';
6import { getElementsByClassName, isInBBox } from '../../util';
7import { Style, TextAttrs } from '../../types';
8
9interface LegendItem {
10 /**
11 * 标记颜色。
12 */
13 color: string;
14 /**
15 * 名称。
16 */
17 name: string;
18 /**
19 * 值。
20 */
21 value?: string | number;
22 /**
23 * 图例标记。
24 */
25 marker?: string;
26}
27export interface LegendProps {
28 /**
29 * 图表。
30 */
31 readonly chart?: Chart;
32 /**
33 * 图例的显示位置。默认为 top。
34 */
35 position?: 'right' | 'left' | 'top' | 'bottom';
36 /**
37 * 图例宽度
38 */
39 width?: number | string;
40 /**
41 * 图例高度
42 */
43 height?: number | string;
44 /**
45 * legend 和图表内容的间距
46 */
47 margin?: number | string;
48 /**
49 * 回调函数,用于格式化图例每项的文本显示。
50 */
51 itemFormatter?: (value: string, name: string) => string;
52 /**
53 * 图例项列表。
54 */
55 items?: LegendItem[];
56 /**
57 * 图例样式。
58 */
59 style?: Style;
60 /**
61 * 图例标记。
62 */
63 marker?: 'circle' | 'square';
64 /**
65 * 用于设置图例项的文本样式
66 */
67 nameStyle?: TextAttrs;
68 /**
69 * 用于设置图例项的文本样式
70 */
71 valueStyle?: TextAttrs;
72 /**
73 * value展示文案的前缀
74 */
75 valuePrefix?: string;
76 /**
77 * 是否可点击
78 */
79 clickable?: boolean;
80 onClick?: (item: LegendItem) => void;
81}
82
83export default (View) => {
84 return class Legend extends Component<LegendProps> {
85 style: Style;
86 itemWidth: Number;
87 constructor(props) {
88 super(props);
89 this.state = {
90 filtered: {},
91 items: [],
92 };
93 }
94
95 getOriginItems() {
96 const { chart } = this.props;
97 return chart.getLegendItems();
98 }
99
100 getItems() {
101 const { props, state } = this;
102 const { filtered } = state;
103 const renderItems = props.items?.length ? props.items : this.getOriginItems();
104 if (!renderItems) return null;
105 return renderItems.map((item) => {
106 const { tickValue } = item;
107 return {
108 ...item,
109 filtered: filtered[tickValue],
110 };
111 });
112 }
113
114 setItems(items) {
115 this.setState({
116 items,
117 });
118 }
119
120 getMaxItemBox(legendShape) {
121 let maxItemWidth = 0;
122 let maxItemHeight = 0;
123 (legendShape.get('children') || []).forEach((child) => {
124 const { width, height } = child.get('attrs');
125 maxItemWidth = Math.max(maxItemWidth, width);
126 maxItemHeight = Math.max(maxItemHeight, height);
127 });
128 return {
129 width: maxItemWidth,
130 height: maxItemHeight,
131 };
132 }
133
134 // 计算 legend 的位置
135 _init() {
136 const { props, context } = this;
137 const {
138 // @ts-ignore
139 layout: parentLayout,
140 width: customWidth,
141 height: customHeight,
142 position = 'top',
143 } = props;
144 const items = this.getItems();
145 if (!items || !items.length) return;
146 const { left, top, right, bottom, width: layoutWidth, height: layoutHeight } = parentLayout;
147 const width = context.px2hd(customWidth) || layoutWidth;
148 const shape = renderShape(this, this.render(), false);
149 const { width: itemMaxWidth, height: itemMaxHeight } = this.getMaxItemBox(shape);
150 // 每行最多的个数
151 const lineMaxCount = Math.floor(width / itemMaxWidth);
152 const itemCount = items.length;
153 // legend item 的行数
154 const lineCount = Math.ceil(itemCount / lineMaxCount);
155 const itemWidth = width / lineMaxCount;
156 const autoHeight = itemMaxHeight * lineCount;
157
158 const style: Style = {
159 left,
160 top,
161 width,
162 // height 默认自适应
163 height: undefined,
164 flexDirection: 'row',
165 flexWrap: 'wrap',
166 alignItems: 'center',
167 justifyContent: 'flex-start',
168 };
169
170 // 如果只有一行,2端对齐
171 if (lineCount === 1) {
172 style.justifyContent = 'space-between';
173 }
174
175 if (position === 'top') {
176 style.height = customHeight ? customHeight : autoHeight;
177 }
178
179 if (position === 'left') {
180 style.flexDirection = 'column';
181 style.justifyContent = 'center';
182 style.width = itemMaxWidth;
183 style.height = customHeight ? customHeight : layoutHeight;
184 }
185
186 if (position === 'right') {
187 style.flexDirection = 'column';
188 style.alignItems = 'flex-start';
189 style.justifyContent = 'center';
190 style.width = itemMaxWidth;
191 style.height = customHeight ? customHeight : layoutHeight;
192 style.left = right - itemMaxWidth;
193 }
194
195 if (position === 'bottom') {
196 style.top = bottom - autoHeight;
197 style.height = customHeight ? customHeight : autoHeight;
198 }
199
200 this.itemWidth = itemWidth;
201 this.style = style;
202
203 shape.remove();
204 }
205
206 updateCoord() {
207 const { context, props, style } = this;
208 const { position = 'top', margin = '30px', chart } = props;
209 const { width, height } = style;
210 const marginNumber = context.px2hd(margin);
211
212 chart.updateCoordFor(this, {
213 position,
214 width: width + marginNumber,
215 height: height + marginNumber,
216 });
217 }
218
219 willMount() {
220 const items = this.getItems();
221 if (!items || !items.length) return;
222 this._init();
223 this.updateCoord();
224 }
225
226 didMount() {
227 this._initEvent();
228 }
229
230 willUpdate(): void {
231 const items = this.getItems();
232 if (!items || !items.length) return;
233 this.updateCoord();
234 }
235
236 _initEvent() {
237 const { context, props, container } = this;
238 const { canvas } = context;
239 const { chart, clickable = true, onClick } = props;
240
241 if (!clickable) return;
242
243 // item 点击事件
244 canvas.on('click', (ev) => {
245 const { points } = ev;
246 const point = points[0];
247 const bbox = container.getBBox();
248 if (!isInBBox(bbox, point)) {
249 return;
250 }
251 const legendItems = getElementsByClassName('legend-item', container);
252 if (!legendItems.length) {
253 return;
254 }
255 const clickItem = find(legendItems, (item) => {
256 const itemBBox = item.getBBox();
257 return isInBBox(itemBBox, point);
258 });
259 if (!clickItem) {
260 return;
261 }
262 const dataItem = clickItem.get('data-item');
263 if (!dataItem) {
264 return;
265 }
266 if (isFunction(onClick)) {
267 onClick(dataItem);
268 }
269 const { field, tickValue } = dataItem;
270
271 const { filtered: prevFiltered } = this.state;
272 const filtered = {
273 ...prevFiltered,
274 [tickValue]: !prevFiltered[tickValue],
275 };
276 this.setState({
277 filtered,
278 });
279 chart.filter(field, (value) => {
280 return !filtered[value];
281 });
282 });
283 }
284
285 render() {
286 const { props, itemWidth, style } = this;
287 const items = this.getItems();
288 if (!items || !items.length) {
289 return null;
290 }
291
292 return (
293 <View
294 {...props}
295 items={items}
296 itemWidth={itemWidth}
297 style={{
298 ...style,
299 ...props.style,
300 }}
301 />
302 );
303 }
304 };
305};