import { jsx, Component, computeLayout, GroupStyleProps, TextStyleProps } from '@antv/f-engine'; import { ChartChildProps } from '../../chart'; import { isFunction } from '@antv/util'; interface LegendItem { /** * 标记颜色。 */ color?: string; /** * 名称。 */ name?: string; /** * 值。 */ value?: string | number; /** * 图例标记。 */ marker?: string; [key: string]: any; } export interface LegendProps { /** * 图例的显示位置。默认为 top。 */ position?: 'right' | 'left' | 'top' | 'bottom'; /** * 布局模式:'uniform' 统一宽度(默认),'adaptive' 自适应宽度 */ layoutMode?: 'uniform' | 'adaptive'; /** * 图例宽度 uniform 模式下生效 */ width?: number | string; /** * 图例高度 */ height?: number | string; /** * legend 和图表内容的间距 */ margin?: number | string; /** * 回调函数,用于格式化图例每项的文本显示。 */ itemFormatter?: (value, name) => string; /** * 图例项列表。 */ items?: LegendItem[]; /** * 图例样式。 */ style?: GroupStyleProps; /** * 图例标记。 */ marker?: 'circle' | 'square' | 'line'; /** * 用于设置图例项的样式 */ itemStyle?: GroupStyleProps; /** * 用于设置图例项的文本样式 */ nameStyle?: Omit; /** * 用于设置图例项的文本样式 */ valueStyle?: Omit; /** * value展示文案的前缀 */ valuePrefix?: string; /** * 是否可点击 */ clickable?: boolean; onClick?: (item: LegendItem) => void; clickMode?: 'filter' | 'highlight'; } export default (View) => { return class Legend extends Component< IProps & ChartChildProps > { legendStyle: GroupStyleProps; itemWidth: Number; constructor(props) { super(props); this.state = { filtered: {}, highlighted: {}, items: [], }; } getOriginItems() { const { chart } = this.props; return chart.getLegendItems(); } getItems() { const { props, state } = this; const { filtered, highlighted } = state; const renderItems = props.items?.length ? props.items : this.getOriginItems(); if (!renderItems) return null; return renderItems.map((item) => { const { tickValue } = item; return { ...item, filtered: filtered[tickValue], highlighted: highlighted[tickValue], }; }); } setItems(items) { this.setState({ items, }); } getMaxItemBox(node) { let maxItemWidth = 0; let maxItemHeight = 0; (node.children || []).forEach((child) => { const { layout } = child; const { width, height } = layout; maxItemWidth = Math.max(maxItemWidth, width); maxItemHeight = Math.max(maxItemHeight, height); }); return { width: maxItemWidth, height: maxItemHeight, }; } getItemBoxes(node) { return (node.children || []).map((child) => { const { layout } = child; const { width, height } = layout; return { width, height }; }); } // 计算 legend 的位置 _init() { const { props, context } = this; const { // @ts-ignore layout: parentLayout, width: customWidth, height: customHeight, position = 'top', layoutMode = 'uniform', } = props; const items = this.getItems(); if (!items || !items.length) return; const { left, top, width: layoutWidth, height: layoutHeight } = parentLayout; const width = context.px2hd(customWidth) || layoutWidth; const node = computeLayout(this, this.render()); const { width: itemMaxWidth, height: itemMaxHeight } = this.getMaxItemBox(node); let lineCount, itemWidth; if (layoutMode === 'adaptive') { const labelBBoxes = this.getItemBoxes(node); let pos = 0; lineCount = 1; for (const { width: boxWidth } of labelBBoxes) { if (pos + boxWidth > width) { pos = boxWidth; lineCount++; } else { pos += boxWidth; } } itemWidth = undefined; } else { // uniform模式 const lineMaxCount = Math.max(1, Math.floor(width / itemMaxWidth)); const itemCount = items.length; lineCount = Math.ceil(itemCount / lineMaxCount); itemWidth = width / lineMaxCount; } const autoHeight = itemMaxHeight * lineCount; const style: GroupStyleProps = { left, top, width, // height 默认自适应 height: undefined, flexDirection: 'row', flexWrap: 'wrap', alignItems: 'center', justifyContent: 'flex-start', }; // 如果只有一行,2端对齐 if (lineCount === 1) { style.justifyContent = 'space-between'; } if (position === 'top') { style.height = customHeight ? customHeight : autoHeight; } if (position === 'left') { style.flexDirection = 'column'; style.justifyContent = 'center'; style.alignItems = layoutMode === 'adaptive' ? 'flex-start' : 'center'; style.width = itemMaxWidth; style.height = customHeight ? customHeight : layoutHeight; } if (position === 'right') { style.flexDirection = 'column'; style.alignItems = 'flex-start'; style.justifyContent = 'center'; style.left = left + (width - itemMaxWidth); style.width = itemMaxWidth; style.height = customHeight ? customHeight : layoutHeight; } if (position === 'bottom') { style.top = top + (layoutHeight - autoHeight); style.height = customHeight ? customHeight : autoHeight; } this.itemWidth = itemWidth; this.legendStyle = style; } updateCoord() { const { context, props, legendStyle } = this; const { position = 'top', margin = '30px', chart } = props; const { width, height } = legendStyle; const marginNumber = context.px2hd(margin); chart.updateCoordFor(this, { position, width: width + marginNumber, height: height + marginNumber, }); } willMount() { const items = this.getItems(); if (!items || !items.length) return; this._init(); this.updateCoord(); } didMount() { // this._initEvent(); } willUpdate(): void { const items = this.getItems(); if (!items || !items.length) return; this._init(); this.updateCoord(); } _onclick = (item) => { const { props } = this; const { chart, clickable = true, onClick, clickMode = 'filter' } = props; if (!clickable) return; const clickItem = item.currentTarget; if (!clickItem) return; // @ts-ignore const dataItem = clickItem.config['data-item']; if (!dataItem) return; if (isFunction(onClick)) { onClick(dataItem); } const { field, tickValue } = dataItem; const { filtered: prevFiltered, highlighted: preHighlighted } = this.state; const filtered = { ...prevFiltered, [tickValue]: !prevFiltered[tickValue], }; if (clickMode === 'filter') { this.setState({ filtered, }); chart.filter(field, (value) => { return !filtered[value]; }); } else if (clickMode === 'highlight') { const highlighted = { [tickValue]: !preHighlighted[tickValue], }; this.setState({ highlighted, }); chart.highlight( field, preHighlighted[tickValue] ? null : (value) => { return highlighted[value]; } ); } }; render() { const { props, itemWidth, legendStyle } = this; const items = this.getItems(); if (!items || !items.length) { return null; } return ( ); } }; };